just-usage 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.4",
19
- description: "One local page for your coding-CLI subscription quotas: Claude, Codex, Cursor, OpenCode Go.",
18
+ version: "0.0.6",
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 --watch src/cli.ts",
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
@@ -477,12 +481,15 @@ async function codexLogin(codexHome, onAuthUrl, timeoutMs = 5 * 60000) {
477
481
 
478
482
  // src/adapters/claude.ts
479
483
  import { createHash } from "node:crypto";
480
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
481
- import { homedir as homedir2 } from "node:os";
482
- import { join as join2 } from "node:path";
484
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
485
+ import { homedir as homedir3 } from "node:os";
486
+ import { join as join3 } from "node:path";
483
487
 
484
488
  // src/proc.ts
485
489
  import { spawn as spawn2 } from "node:child_process";
490
+ import { existsSync } from "node:fs";
491
+ import { homedir as homedir2 } from "node:os";
492
+ import { delimiter, join as join2 } from "node:path";
486
493
  function run(cmd, args, opts = {}) {
487
494
  return new Promise((resolve) => {
488
495
  let stdout = "";
@@ -496,7 +503,7 @@ function run(cmd, args, opts = {}) {
496
503
  resolve({ code, stdout, stderr });
497
504
  };
498
505
  const child = spawn2(cmd, args, {
499
- env: { ...process.env, ...opts.env },
506
+ env: spawnEnv(opts.env),
500
507
  stdio: ["pipe", "pipe", "pipe"]
501
508
  });
502
509
  const timer = setTimeout(() => {
@@ -519,23 +526,70 @@ function run(cmd, args, opts = {}) {
519
526
  }
520
527
  function runInteractive(cmd, args, env) {
521
528
  return new Promise((resolve, reject) => {
522
- const child = spawn2(cmd, args, { env: { ...process.env, ...env }, stdio: "inherit" });
529
+ const child = spawn2(cmd, args, { env: spawnEnv(env), stdio: "inherit" });
523
530
  child.on("error", reject);
524
531
  child.on("close", (code) => resolve(code));
525
532
  });
526
533
  }
527
534
  var whichCache = new Map;
528
- function which(bin) {
535
+ function extraBinDirs() {
536
+ const home = homedir2();
537
+ const dirs = [
538
+ join2(home, ".local", "bin"),
539
+ join2(home, "bin"),
540
+ "/opt/homebrew/bin",
541
+ "/usr/local/bin",
542
+ "/home/linuxbrew/.linuxbrew/bin",
543
+ join2(home, ".npm-global", "bin")
544
+ ];
545
+ if (process.platform === "win32") {
546
+ const local = process.env.LOCALAPPDATA;
547
+ if (local)
548
+ dirs.push(join2(local, "agy", "bin"));
549
+ }
550
+ return dirs.filter((d) => existsSync(d));
551
+ }
552
+ function spawnEnv(extra) {
553
+ const pathKey = process.platform === "win32" && process.env.Path && !process.env.PATH ? "Path" : "PATH";
554
+ const current = extra?.[pathKey] ?? extra?.PATH ?? process.env[pathKey] ?? process.env.PATH ?? "";
555
+ const seen = new Set;
556
+ const parts = [];
557
+ for (const dir of [...extraBinDirs(), ...current.split(delimiter)]) {
558
+ if (!dir || seen.has(dir))
559
+ continue;
560
+ seen.add(dir);
561
+ parts.push(dir);
562
+ }
563
+ return { ...process.env, ...extra, [pathKey]: parts.join(delimiter) };
564
+ }
565
+ async function resolveBin(bin) {
566
+ const finder = process.platform === "win32" ? "where" : "which";
567
+ const res = await run(finder, [bin], { timeoutMs: 5000 });
568
+ if (res.code === 0) {
569
+ const first = res.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
570
+ if (first && existsSync(first))
571
+ return first;
572
+ }
573
+ const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`] : [bin];
574
+ for (const dir of extraBinDirs()) {
575
+ for (const name of names) {
576
+ const candidate = join2(dir, name);
577
+ if (existsSync(candidate))
578
+ return candidate;
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+ function clearBinCache() {
584
+ whichCache.clear();
585
+ versionCache.clear();
586
+ }
587
+ function which(bin, opts = {}) {
588
+ if (opts.fresh)
589
+ whichCache.delete(bin);
529
590
  let p = whichCache.get(bin);
530
591
  if (!p) {
531
- p = (async () => {
532
- const finder = process.platform === "win32" ? "where" : "which";
533
- const res = await run(finder, [bin], { timeoutMs: 5000 });
534
- if (res.code !== 0)
535
- return null;
536
- const first = res.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
537
- return first ?? null;
538
- })();
592
+ p = resolveBin(bin);
539
593
  whichCache.set(bin, p);
540
594
  }
541
595
  return p;
@@ -577,7 +631,7 @@ function withTimeout(p, ms, label) {
577
631
  }
578
632
 
579
633
  // src/secrets.ts
580
- import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
634
+ import { existsSync as existsSync2, readFileSync, writeFileSync, chmodSync } from "node:fs";
581
635
  import { dirname } from "node:path";
582
636
  var SERVICE = "just-usage";
583
637
 
@@ -604,7 +658,7 @@ class FileStore {
604
658
  kind = "file";
605
659
  read() {
606
660
  const file = paths.secrets();
607
- if (!existsSync(file))
661
+ if (!existsSync2(file))
608
662
  return {};
609
663
  try {
610
664
  return JSON.parse(readFileSync(file, "utf8"));
@@ -676,8 +730,8 @@ async function readClaudeCredentials(configDir) {
676
730
  return creds;
677
731
  }
678
732
  }
679
- const file = join2(configDir ?? join2(homedir2(), ".claude"), ".credentials.json");
680
- if (existsSync2(file))
733
+ const file = join3(configDir ?? join3(homedir3(), ".claude"), ".credentials.json");
734
+ if (existsSync3(file))
681
735
  return parseCreds(readFileSync2(file, "utf8"));
682
736
  return null;
683
737
  }
@@ -828,24 +882,389 @@ async function verifyClaudeToken(token) {
828
882
  // src/accounts.ts
829
883
  import { renameSync, rmSync as rmSync2 } from "node:fs";
830
884
 
885
+ // src/adapters/antigravity.ts
886
+ import { createHash as createHash2, randomBytes } from "node:crypto";
887
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
888
+ import { homedir as homedir4 } from "node:os";
889
+ import { join as join4 } from "node:path";
890
+ var AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
891
+ var AGY_CLIENT_SECRET = "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";
892
+ var AGY_REDIRECT_URI = "https://antigravity.google/oauth-callback";
893
+ var AGY_SCOPES = [
894
+ "openid",
895
+ "https://www.googleapis.com/auth/userinfo.email",
896
+ "https://www.googleapis.com/auth/userinfo.profile",
897
+ "https://www.googleapis.com/auth/cloud-platform",
898
+ "https://www.googleapis.com/auth/cclog",
899
+ "https://www.googleapis.com/auth/experimentsandconfigs"
900
+ ];
901
+ var USAGE_HOST = "https://daily-cloudcode-pa.googleapis.com";
902
+ var KEYCHAIN_SERVICE = "gemini";
903
+ var KEYCHAIN_ACCOUNT = "antigravity";
904
+ var FALLBACK_CLI_VERSION2 = "1.1.26";
905
+ function settingsFile() {
906
+ return join4(homedir4(), ".gemini", "antigravity-cli", "settings.json");
907
+ }
908
+ function usesGeminiApiKey() {
909
+ try {
910
+ if (!existsSync4(settingsFile()))
911
+ return false;
912
+ const parsed = JSON.parse(readFileSync3(settingsFile(), "utf8"));
913
+ return parsed.modelProvider === "gemini" && Boolean(process.env.GEMINI_API_KEY?.trim());
914
+ } catch {
915
+ return false;
916
+ }
917
+ }
918
+ function parseAgyKeyringBlob(raw) {
919
+ const text = raw.replace(/\r?\n$/, "");
920
+ const prefix = "go-keyring-base64:";
921
+ const json = text.startsWith(prefix) ? Buffer.from(text.slice(prefix.length), "base64").toString("utf8") : text;
922
+ try {
923
+ const parsed = JSON.parse(json);
924
+ const token = isObject(parsed.token) ? parsed.token : parsed;
925
+ if (typeof token.access_token !== "string" || !token.access_token)
926
+ return null;
927
+ if (typeof token.refresh_token !== "string" || !token.refresh_token)
928
+ return null;
929
+ const expiry = typeof token.expiry === "string" ? Date.parse(token.expiry) : typeof token.expiry === "number" ? token.expiry : NaN;
930
+ return {
931
+ accessToken: token.access_token,
932
+ refreshToken: token.refresh_token,
933
+ expiry: Number.isFinite(expiry) ? expiry : null
934
+ };
935
+ } catch {
936
+ return null;
937
+ }
938
+ }
939
+ function parseStoredAgySecret(raw) {
940
+ try {
941
+ const parsed = JSON.parse(raw);
942
+ if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token)
943
+ return parseAgyKeyringBlob(raw);
944
+ return {
945
+ accessToken: typeof parsed.access_token === "string" ? parsed.access_token : "",
946
+ refreshToken: parsed.refresh_token,
947
+ expiry: typeof parsed.expiry === "number" ? parsed.expiry : typeof parsed.expiry === "string" ? Date.parse(parsed.expiry) || null : null
948
+ };
949
+ } catch {
950
+ return parseAgyKeyringBlob(raw);
951
+ }
952
+ }
953
+ function serializeAgySecret(token) {
954
+ return JSON.stringify({
955
+ access_token: token.accessToken,
956
+ refresh_token: token.refreshToken,
957
+ expiry: token.expiry
958
+ });
959
+ }
960
+ async function tokenFromKeychain() {
961
+ if (process.platform !== "darwin")
962
+ return null;
963
+ const res = await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], { timeoutMs: 20000 });
964
+ if (res.code !== 0)
965
+ return null;
966
+ return parseAgyKeyringBlob(res.stdout);
967
+ }
968
+ async function tokenFromSecretTool() {
969
+ if (process.platform === "darwin" || process.platform === "win32")
970
+ return null;
971
+ const res = await run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "username", KEYCHAIN_ACCOUNT], { timeoutMs: 20000 });
972
+ if (res.code !== 0)
973
+ return null;
974
+ return parseAgyKeyringBlob(res.stdout);
975
+ }
976
+ function tokenFromOauthFile() {
977
+ const file = join4(homedir4(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
978
+ if (!existsSync4(file))
979
+ return null;
980
+ try {
981
+ return parseAgyKeyringBlob(readFileSync3(file, "utf8"));
982
+ } catch {
983
+ return null;
984
+ }
985
+ }
986
+ async function readDefaultAgyToken() {
987
+ return await tokenFromKeychain() ?? await tokenFromSecretTool() ?? tokenFromOauthFile();
988
+ }
989
+ function createPkce() {
990
+ const verifier = randomBytes(32).toString("base64url");
991
+ const challenge = createHash2("sha256").update(verifier).digest("base64url");
992
+ return { verifier, challenge };
993
+ }
994
+ function antigravityAuthUrl(opts) {
995
+ const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
996
+ url.searchParams.set("client_id", AGY_CLIENT_ID);
997
+ url.searchParams.set("redirect_uri", AGY_REDIRECT_URI);
998
+ url.searchParams.set("response_type", "code");
999
+ url.searchParams.set("scope", AGY_SCOPES.join(" "));
1000
+ url.searchParams.set("access_type", "offline");
1001
+ url.searchParams.set("prompt", "consent");
1002
+ url.searchParams.set("state", opts.state);
1003
+ url.searchParams.set("code_challenge", opts.challenge);
1004
+ url.searchParams.set("code_challenge_method", "S256");
1005
+ return url.toString();
1006
+ }
1007
+ function isAntigravityCallbackUrl(raw) {
1008
+ let u;
1009
+ try {
1010
+ u = new URL(raw.trim());
1011
+ } catch {
1012
+ return false;
1013
+ }
1014
+ return u.protocol === "https:" && u.hostname === "antigravity.google" && u.pathname === "/oauth-callback" && Boolean(u.searchParams.get("code"));
1015
+ }
1016
+ function parseAntigravityAuthInput(raw) {
1017
+ const text = raw.trim().replace(/^["']|["']$/g, "");
1018
+ if (!text)
1019
+ return null;
1020
+ if (isAntigravityCallbackUrl(text)) {
1021
+ const u = new URL(text);
1022
+ return { code: u.searchParams.get("code"), state: u.searchParams.get("state") };
1023
+ }
1024
+ if (/^https?:\/\//i.test(text))
1025
+ return null;
1026
+ if (/^4\/[A-Za-z0-9_\-/]+$/.test(text) && text.length >= 20)
1027
+ return { code: text, state: null };
1028
+ return null;
1029
+ }
1030
+ async function userAgent2() {
1031
+ const v = await binVersion("agy") ?? FALLBACK_CLI_VERSION2;
1032
+ return `antigravity-cli/${v}`;
1033
+ }
1034
+ function headers2(token, ua) {
1035
+ return {
1036
+ Authorization: `Bearer ${token}`,
1037
+ "Content-Type": "application/json",
1038
+ Accept: "application/json",
1039
+ "User-Agent": ua
1040
+ };
1041
+ }
1042
+ async function refreshAgyToken(refreshToken) {
1043
+ const res = await fetchJson("https://oauth2.googleapis.com/token", {
1044
+ method: "POST",
1045
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1046
+ body: new URLSearchParams({
1047
+ client_id: AGY_CLIENT_ID,
1048
+ client_secret: AGY_CLIENT_SECRET,
1049
+ refresh_token: refreshToken,
1050
+ grant_type: "refresh_token"
1051
+ }).toString()
1052
+ });
1053
+ if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string")
1054
+ return null;
1055
+ const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
1056
+ return {
1057
+ accessToken: res.body.access_token,
1058
+ refreshToken: typeof res.body.refresh_token === "string" && res.body.refresh_token ? res.body.refresh_token : refreshToken,
1059
+ expiry: Date.now() + expiresIn * 1000
1060
+ };
1061
+ }
1062
+ async function exchangeAntigravityCode(code, verifier) {
1063
+ const res = await fetchJson("https://oauth2.googleapis.com/token", {
1064
+ method: "POST",
1065
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1066
+ body: new URLSearchParams({
1067
+ client_id: AGY_CLIENT_ID,
1068
+ client_secret: AGY_CLIENT_SECRET,
1069
+ code,
1070
+ code_verifier: verifier,
1071
+ grant_type: "authorization_code",
1072
+ redirect_uri: AGY_REDIRECT_URI
1073
+ }).toString()
1074
+ });
1075
+ if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string" || typeof res.body.refresh_token !== "string") {
1076
+ throw new Error(isObject(res.body) && typeof res.body.error_description === "string" ? res.body.error_description : `Token exchange failed (HTTP ${res.status}).`);
1077
+ }
1078
+ const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
1079
+ return {
1080
+ accessToken: res.body.access_token,
1081
+ refreshToken: res.body.refresh_token,
1082
+ expiry: Date.now() + expiresIn * 1000
1083
+ };
1084
+ }
1085
+ async function fetchAgyEmail(accessToken) {
1086
+ const res = await fetchJson("https://www.googleapis.com/oauth2/v2/userinfo", {
1087
+ headers: { Authorization: `Bearer ${accessToken}` }
1088
+ });
1089
+ if (res.status !== 200 || !isObject(res.body) || typeof res.body.email !== "string")
1090
+ return null;
1091
+ return res.body.email;
1092
+ }
1093
+ var GOOGLE_AI_PLANS = {
1094
+ "g1-plus-tier": "Plus",
1095
+ "g1-pro-tier": "Pro",
1096
+ "g1-ultra-tier": "Ultra"
1097
+ };
1098
+ var PRODUCT_TIER_NAMES = /^(antigravity|gemini code assist)$/i;
1099
+ function planFromTier(tier) {
1100
+ if (!tier)
1101
+ return null;
1102
+ const id = typeof tier.id === "string" ? tier.id.trim().toLowerCase() : "";
1103
+ if (id && GOOGLE_AI_PLANS[id])
1104
+ return GOOGLE_AI_PLANS[id];
1105
+ if (id.includes("ultra"))
1106
+ return "Ultra";
1107
+ if (id.includes("pro"))
1108
+ return "Pro";
1109
+ if (id.includes("plus"))
1110
+ return "Plus";
1111
+ const name = typeof tier.name === "string" ? tier.name.trim() : "";
1112
+ if (name && !PRODUCT_TIER_NAMES.test(name))
1113
+ return name;
1114
+ if (id === "free-tier")
1115
+ return "Free";
1116
+ if (id === "standard-tier")
1117
+ return "Standard";
1118
+ if (id === "legacy-tier")
1119
+ return "Legacy";
1120
+ return name || null;
1121
+ }
1122
+ function planFromCodeAssist(body) {
1123
+ if (!isObject(body))
1124
+ return null;
1125
+ const current = isObject(body.currentTier) ? body.currentTier : null;
1126
+ const paid = isObject(body.paidTier) ? body.paidTier : null;
1127
+ return planFromTier(paid) ?? planFromTier(current);
1128
+ }
1129
+ function windowMinutes(window) {
1130
+ const key = (window ?? "").toLowerCase().replace(/[_-]/g, "");
1131
+ if (key === "5h" || key === "fivehour" || key === "fivehours")
1132
+ return 300;
1133
+ if (key === "weekly" || key === "week")
1134
+ return 10080;
1135
+ return null;
1136
+ }
1137
+ function usageLabel(minutes, fallback) {
1138
+ if (minutes === 300)
1139
+ return "5h Usage";
1140
+ if (minutes === 10080)
1141
+ return "Weekly Usage";
1142
+ return fallback.replace(/\s+remaining$/i, "").trim() || "Usage";
1143
+ }
1144
+ function normalizeAntigravityQuota(body) {
1145
+ if (!isObject(body) || !Array.isArray(body.groups))
1146
+ return [];
1147
+ const out = [];
1148
+ for (const group of body.groups) {
1149
+ if (!isObject(group) || !Array.isArray(group.buckets))
1150
+ continue;
1151
+ const rawGroup = typeof group.displayName === "string" && group.displayName.trim() ? group.displayName.trim() : undefined;
1152
+ const groupName = rawGroup && !/^gemini models$/i.test(rawGroup) ? rawGroup : undefined;
1153
+ for (const bucket of group.buckets) {
1154
+ if (!isObject(bucket) || bucket.disabled === true)
1155
+ continue;
1156
+ const remaining = typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : null;
1157
+ const used = remaining === null ? null : clampPercent((1 - remaining) * 100);
1158
+ if (used === null)
1159
+ continue;
1160
+ const id = typeof bucket.bucketId === "string" && bucket.bucketId ? bucket.bucketId : `${groupName ?? "quota"}:${out.length}`;
1161
+ const minutes = windowMinutes(typeof bucket.window === "string" ? bucket.window : undefined);
1162
+ const fallback = typeof bucket.displayName === "string" ? bucket.displayName : "Usage";
1163
+ out.push({
1164
+ id,
1165
+ label: usageLabel(minutes, fallback),
1166
+ group: groupName,
1167
+ usedPercent: used,
1168
+ resetsAt: isoOrNull(bucket.resetTime),
1169
+ windowMinutes: minutes,
1170
+ kind: "rolling"
1171
+ });
1172
+ }
1173
+ }
1174
+ return out;
1175
+ }
1176
+ async function resolveToken2(account) {
1177
+ if (account.kind === "token") {
1178
+ const raw = await secretStore().get(account.id);
1179
+ if (!raw)
1180
+ return { fail: snapshot(account, "error", { message: "Stored Google session missing. Remove the account and sign in again." }) };
1181
+ const parsed = parseStoredAgySecret(raw);
1182
+ if (!parsed)
1183
+ return { fail: snapshot(account, "error", { message: "Stored Google session is unreadable. Remove the account and sign in again." }) };
1184
+ return { token: parsed };
1185
+ }
1186
+ const token = await readDefaultAgyToken();
1187
+ if (!token) {
1188
+ if (usesGeminiApiKey()) {
1189
+ return { fail: snapshot(account, "unsupported", { message: "This CLI is using a Gemini API key, which has no subscription quota." }) };
1190
+ }
1191
+ return { fail: snapshot(account, "signed_out", { message: "Not signed in. Run `agy` and complete Google sign-in." }) };
1192
+ }
1193
+ return { token };
1194
+ }
1195
+ async function liveToken(account, token) {
1196
+ if (token.expiry && token.expiry > Date.now() + 60000 && token.accessToken)
1197
+ return token;
1198
+ const refreshed = await refreshAgyToken(token.refreshToken);
1199
+ if (!refreshed) {
1200
+ return { fail: snapshot(account, "error", { message: "Google session expired. Sign in again with `agy`." }) };
1201
+ }
1202
+ if (account.kind === "token") {
1203
+ await secretStore().set(account.id, serializeAgySecret(refreshed));
1204
+ }
1205
+ return refreshed;
1206
+ }
1207
+ async function fetchAntigravity(account) {
1208
+ try {
1209
+ const resolved = await resolveToken2(account);
1210
+ if ("fail" in resolved)
1211
+ return resolved.fail;
1212
+ const live = await liveToken(account, resolved.token);
1213
+ if ("fail" in live)
1214
+ return live.fail;
1215
+ const ua = await userAgent2();
1216
+ const h = headers2(live.accessToken, ua);
1217
+ const [usage, assist, email] = await Promise.all([
1218
+ fetchJson(`${USAGE_HOST}/v1internal:retrieveUserQuotaSummary`, { method: "POST", headers: h, body: "{}" }),
1219
+ fetchJson(`${USAGE_HOST}/v1internal:loadCodeAssist`, {
1220
+ method: "POST",
1221
+ headers: h,
1222
+ body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } })
1223
+ }),
1224
+ account.email ? Promise.resolve(account.email) : fetchAgyEmail(live.accessToken)
1225
+ ]);
1226
+ const plan = assist.status === 200 ? planFromCodeAssist(assist.body) : null;
1227
+ if (usage.status === 401) {
1228
+ return snapshot(account, "error", { email, plan, message: "Google session rejected (401). Sign in again." });
1229
+ }
1230
+ if (usage.status === 403) {
1231
+ return snapshot(account, "unsupported", {
1232
+ email,
1233
+ plan,
1234
+ message: "This Google account has no Antigravity quota. A Google AI Pro / Antigravity subscription is required."
1235
+ });
1236
+ }
1237
+ if (usage.status !== 200) {
1238
+ return snapshot(account, "error", { email, plan, message: `Usage endpoint returned HTTP ${usage.status}.` });
1239
+ }
1240
+ const windows = normalizeAntigravityQuota(usage.body);
1241
+ if (windows.length === 0) {
1242
+ return snapshot(account, "unsupported", { email, plan, message: "Usage response had no recognizable windows (schema may have changed)." });
1243
+ }
1244
+ return snapshot(account, "ok", { email, plan, windows });
1245
+ } catch (e) {
1246
+ return snapshot(account, "error", { message: errorMessage(e) });
1247
+ }
1248
+ }
1249
+
831
1250
  // src/adapters/opencode.ts
832
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
833
- import { homedir as homedir3 } from "node:os";
834
- import { join as join3 } from "node:path";
1251
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
1252
+ import { homedir as homedir5 } from "node:os";
1253
+ import { join as join5 } from "node:path";
835
1254
  var USAGE_URL2 = "https://opencode.ai/zen/go/v1/usage";
836
1255
  function openCodeAuthFile() {
837
1256
  if (process.env.OPENCODE_AUTH_FILE)
838
1257
  return process.env.OPENCODE_AUTH_FILE;
839
1258
  const xdg = process.env.XDG_DATA_HOME;
840
- const base = xdg && xdg.trim() ? xdg : join3(homedir3(), ".local", "share");
841
- return join3(base, "opencode", "auth.json");
1259
+ const base = xdg && xdg.trim() ? xdg : join5(homedir5(), ".local", "share");
1260
+ return join5(base, "opencode", "auth.json");
842
1261
  }
843
1262
  function readOpenCodeGoKey() {
844
1263
  const file = openCodeAuthFile();
845
- if (!existsSync3(file))
1264
+ if (!existsSync5(file))
846
1265
  return null;
847
1266
  try {
848
- const parsed = JSON.parse(readFileSync3(file, "utf8"));
1267
+ const parsed = JSON.parse(readFileSync4(file, "utf8"));
849
1268
  const entry = parsed["opencode-go"];
850
1269
  if (isObject(entry) && typeof entry.key === "string" && entry.key)
851
1270
  return entry.key;
@@ -904,15 +1323,73 @@ async function fetchOpenCode(account) {
904
1323
  }
905
1324
  }
906
1325
 
1326
+ // src/log.ts
1327
+ import { appendFileSync } from "node:fs";
1328
+ import { homedir as homedir6 } from "node:os";
1329
+ import { join as join6 } from "node:path";
1330
+ var SECRET_KEY = /^(.*[._-]?)?(secret|token|password|authorization|cookie|verifier|refresh_token|access_token|api[_-]?key|key)$/i;
1331
+ var SKIP_KEY = /^(url|authurl|callback|body|headers|authorization)$/i;
1332
+ var MAX_STRING = 400;
1333
+ function logDir() {
1334
+ const override = process.env.JUST_USAGE_LOG_DIR;
1335
+ if (override && override.trim())
1336
+ return override.trim();
1337
+ return join6(homedir6(), ".just-usage", "logs");
1338
+ }
1339
+ function logFile(at = new Date) {
1340
+ return join6(logDir(), `${at.toISOString().slice(0, 10)}.log`);
1341
+ }
1342
+ function sanitizeFields(fields) {
1343
+ if (!fields)
1344
+ return {};
1345
+ const out = {};
1346
+ for (const [k, v] of Object.entries(fields)) {
1347
+ if (v === undefined)
1348
+ continue;
1349
+ if (SECRET_KEY.test(k) || SKIP_KEY.test(k))
1350
+ continue;
1351
+ if (v === null || typeof v === "number" || typeof v === "boolean") {
1352
+ out[k] = v;
1353
+ continue;
1354
+ }
1355
+ if (typeof v === "string") {
1356
+ out[k] = v.length <= MAX_STRING ? v : `${v.slice(0, MAX_STRING)}…`;
1357
+ continue;
1358
+ }
1359
+ if (Array.isArray(v) && v.every((x) => typeof x === "string" || typeof x === "number")) {
1360
+ out[k] = v.slice(0, 20);
1361
+ }
1362
+ }
1363
+ return out;
1364
+ }
1365
+ function log(level, event, fields) {
1366
+ try {
1367
+ ensureDir(logDir());
1368
+ const line = JSON.stringify({
1369
+ ts: new Date().toISOString(),
1370
+ level,
1371
+ event,
1372
+ pid: process.pid,
1373
+ v: VERSION,
1374
+ ...sanitizeFields(fields)
1375
+ });
1376
+ appendFileSync(logFile(), `${line}
1377
+ `, { encoding: "utf8", mode: 384 });
1378
+ } catch {}
1379
+ }
1380
+ function logError(event, e, fields) {
1381
+ log("error", event, { ...fields, message: errorMessage(e) });
1382
+ }
1383
+
907
1384
  // src/registry.ts
908
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, rmSync } from "node:fs";
909
- import { dirname as dirname2, join as join4 } from "node:path";
1385
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "node:fs";
1386
+ import { dirname as dirname2, join as join7 } from "node:path";
910
1387
  function readRegistry() {
911
1388
  const file = paths.registry();
912
- if (!existsSync4(file))
1389
+ if (!existsSync6(file))
913
1390
  return { version: 1, accounts: [] };
914
1391
  try {
915
- const parsed = JSON.parse(readFileSync4(file, "utf8"));
1392
+ const parsed = JSON.parse(readFileSync5(file, "utf8"));
916
1393
  return { version: 1, accounts: Array.isArray(parsed.accounts) ? parsed.accounts : [] };
917
1394
  } catch {
918
1395
  return { version: 1, accounts: [] };
@@ -945,7 +1422,7 @@ function newAccountId(provider, hint) {
945
1422
  }
946
1423
  }
947
1424
  function profileDirFor(provider, id) {
948
- return join4(paths.profiles(provider), id.split(":")[1] ?? "account");
1425
+ return join7(paths.profiles(provider), id.split(":")[1] ?? "account");
949
1426
  }
950
1427
  function saveAccount(record) {
951
1428
  const reg = readRegistry();
@@ -1002,6 +1479,7 @@ async function addClaudeToken(token, label) {
1002
1479
  createdAt: new Date().toISOString()
1003
1480
  };
1004
1481
  saveAccount(account);
1482
+ log("info", "account.add", { account: account.id, provider: account.provider, kind: account.kind });
1005
1483
  return { account };
1006
1484
  }
1007
1485
  async function addOpenCodeKey(key, label) {
@@ -1024,6 +1502,12 @@ async function addOpenCodeKey(key, label) {
1024
1502
  createdAt: new Date().toISOString()
1025
1503
  };
1026
1504
  saveAccount(account);
1505
+ log("info", "account.add", {
1506
+ account: account.id,
1507
+ provider: account.provider,
1508
+ kind: account.kind,
1509
+ warning: status === 403 ? "no-subscription" : undefined
1510
+ });
1027
1511
  return {
1028
1512
  account,
1029
1513
  warning: status === 403 ? "Key is valid but has no active OpenCode Go subscription." : undefined
@@ -1035,7 +1519,9 @@ function renameExtraAccount(id, label) {
1035
1519
  if (!getAccount(id))
1036
1520
  throw new AccountError(`Unknown account: ${id}`, 404);
1037
1521
  updateAccount(id, { label: label.trim() });
1038
- return getAccount(id);
1522
+ const account = getAccount(id);
1523
+ log("info", "account.rename", { account: account.id, provider: account.provider });
1524
+ return account;
1039
1525
  }
1040
1526
  async function removeExtraAccount(id) {
1041
1527
  if (id.endsWith(":default"))
@@ -1045,6 +1531,7 @@ async function removeExtraAccount(id) {
1045
1531
  throw new AccountError(`Unknown account: ${id}`, 404);
1046
1532
  if (rec.kind === "token")
1047
1533
  await secretStore().delete(id);
1534
+ log("info", "account.remove", { account: rec.id, provider: rec.provider, kind: rec.kind });
1048
1535
  return rec;
1049
1536
  }
1050
1537
  function saveCodexProfile(opts) {
@@ -1066,6 +1553,7 @@ function saveCodexProfile(opts) {
1066
1553
  createdAt: new Date().toISOString()
1067
1554
  };
1068
1555
  saveAccount(account);
1556
+ log("info", "account.add", { account: account.id, provider: account.provider, kind: account.kind });
1069
1557
  return account;
1070
1558
  }
1071
1559
  function isLocalCallbackUrl(raw) {
@@ -1105,7 +1593,7 @@ function dropCodexSession(id, removeDir) {
1105
1593
  }
1106
1594
  }
1107
1595
  async function beginCodexAdd(label) {
1108
- if (!await which("codex"))
1596
+ if (!await which("codex", { fresh: true }))
1109
1597
  throw new AccountError("codex is not installed (npm i -g @openai/codex).");
1110
1598
  const tmpId = newAccountId("codex", label?.trim() || "pending");
1111
1599
  const dir = ensureDir(profileDirFor("codex", tmpId));
@@ -1124,11 +1612,14 @@ async function beginCodexAdd(label) {
1124
1612
  handle.completed.then((info) => {
1125
1613
  rec.account = saveCodexProfile({ label: rec.label, tmpId, dir, email: info.email });
1126
1614
  rec.status = "done";
1615
+ log("info", "account.login.done", { account: rec.account.id, provider: "codex" });
1127
1616
  }).catch((e) => {
1128
1617
  rec.status = "error";
1129
1618
  rec.error = e instanceof Error ? e.message : String(e);
1619
+ logError("account.login.error", e, { provider: "codex" });
1130
1620
  });
1131
1621
  codexSessions.set(id, rec);
1622
+ log("info", "account.login.start", { provider: "codex", kind: "add" });
1132
1623
  return { sessionId: id, authUrl: handle.authUrl };
1133
1624
  }
1134
1625
  async function beginCodexRelogin(accountId) {
@@ -1153,11 +1644,14 @@ async function beginCodexRelogin(accountId) {
1153
1644
  updateAccount(existing.id, { email: info.email });
1154
1645
  rec.account = { ...existing, email: info.email };
1155
1646
  rec.status = "done";
1647
+ log("info", "account.login.done", { account: existing.id, provider: "codex" });
1156
1648
  }).catch((e) => {
1157
1649
  rec.status = "error";
1158
1650
  rec.error = e instanceof Error ? e.message : String(e);
1651
+ logError("account.login.error", e, { account: existing.id, provider: "codex" });
1159
1652
  });
1160
1653
  codexSessions.set(id, rec);
1654
+ log("info", "account.login.start", { account: existing.id, provider: "codex", kind: "relogin" });
1161
1655
  return { sessionId: id, authUrl: handle.authUrl };
1162
1656
  }
1163
1657
  function codexSessionStatus(sessionId) {
@@ -1176,50 +1670,151 @@ async function submitCodexCallback(sessionId, url) {
1176
1670
  return;
1177
1671
  await submitLocalCallback(url);
1178
1672
  }
1673
+ var agySessions = new Map;
1674
+ function dropAgySession(id) {
1675
+ const rec = agySessions.get(id);
1676
+ if (!rec)
1677
+ return;
1678
+ clearTimeout(rec.timer);
1679
+ agySessions.delete(id);
1680
+ }
1681
+ function startAgySession(opts) {
1682
+ const pkce = createPkce();
1683
+ const state = crypto.randomUUID();
1684
+ const id = crypto.randomUUID();
1685
+ const authUrl = antigravityAuthUrl({ state, challenge: pkce.challenge });
1686
+ const rec = {
1687
+ id,
1688
+ label: opts.label?.trim() || undefined,
1689
+ accountId: opts.accountId,
1690
+ state,
1691
+ verifier: pkce.verifier,
1692
+ authUrl,
1693
+ status: "waiting",
1694
+ timer: setTimeout(() => dropAgySession(id), SESSION_TTL_MS)
1695
+ };
1696
+ rec.timer.unref?.();
1697
+ agySessions.set(id, rec);
1698
+ return { sessionId: id, authUrl };
1699
+ }
1700
+ async function beginAntigravityAdd(label) {
1701
+ if (!await which("agy", { fresh: true }))
1702
+ throw new AccountError("agy is not installed (https://antigravity.google/docs/cli/install).");
1703
+ const started = startAgySession({ label });
1704
+ log("info", "account.login.start", { provider: "antigravity", kind: "add" });
1705
+ return started;
1706
+ }
1707
+ async function beginAntigravityRelogin(accountId) {
1708
+ const existing = getAccount(accountId);
1709
+ if (!existing)
1710
+ throw new AccountError(`Unknown account: ${accountId}`, 404);
1711
+ if (existing.provider !== "antigravity" || existing.kind !== "token") {
1712
+ throw new AccountError(`${accountId} cannot be re-authenticated this way.`);
1713
+ }
1714
+ const started = startAgySession({ accountId });
1715
+ log("info", "account.login.start", { account: accountId, provider: "antigravity", kind: "relogin" });
1716
+ return started;
1717
+ }
1718
+ function antigravitySessionStatus(sessionId) {
1719
+ const rec = agySessions.get(sessionId);
1720
+ if (!rec)
1721
+ return { status: "error", error: "Login session expired." };
1722
+ return { status: rec.status, authUrl: rec.authUrl, error: rec.error, account: rec.account };
1723
+ }
1724
+ async function submitAntigravityCallback(sessionId, raw) {
1725
+ const parsed = parseAntigravityAuthInput(raw);
1726
+ if (!parsed) {
1727
+ throw new AccountError("Paste the code from the Antigravity page, or the antigravity.google/oauth-callback URL.");
1728
+ }
1729
+ const rec = agySessions.get(sessionId);
1730
+ if (!rec)
1731
+ throw new AccountError("Login session expired.", 404);
1732
+ if (rec.status === "done" && rec.account)
1733
+ return rec.account;
1734
+ const { code, state } = parsed;
1735
+ if (state && state !== rec.state)
1736
+ throw new AccountError("Sign-in state did not match. Start again.");
1737
+ try {
1738
+ const token = await exchangeAntigravityCode(code, rec.verifier);
1739
+ const email = await fetchAgyEmail(token.accessToken);
1740
+ const named = rec.label?.trim() || "";
1741
+ if (rec.accountId) {
1742
+ await secretStore().set(rec.accountId, serializeAgySecret(token));
1743
+ updateAccount(rec.accountId, { email });
1744
+ rec.account = getAccount(rec.accountId);
1745
+ } else {
1746
+ const id = newAccountId("antigravity", named || email || "account");
1747
+ await secretStore().set(id, serializeAgySecret(token));
1748
+ rec.account = {
1749
+ id,
1750
+ provider: "antigravity",
1751
+ label: named,
1752
+ kind: "token",
1753
+ email,
1754
+ createdAt: new Date().toISOString()
1755
+ };
1756
+ saveAccount(rec.account);
1757
+ }
1758
+ rec.status = "done";
1759
+ log("info", rec.accountId ? "account.login.done" : "account.add", {
1760
+ account: rec.account.id,
1761
+ provider: "antigravity",
1762
+ kind: rec.account.kind
1763
+ });
1764
+ return rec.account;
1765
+ } catch (e) {
1766
+ rec.status = "error";
1767
+ rec.error = e instanceof Error ? e.message : String(e);
1768
+ logError("account.login.error", e, { provider: "antigravity", account: rec.accountId });
1769
+ throw new AccountError(rec.error);
1770
+ }
1771
+ }
1179
1772
 
1180
1773
  // src/collect.ts
1181
- import { hostname } from "node:os";
1774
+ import { existsSync as existsSync9 } from "node:fs";
1775
+ import { homedir as homedir9, hostname } from "node:os";
1776
+ import { join as join10 } from "node:path";
1182
1777
 
1183
1778
  // src/adapters/cursor.ts
1184
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1185
- import { homedir as homedir4 } from "node:os";
1186
- import { join as join5 } from "node:path";
1779
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
1780
+ import { homedir as homedir7 } from "node:os";
1781
+ import { join as join8 } from "node:path";
1187
1782
  var USAGE_URL3 = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage";
1188
1783
  var GROK_BOT_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetSandUsageStatus";
1189
1784
  var PLAN_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetPlanInfo";
1190
- var KEYCHAIN_ACCOUNT = "cursor-user";
1191
- var KEYCHAIN_SERVICE = "cursor-access-token";
1785
+ var KEYCHAIN_ACCOUNT2 = "cursor-user";
1786
+ var KEYCHAIN_SERVICE2 = "cursor-access-token";
1192
1787
  function tokenFromAuthJson(parsed) {
1193
1788
  if (!isObject(parsed))
1194
1789
  return null;
1195
1790
  return typeof parsed.accessToken === "string" && parsed.accessToken ? parsed.accessToken : null;
1196
1791
  }
1197
1792
  function tokenFromAuthFile(file) {
1198
- if (!existsSync5(file))
1793
+ if (!existsSync7(file))
1199
1794
  return null;
1200
1795
  try {
1201
- return tokenFromAuthJson(JSON.parse(readFileSync5(file, "utf8")));
1796
+ return tokenFromAuthJson(JSON.parse(readFileSync6(file, "utf8")));
1202
1797
  } catch {
1203
1798
  return null;
1204
1799
  }
1205
1800
  }
1206
1801
  function authFiles() {
1207
- const home = homedir4();
1802
+ const home = homedir7();
1208
1803
  const out = [];
1209
1804
  if (process.platform === "win32") {
1210
- const roaming = process.env.APPDATA || join5(home, "AppData", "Roaming");
1211
- out.push(join5(roaming, "Cursor", "auth.json"));
1805
+ const roaming = process.env.APPDATA || join8(home, "AppData", "Roaming");
1806
+ out.push(join8(roaming, "Cursor", "auth.json"));
1212
1807
  } else if (process.platform !== "darwin") {
1213
- const xdg = process.env.XDG_CONFIG_HOME || join5(home, ".config");
1214
- out.push(join5(xdg, "cursor", "auth.json"));
1808
+ const xdg = process.env.XDG_CONFIG_HOME || join8(home, ".config");
1809
+ out.push(join8(xdg, "cursor", "auth.json"));
1215
1810
  }
1216
- out.push(join5(home, ".cursor", "auth.json"));
1811
+ out.push(join8(home, ".cursor", "auth.json"));
1217
1812
  return out;
1218
1813
  }
1219
- async function tokenFromKeychain() {
1814
+ async function tokenFromKeychain2() {
1220
1815
  if (process.platform !== "darwin")
1221
1816
  return null;
1222
- const res = await run("security", ["find-generic-password", "-a", KEYCHAIN_ACCOUNT, "-s", KEYCHAIN_SERVICE, "-w"], { timeoutMs: 20000 });
1817
+ const res = await run("security", ["find-generic-password", "-a", KEYCHAIN_ACCOUNT2, "-s", KEYCHAIN_SERVICE2, "-w"], { timeoutMs: 20000 });
1223
1818
  if (res.code !== 0)
1224
1819
  return null;
1225
1820
  const v = res.stdout.replace(/\r?\n$/, "");
@@ -1234,7 +1829,7 @@ async function readCursorAccessToken() {
1234
1829
  if (fromFile)
1235
1830
  return fromFile;
1236
1831
  }
1237
- const fromKeychain = await tokenFromKeychain();
1832
+ const fromKeychain = await tokenFromKeychain2();
1238
1833
  if (fromKeychain)
1239
1834
  return fromKeychain;
1240
1835
  if (process.env.CURSOR_AUTH_FILE)
@@ -1392,6 +1987,236 @@ async function fetchCursor(account) {
1392
1987
  }
1393
1988
  }
1394
1989
 
1990
+ // src/adapters/grok.ts
1991
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
1992
+ import { homedir as homedir8 } from "node:os";
1993
+ import { join as join9 } from "node:path";
1994
+ var DEFAULT_PROXY = "https://cli-chat-proxy.grok.com/v1";
1995
+ var TOKEN_URL = "https://auth.x.ai/oauth2/token";
1996
+ var FALLBACK_CLI_VERSION3 = "1.0.13";
1997
+ var TIER_NAMES = {
1998
+ supergrok: "SuperGrok",
1999
+ supergrok_lite: "SuperGrok Lite",
2000
+ supergrok_plus: "SuperGrok Plus",
2001
+ supergrok_heavy: "SuperGrok Heavy",
2002
+ x_premium: "X Premium",
2003
+ x_premium_plus: "X Premium+"
2004
+ };
2005
+ function authFile() {
2006
+ if (process.env.GROK_AUTH_FILE)
2007
+ return process.env.GROK_AUTH_FILE;
2008
+ return join9(homedir8(), ".grok", "auth.json");
2009
+ }
2010
+ function proxyBase() {
2011
+ const raw = process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim();
2012
+ if (!raw)
2013
+ return DEFAULT_PROXY;
2014
+ return raw.replace(/\/+$/, "");
2015
+ }
2016
+ function num(v) {
2017
+ if (typeof v === "number" && Number.isFinite(v))
2018
+ return v;
2019
+ if (isObject(v) && typeof v.val === "number" && Number.isFinite(v.val))
2020
+ return v.val;
2021
+ if (typeof v === "string" && v.trim()) {
2022
+ const n = Number(v);
2023
+ return Number.isFinite(n) ? n : null;
2024
+ }
2025
+ return null;
2026
+ }
2027
+ function money2(n) {
2028
+ return `$${n.toFixed(n % 1 ? 2 : 0)}`;
2029
+ }
2030
+ function sessionFromEntry(entry) {
2031
+ const access = typeof entry.key === "string" && entry.key ? entry.key : null;
2032
+ if (!access)
2033
+ return null;
2034
+ const expiry = typeof entry.expires_at === "string" ? Date.parse(entry.expires_at) : NaN;
2035
+ return {
2036
+ accessToken: access,
2037
+ refreshToken: typeof entry.refresh_token === "string" && entry.refresh_token ? entry.refresh_token : null,
2038
+ clientId: typeof entry.oidc_client_id === "string" && entry.oidc_client_id ? entry.oidc_client_id : null,
2039
+ email: typeof entry.email === "string" && entry.email.trim() ? entry.email.trim() : null,
2040
+ expiresAt: Number.isFinite(expiry) ? expiry : null,
2041
+ authMode: typeof entry.auth_mode === "string" ? entry.auth_mode : null
2042
+ };
2043
+ }
2044
+ function sessionFromAuthJson(parsed) {
2045
+ if (!isObject(parsed))
2046
+ return null;
2047
+ if (typeof parsed.key === "string" && parsed.key)
2048
+ return sessionFromEntry(parsed);
2049
+ const entries = Object.values(parsed).filter(isObject);
2050
+ const oidc = entries.find((e) => e.auth_mode === "oidc" && typeof e.key === "string" && e.key);
2051
+ if (oidc)
2052
+ return sessionFromEntry(oidc);
2053
+ const any = entries.find((e) => typeof e.key === "string" && e.key);
2054
+ return any ? sessionFromEntry(any) : null;
2055
+ }
2056
+ function readGrokSession() {
2057
+ const file = authFile();
2058
+ if (!existsSync8(file))
2059
+ return null;
2060
+ try {
2061
+ return sessionFromAuthJson(JSON.parse(readFileSync7(file, "utf8")));
2062
+ } catch {
2063
+ return null;
2064
+ }
2065
+ }
2066
+ function grokUsesApiKey(session) {
2067
+ if (session?.authMode === "oidc" && session.accessToken)
2068
+ return false;
2069
+ return Boolean(process.env.XAI_API_KEY?.trim()) || session?.authMode === "api_key";
2070
+ }
2071
+ function planFromGrokSettings(body) {
2072
+ if (!isObject(body))
2073
+ return null;
2074
+ if (typeof body.subscription_tier_display === "string" && body.subscription_tier_display.trim()) {
2075
+ return body.subscription_tier_display.trim();
2076
+ }
2077
+ const raw = typeof body.subscription_tier === "string" ? body.subscription_tier.trim() : "";
2078
+ if (!raw)
2079
+ return null;
2080
+ return TIER_NAMES[raw] ?? raw.replace(/_/g, " ");
2081
+ }
2082
+ function creditsRoot(body) {
2083
+ if (!isObject(body))
2084
+ return null;
2085
+ return isObject(body.config) ? body.config : body;
2086
+ }
2087
+ function normalizeGrokCredits(body) {
2088
+ const cfg = creditsRoot(body);
2089
+ if (!cfg)
2090
+ return [];
2091
+ const out = [];
2092
+ const period = isObject(cfg.currentPeriod) ? cfg.currentPeriod : null;
2093
+ const periodType = typeof period?.type === "string" ? period.type : "";
2094
+ const weekly = periodType.includes("WEEKLY");
2095
+ const resetsAt = isoOrNull(period?.end) ?? isoOrNull(cfg.billingPeriodEnd);
2096
+ const used = clampPercent(num(cfg.creditUsagePercent));
2097
+ if (used !== null) {
2098
+ out.push({
2099
+ id: "weekly",
2100
+ label: weekly || !periodType ? "Weekly Usage" : "Usage",
2101
+ usedPercent: used,
2102
+ resetsAt,
2103
+ windowMinutes: weekly || !periodType ? 10080 : null,
2104
+ kind: weekly || !periodType ? "rolling" : "cycle"
2105
+ });
2106
+ }
2107
+ const cap = num(cfg.onDemandCap);
2108
+ const spent = num(cfg.onDemandUsed);
2109
+ if (cap && cap > 0 && spent !== null) {
2110
+ out.push({
2111
+ id: "on_demand",
2112
+ label: "On-demand",
2113
+ usedPercent: clampPercent(spent / cap * 100),
2114
+ resetsAt,
2115
+ windowMinutes: weekly ? 10080 : null,
2116
+ kind: weekly ? "rolling" : "cycle",
2117
+ note: `${money2(spent)} of ${money2(cap)}`
2118
+ });
2119
+ }
2120
+ return out;
2121
+ }
2122
+ async function clientVersion() {
2123
+ return await binVersion("grok") ?? FALLBACK_CLI_VERSION3;
2124
+ }
2125
+ function headers3(token, version, email) {
2126
+ const h = {
2127
+ Authorization: `Bearer ${token}`,
2128
+ Accept: "application/json",
2129
+ "User-Agent": "xai-grok-cli",
2130
+ "x-grok-client-version": version,
2131
+ "x-grok-client-mode": "cli"
2132
+ };
2133
+ if (email)
2134
+ h["x-email"] = email;
2135
+ return h;
2136
+ }
2137
+ async function refreshAccessToken(session) {
2138
+ if (!session.refreshToken || !session.clientId)
2139
+ return null;
2140
+ const res = await fetchJson(TOKEN_URL, {
2141
+ method: "POST",
2142
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
2143
+ body: new URLSearchParams({
2144
+ grant_type: "refresh_token",
2145
+ refresh_token: session.refreshToken,
2146
+ client_id: session.clientId
2147
+ }).toString()
2148
+ });
2149
+ if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string" || !res.body.access_token) {
2150
+ return null;
2151
+ }
2152
+ const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
2153
+ return {
2154
+ ...session,
2155
+ accessToken: res.body.access_token,
2156
+ refreshToken: typeof res.body.refresh_token === "string" && res.body.refresh_token ? res.body.refresh_token : session.refreshToken,
2157
+ expiresAt: Date.now() + expiresIn * 1000
2158
+ };
2159
+ }
2160
+ async function liveSession(session) {
2161
+ if (session.expiresAt && session.expiresAt > Date.now() + 60000)
2162
+ return session;
2163
+ return await refreshAccessToken(session) ?? (session.expiresAt && session.expiresAt > Date.now() ? session : null);
2164
+ }
2165
+ async function fetchGrok(account) {
2166
+ try {
2167
+ const stored = readGrokSession();
2168
+ if (!stored) {
2169
+ if (grokUsesApiKey(null)) {
2170
+ return snapshot(account, "unsupported", { message: "This CLI is using an xAI API key, which has no SuperGrok quota." });
2171
+ }
2172
+ return snapshot(account, "signed_out", { message: "Not signed in. Run `grok login --oauth`." });
2173
+ }
2174
+ if (stored.authMode && stored.authMode !== "oidc") {
2175
+ return snapshot(account, "unsupported", { email: stored.email, message: "This CLI is using an xAI API key, which has no SuperGrok quota." });
2176
+ }
2177
+ let session = await liveSession(stored);
2178
+ if (!session) {
2179
+ return snapshot(account, "error", { email: stored.email, message: "Grok session expired. Run `grok login --oauth`." });
2180
+ }
2181
+ const version = await clientVersion();
2182
+ const base = proxyBase();
2183
+ const get = (token, path) => fetchJson(`${base}${path}`, { headers: headers3(token, version, session.email) });
2184
+ let [credits, settings] = await Promise.all([
2185
+ get(session.accessToken, "/billing?format=credits"),
2186
+ get(session.accessToken, "/settings").catch(() => ({ status: 0, body: null, text: "" }))
2187
+ ]);
2188
+ if (credits.status === 401 || credits.status === 403) {
2189
+ const refreshed = await refreshAccessToken(session);
2190
+ if (refreshed) {
2191
+ session = refreshed;
2192
+ [credits, settings] = await Promise.all([
2193
+ get(session.accessToken, "/billing?format=credits"),
2194
+ get(session.accessToken, "/settings").catch(() => ({ status: 0, body: null, text: "" }))
2195
+ ]);
2196
+ }
2197
+ }
2198
+ const email = session.email ?? account.email ?? null;
2199
+ const plan = settings.status === 200 ? planFromGrokSettings(settings.body) : null;
2200
+ if (credits.status === 401 || credits.status === 403) {
2201
+ return snapshot(account, "error", { email, plan, message: `Grok rejected the session (${credits.status}). Run \`grok login --oauth\`.` });
2202
+ }
2203
+ if (credits.status !== 200) {
2204
+ return snapshot(account, "error", { email, plan, message: `Usage endpoint returned HTTP ${credits.status}.` });
2205
+ }
2206
+ const windows = normalizeGrokCredits(credits.body);
2207
+ if (windows.length === 0) {
2208
+ return snapshot(account, "unsupported", {
2209
+ email,
2210
+ plan,
2211
+ message: "Signed in, but this account has no SuperGrok / Grok Build allowance."
2212
+ });
2213
+ }
2214
+ return snapshot(account, "ok", { email, plan, windows });
2215
+ } catch (e) {
2216
+ return snapshot(account, "error", { message: errorMessage(e) });
2217
+ }
2218
+ }
2219
+
1395
2220
  // src/adapters/index.ts
1396
2221
  function fetchSnapshot(account) {
1397
2222
  switch (account.provider) {
@@ -1401,6 +2226,10 @@ function fetchSnapshot(account) {
1401
2226
  return fetchCodex(account);
1402
2227
  case "cursor":
1403
2228
  return fetchCursor(account);
2229
+ case "antigravity":
2230
+ return fetchAntigravity(account);
2231
+ case "grok":
2232
+ return fetchGrok(account);
1404
2233
  case "opencode":
1405
2234
  return fetchOpenCode(account);
1406
2235
  }
@@ -1411,6 +2240,8 @@ var PROVIDERS = [
1411
2240
  { id: "claude", name: "Claude", bin: "claude" },
1412
2241
  { id: "codex", name: "Codex", bin: "codex" },
1413
2242
  { id: "cursor", name: "Cursor", bin: "cursor-agent" },
2243
+ { id: "antigravity", name: "Antigravity", bin: "agy" },
2244
+ { id: "grok", name: "Grok", bin: "grok" },
1414
2245
  { id: "opencode", name: "OpenCode Go", bin: "opencode" }
1415
2246
  ];
1416
2247
  function providerName(id) {
@@ -1418,10 +2249,53 @@ function providerName(id) {
1418
2249
  }
1419
2250
 
1420
2251
  // src/collect.ts
2252
+ function providerHomeMarkers(id) {
2253
+ const home = homedir9();
2254
+ switch (id) {
2255
+ case "antigravity":
2256
+ return [
2257
+ join10(home, ".gemini", "antigravity-cli", "antigravity-oauth-token"),
2258
+ join10(home, ".gemini", "antigravity-cli", "settings.json"),
2259
+ join10(home, ".gemini", "antigravity-cli", "installation_id")
2260
+ ];
2261
+ case "claude":
2262
+ return [join10(home, ".claude", ".credentials.json"), join10(home, ".claude", "settings.json")];
2263
+ case "codex":
2264
+ return [join10(home, ".codex", "auth.json"), join10(home, ".codex", "config.toml")];
2265
+ case "cursor":
2266
+ return [join10(home, ".cursor", "auth.json")];
2267
+ case "grok":
2268
+ return [join10(home, ".grok", "auth.json")];
2269
+ case "opencode": {
2270
+ const xdg = process.env.XDG_DATA_HOME;
2271
+ const base = xdg && xdg.trim() ? xdg : join10(home, ".local", "share");
2272
+ return [join10(base, "opencode", "auth.json")];
2273
+ }
2274
+ }
2275
+ }
2276
+ function providerHomeExists(id) {
2277
+ return providerHomeMarkers(id).some((file) => existsSync9(file));
2278
+ }
2279
+ function isProviderPresent(opts) {
2280
+ if (opts.binPath)
2281
+ return true;
2282
+ if (opts.provider === "opencode")
2283
+ return true;
2284
+ return opts.homeExists;
2285
+ }
1421
2286
  async function detectProviders() {
2287
+ clearBinCache();
1422
2288
  return Promise.all(PROVIDERS.map(async (p) => {
1423
- const path = await which(p.bin);
1424
- return { id: p.id, installed: path !== null, version: null };
2289
+ const path = await which(p.bin, { fresh: true });
2290
+ const homeExists = providerHomeExists(p.id);
2291
+ const installed = isProviderPresent({ binPath: path, homeExists, provider: p.id });
2292
+ log("info", "detect", {
2293
+ provider: p.id,
2294
+ installed,
2295
+ path: path ?? undefined,
2296
+ home: homeExists || undefined
2297
+ });
2298
+ return { id: p.id, installed, version: null };
1425
2299
  }));
1426
2300
  }
1427
2301
  function resolveAccounts(provider, installed) {
@@ -1436,9 +2310,24 @@ function resolveAccounts(provider, installed) {
1436
2310
  }
1437
2311
  async function fetchAccount(account) {
1438
2312
  try {
1439
- return await withTimeout(fetchSnapshot(account), FETCH_TIMEOUT_MS, `${account.provider} fetch`);
2313
+ const snap = await withTimeout(fetchSnapshot(account), FETCH_TIMEOUT_MS, `${account.provider} fetch`);
2314
+ log(snap.status === "error" ? "error" : "info", "quotas.fetch", {
2315
+ account: snap.account.id,
2316
+ provider: snap.account.provider,
2317
+ status: snap.status,
2318
+ windows: snap.windows.length,
2319
+ message: snap.message ?? undefined
2320
+ });
2321
+ return snap;
1440
2322
  } catch (e) {
1441
- return snapshot(account, "error", { message: e instanceof Error ? e.message : String(e) });
2323
+ const snap = snapshot(account, "error", { message: e instanceof Error ? e.message : String(e) });
2324
+ log("error", "quotas.fetch", {
2325
+ account: account.id,
2326
+ provider: account.provider,
2327
+ status: snap.status,
2328
+ message: snap.message ?? undefined
2329
+ });
2330
+ return snap;
1442
2331
  }
1443
2332
  }
1444
2333
  async function collectReport(update, only) {
@@ -1448,10 +2337,19 @@ async function collectReport(update, only) {
1448
2337
  const accounts = resolveAccounts(p.id, pres.installed);
1449
2338
  const [snapshots, version] = await Promise.all([
1450
2339
  Promise.all(accounts.map(fetchAccount)),
1451
- pres.installed ? binVersion(p.bin) : Promise.resolve(null)
2340
+ pres.installed ? binVersion(await which(p.bin) ?? p.bin) : Promise.resolve(null)
1452
2341
  ]);
1453
2342
  return { id: p.id, name: p.name, installed: pres.installed, version, accounts: snapshots };
1454
2343
  }));
2344
+ const accounts = providers.flatMap((p) => p.accounts);
2345
+ log("info", "quotas.collect", {
2346
+ providers: providers.length,
2347
+ accounts: accounts.length,
2348
+ ok: accounts.filter((a) => a.status === "ok").length,
2349
+ error: accounts.filter((a) => a.status === "error").length,
2350
+ signed_out: accounts.filter((a) => a.status === "signed_out").length,
2351
+ unsupported: accounts.filter((a) => a.status === "unsupported").length
2352
+ });
1455
2353
  return {
1456
2354
  version: VERSION,
1457
2355
  hostname: formatHostname(hostname()),
@@ -1495,10 +2393,235 @@ class ReportCache {
1495
2393
  }
1496
2394
  }
1497
2395
 
2396
+ // src/instance.ts
2397
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2398
+ function looksLikeJustUsageCommand(command) {
2399
+ const s = command.replace(/\\/g, "/").toLowerCase();
2400
+ return s.includes("just-usage") || /\/just-usage\/(?:src\/|dist\/)?cli\.(ts|js)\b/.test(s);
2401
+ }
2402
+ function parseRunRecord(raw) {
2403
+ try {
2404
+ const parsed = JSON.parse(raw);
2405
+ if (!isObject(parsed))
2406
+ return null;
2407
+ const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
2408
+ const port = typeof parsed.port === "number" ? parsed.port : Number(parsed.port);
2409
+ if (!Number.isInteger(pid) || pid <= 0)
2410
+ return null;
2411
+ if (!Number.isInteger(port) || port <= 0 || port > 65535)
2412
+ return null;
2413
+ return {
2414
+ pid,
2415
+ port,
2416
+ host: typeof parsed.host === "string" && parsed.host ? parsed.host : "0.0.0.0",
2417
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : ""
2418
+ };
2419
+ } catch {
2420
+ return null;
2421
+ }
2422
+ }
2423
+ function writeRunRecord(opts) {
2424
+ ensureDir(paths.runDir());
2425
+ writeFileSync3(paths.runRecord(opts.port), JSON.stringify({
2426
+ pid: process.pid,
2427
+ port: opts.port,
2428
+ host: opts.host,
2429
+ startedAt: new Date().toISOString()
2430
+ }), { encoding: "utf8", mode: 384 });
2431
+ }
2432
+ function readRunRecord(port) {
2433
+ const file = paths.runRecord(port);
2434
+ if (!existsSync10(file))
2435
+ return null;
2436
+ return parseRunRecord(readFileSync8(file, "utf8"));
2437
+ }
2438
+ function removeRunRecord(port) {
2439
+ const file = paths.runRecord(port);
2440
+ try {
2441
+ unlinkSync(file);
2442
+ } catch {}
2443
+ }
2444
+ function listRunPorts() {
2445
+ const dir = paths.runDir();
2446
+ if (!existsSync10(dir))
2447
+ return [];
2448
+ const ports = [];
2449
+ for (const name of readdirSync(dir)) {
2450
+ const m = name.match(/^(\d+)\.json$/);
2451
+ if (!m)
2452
+ continue;
2453
+ const port = Number(m[1]);
2454
+ if (Number.isInteger(port) && port > 0 && port <= 65535)
2455
+ ports.push(port);
2456
+ }
2457
+ return ports.sort((a, b) => a - b);
2458
+ }
2459
+ function processAlive(pid) {
2460
+ try {
2461
+ process.kill(pid, 0);
2462
+ return true;
2463
+ } catch {
2464
+ return false;
2465
+ }
2466
+ }
2467
+ async function processCommand(pid) {
2468
+ if (process.platform === "win32") {
2469
+ const res = await run("wmic", ["process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value"], { timeoutMs: 5000 });
2470
+ const line = res.stdout.split(/\r?\n/).map((s) => s.trim()).find((s) => s.toLowerCase().startsWith("commandline="));
2471
+ return line ? line.slice(line.indexOf("=") + 1).trim() : "";
2472
+ }
2473
+ const res = await run("ps", ["-p", String(pid), "-www", "-o", "command="], { timeoutMs: 5000 });
2474
+ return res.stdout.split(/\r?\n/).map((s) => s.trim()).find((s) => s && !/^command$/i.test(s)) ?? "";
2475
+ }
2476
+ async function pidsOnPort(port) {
2477
+ if (process.platform === "win32") {
2478
+ const res = await run("netstat", ["-ano"], { timeoutMs: 8000 });
2479
+ const re = new RegExp(`[:\\[]${port}\\]?(?:\\s+\\S+){1,2}\\s+LISTENING\\s+(\\d+)`, "gi");
2480
+ const pids = new Set;
2481
+ for (const m of res.stdout.matchAll(re)) {
2482
+ const pid = Number(m[1]);
2483
+ if (Number.isInteger(pid) && pid > 0)
2484
+ pids.add(pid);
2485
+ }
2486
+ return [...pids];
2487
+ }
2488
+ const res = await run("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeoutMs: 8000 });
2489
+ const pids = new Set;
2490
+ for (const line of res.stdout.split(/\r?\n/)) {
2491
+ const pid = Number(line.trim());
2492
+ if (Number.isInteger(pid) && pid > 0)
2493
+ pids.add(pid);
2494
+ }
2495
+ return [...pids];
2496
+ }
2497
+ function parseHealth(body) {
2498
+ if (!isObject(body) || body.ok !== true)
2499
+ return null;
2500
+ const pid = typeof body.pid === "number" && Number.isInteger(body.pid) && body.pid > 0 ? body.pid : null;
2501
+ return {
2502
+ ok: true,
2503
+ version: typeof body.version === "string" ? body.version : null,
2504
+ name: typeof body.name === "string" ? body.name : null,
2505
+ pid
2506
+ };
2507
+ }
2508
+ function isJustUsageHealth(info) {
2509
+ if (!info?.ok)
2510
+ return false;
2511
+ if (info.name && info.name !== PACKAGE_NAME)
2512
+ return false;
2513
+ return info.name === PACKAGE_NAME || Boolean(info.version);
2514
+ }
2515
+ async function fetchHealth(port) {
2516
+ try {
2517
+ const res = await fetchJson(`http://127.0.0.1:${port}/api/health`, { timeoutMs: 2000 });
2518
+ if (res.status !== 200)
2519
+ return null;
2520
+ return parseHealth(res.body);
2521
+ } catch {
2522
+ return null;
2523
+ }
2524
+ }
2525
+ async function oursOnPort(port) {
2526
+ const record = readRunRecord(port);
2527
+ const listeners = await pidsOnPort(port);
2528
+ const health = await fetchHealth(port);
2529
+ const ours = new Set;
2530
+ const known = new Set(listeners);
2531
+ if (record && processAlive(record.pid))
2532
+ known.add(record.pid);
2533
+ if (health?.pid && processAlive(health.pid))
2534
+ known.add(health.pid);
2535
+ for (const pid of known) {
2536
+ if (pid === process.pid)
2537
+ continue;
2538
+ const command = await processCommand(pid);
2539
+ const listening = listeners.includes(pid);
2540
+ if (looksLikeJustUsageCommand(command) || isJustUsageHealth(health) && (listening || pid === record?.pid || pid === health?.pid)) {
2541
+ ours.add(pid);
2542
+ }
2543
+ }
2544
+ return { pids: [...ours], health, listening: listeners.length > 0 };
2545
+ }
2546
+ async function terminate(pids) {
2547
+ for (const pid of pids) {
2548
+ try {
2549
+ process.kill(pid, "SIGTERM");
2550
+ } catch {}
2551
+ }
2552
+ const deadline = Date.now() + 2500;
2553
+ while (Date.now() < deadline && pids.some(processAlive)) {
2554
+ await new Promise((r) => setTimeout(r, 80));
2555
+ }
2556
+ for (const pid of pids) {
2557
+ if (!processAlive(pid))
2558
+ continue;
2559
+ try {
2560
+ process.kill(pid, "SIGKILL");
2561
+ } catch {}
2562
+ }
2563
+ }
2564
+ async function stopPort(port) {
2565
+ const found = await oursOnPort(port);
2566
+ if (found.pids.length === 0) {
2567
+ if (found.listening && !isJustUsageHealth(found.health)) {
2568
+ log("warn", "stop.busy", { port });
2569
+ return { status: "busy", port };
2570
+ }
2571
+ removeRunRecord(port);
2572
+ log("info", "stop.idle", { port });
2573
+ return { status: "idle", port };
2574
+ }
2575
+ await terminate(found.pids);
2576
+ removeRunRecord(port);
2577
+ log("info", "stop.ok", { port, pids: found.pids });
2578
+ return { status: "stopped", port, pids: found.pids };
2579
+ }
2580
+ async function stopServers(opts) {
2581
+ const ports = opts.all ? [...new Set([DEFAULT_PORT, ...listRunPorts(), ...opts.port ? [opts.port] : []])] : [opts.port ?? DEFAULT_PORT];
2582
+ const out = [];
2583
+ for (const port of ports)
2584
+ out.push(await stopPort(port));
2585
+ return out;
2586
+ }
2587
+ function formatStopResults(results) {
2588
+ const stopped = results.filter((r) => r.status === "stopped");
2589
+ const busy = results.filter((r) => r.status === "busy");
2590
+ const lines = [];
2591
+ for (const r of stopped) {
2592
+ lines.push(`Stopped ${PACKAGE_NAME} on port ${r.port} (pid ${r.pids.join(", ")}).`);
2593
+ }
2594
+ for (const r of busy) {
2595
+ lines.push(`Port ${r.port} is in use, but it is not a ${PACKAGE_NAME} server.`);
2596
+ }
2597
+ if (stopped.length === 0 && busy.length === 0) {
2598
+ if (results.length === 1)
2599
+ lines.push(`No ${PACKAGE_NAME} server is running on port ${results[0].port}.`);
2600
+ else
2601
+ lines.push(`No ${PACKAGE_NAME} server is running.`);
2602
+ }
2603
+ const code = busy.length ? 1 : stopped.length ? 0 : 1;
2604
+ return { text: lines.join(`
2605
+ `), code };
2606
+ }
2607
+
1498
2608
  // src/server.ts
1499
2609
  import { createServer } from "node:http";
1500
2610
  import { hostname as hostname2, networkInterfaces } from "node:os";
1501
2611
 
2612
+ // src/ui/favicon.svg
2613
+ var favicon_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2614
+ <rect width="32" height="32" rx="2.5" fill="#000"/>
2615
+ <defs>
2616
+ <mask id="card">
2617
+ <rect x="6" y="6" width="20" height="20" rx="4.5" fill="#fff"/>
2618
+ <rect x="20.5" y="20.5" width="5.5" height="5.5" fill="#000"/>
2619
+ </mask>
2620
+ </defs>
2621
+ <rect width="32" height="32" fill="#fff" mask="url(#card)"/>
2622
+ </svg>
2623
+ `;
2624
+
1502
2625
  // src/ui/index.html
1503
2626
  var ui_default = `<!doctype html>
1504
2627
  <html lang="en">
@@ -1507,7 +2630,7 @@ var ui_default = `<!doctype html>
1507
2630
  <meta name="viewport" content="width=device-width,initial-scale=1">
1508
2631
  <meta name="color-scheme" content="dark">
1509
2632
  <title>just-usage</title>
1510
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Crect x='7' y='14' width='18' height='4' rx='2' fill='%232a2a2a'/%3E%3Crect x='7' y='14' width='11' height='4' rx='2' fill='%23fff'/%3E%3C/svg%3E">
2633
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
1511
2634
  <style>
1512
2635
  :root {
1513
2636
  --bg: #000;
@@ -1613,24 +2736,25 @@ var ui_default = `<!doctype html>
1613
2736
  .credits-avail:hover, .credits-avail:focus-visible { color: var(--fg); }
1614
2737
  .credits-tip {
1615
2738
  display: none; position: absolute; right: 0; bottom: calc(100% + 8px);
1616
- min-width: 240px; padding: 8px 10px;
2739
+ min-width: 0; padding: 8px 10px;
1617
2740
  background: var(--surface); border: 1px solid var(--line-2); border-radius: 8px;
1618
2741
  color: var(--muted); font-size: 11px; line-height: 1.45; white-space: nowrap; z-index: 5;
1619
2742
  }
1620
2743
  .credits-avail:hover .credits-tip, .credits-avail:focus-visible .credits-tip { display: block; }
1621
- .credits-tip-row { display: flex; align-items: baseline; gap: 12px; }
2744
+ .credits-tip-row { display: flex; align-items: baseline; gap: 10px; width: max-content; }
1622
2745
  .credits-tip-row + .credits-tip-row { margin-top: 6px; }
1623
2746
  .credits-tip-n { color: var(--fg); font-variant-numeric: tabular-nums; min-width: 1em; }
1624
- .credits-tip-date { color: var(--fg); flex: 1; }
1625
- .credits-tip-left { color: var(--dim); font-variant-numeric: tabular-nums; margin-left: auto; }
2747
+ .credits-tip-date { color: var(--fg); }
2748
+ .credits-tip-left { color: var(--dim); font-variant-numeric: tabular-nums; }
1626
2749
 
1627
2750
  #dash[hidden], #settings[hidden] { display: none; }
1628
2751
  .settings { display: flex; flex-direction: column; gap: 22px; }
1629
2752
  .opts-label { font-size: 11px; color: var(--dim); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 8px; }
1630
- .opts-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 0; user-select: none; }
1631
- .opts-row.dragging {
1632
- background: var(--surface); border: 1px solid var(--line-2); border-radius: 8px; padding: 6px 8px;
1633
- }
2753
+ #opts-providers { position: relative; }
2754
+ .opts-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 0; user-select: none; transition: opacity .22s ease; }
2755
+ .opts-row.dragging { opacity: .62; cursor: grabbing; }
2756
+ .opts-row.dragging .grip { cursor: grabbing; }
2757
+ .opts-email { margin-top: 10px; padding-top: 16px; border-top: 1px solid var(--line); }
1634
2758
  .opts-prov { display: flex; align-items: center; gap: 8px; min-width: 0; }
1635
2759
  .opts-row img.logo { width: 14px; height: 14px; }
1636
2760
  .grip {
@@ -1662,11 +2786,11 @@ var ui_default = `<!doctype html>
1662
2786
  .seg input { appearance: none; position: absolute; }
1663
2787
  .seg label:has(input:checked) { background: #121212; color: var(--fg); }
1664
2788
  .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
2789
  .opts-link { background: none; border: none; color: var(--muted); font: inherit; font-size: 12px; cursor: pointer; padding: 0; }
1667
2790
  .opts-link:hover { color: var(--fg); }
1668
2791
  .opts-link[hidden] { display: none; }
1669
2792
  .acct-group { padding-bottom: 12px; }
2793
+ .acct-group:last-child { padding-bottom: 0; }
1670
2794
  .acct-group + .acct-group { border-top: 1px solid var(--line); padding-top: 12px; }
1671
2795
  .acct-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 13px; }
1672
2796
  .acct-head img.logo { width: 14px; height: 14px; }
@@ -1683,6 +2807,7 @@ var ui_default = `<!doctype html>
1683
2807
  border: 1px solid var(--line); border-radius: 12px;
1684
2808
  padding: 0;
1685
2809
  }
2810
+ dialog.dlg form { margin: 0; }
1686
2811
  dialog.dlg::backdrop { background: rgba(0, 0, 0, .9); }
1687
2812
  .dlg-head {
1688
2813
  display: flex; justify-content: space-between; align-items: center;
@@ -1751,19 +2876,18 @@ var ui_default = `<!doctype html>
1751
2876
  <label><input type="radio" name="meter" value="used"> Used</label>
1752
2877
  </div>
1753
2878
  </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
2879
  <div>
1759
2880
  <div class="opts-inline tight">
1760
2881
  <div class="opts-label">Accounts</div>
1761
2882
  <button class="refresh" id="acct-add" type="button">Add account</button>
1762
2883
  </div>
1763
2884
  <div id="opts-accounts"></div>
2885
+ <label class="opts-row opts-email" for="opts-email">
2886
+ <span>Show email on accounts</span>
2887
+ <input class="chk" id="opts-email" type="checkbox">
2888
+ </label>
1764
2889
  </div>
1765
2890
  <div class="opts-actions">
1766
- <button class="opts-link" id="opts-signed-in" type="button">Signed-in only</button>
1767
2891
  <button class="refresh" id="refresh" type="button">Refresh data</button>
1768
2892
  </div>
1769
2893
  </div>
@@ -1774,11 +2898,13 @@ var ui_default = `<!doctype html>
1774
2898
  </div>
1775
2899
 
1776
2900
  <dialog class="dlg" id="add-acct">
1777
- <div class="dlg-head">
1778
- <strong id="add-acct-title">Add account</strong>
1779
- <button class="opts-link" id="add-acct-close" type="button">Close</button>
1780
- </div>
1781
- <div class="dlg-body" id="add-acct-body"></div>
2901
+ <form autocomplete="off" onsubmit="return false">
2902
+ <div class="dlg-head">
2903
+ <strong id="add-acct-title">Add account</strong>
2904
+ <button class="opts-link" id="add-acct-close" type="button">Close</button>
2905
+ </div>
2906
+ <div class="dlg-body" id="add-acct-body"></div>
2907
+ </form>
1782
2908
  </dialog>
1783
2909
 
1784
2910
  <script>
@@ -1787,13 +2913,17 @@ var ui_default = `<!doctype html>
1787
2913
  ["claude", "Claude"],
1788
2914
  ["codex", "Codex"],
1789
2915
  ["cursor", "Cursor"],
2916
+ ["antigravity", "Antigravity"],
2917
+ ["grok", "Grok"],
1790
2918
  ["opencode", "OpenCode Go"],
1791
2919
  ];
1792
2920
  const SIGNIN_HINT = {
1793
2921
  claude: "Run <code>claude</code>, then <code>/login</code>.",
1794
2922
  codex: "Run <code>codex login</code>.",
1795
2923
  cursor: "Run <code>cursor-agent login</code>.",
2924
+ grok: "Run <code>grok login --oauth</code>.",
1796
2925
  opencode: "Run <code>opencode auth login</code> or <code>just-usage add opencode</code>.",
2926
+ antigravity: "Run <code>agy</code> and complete Google sign-in.",
1797
2927
  };
1798
2928
  const $ = (id) => document.getElementById(id);
1799
2929
  let report = null;
@@ -1802,8 +2932,8 @@ var ui_default = `<!doctype html>
1802
2932
  if (cached && Array.isArray(cached.providers)) report = cached;
1803
2933
  } catch {}
1804
2934
  const params = new URLSearchParams(location.search);
1805
- const wanted = params.get("tab");
1806
- let active = PROVIDERS.some(([id]) => id === wanted) ? wanted : localStorage.getItem("ju.tab") || "claude";
2935
+ let active = localStorage.getItem("ju.tab") || "claude";
2936
+ if (!PROVIDERS.some(([id]) => id === active)) active = "claude";
1807
2937
  let settingsOpen = params.get("view") === "settings";
1808
2938
  let loading = false;
1809
2939
  let busy = false;
@@ -1823,22 +2953,37 @@ var ui_default = `<!doctype html>
1823
2953
  } catch {}
1824
2954
  let dragging = false;
1825
2955
  let showLeft = localStorage.getItem("ju.showLeft") !== "0";
1826
- let order = PROVIDERS.map(([id]) => id);
2956
+ const defaultOrder = PROVIDERS.map(([id]) => id);
2957
+ let order = defaultOrder.slice();
1827
2958
  try {
1828
2959
  const raw = JSON.parse(localStorage.getItem("ju.order"));
1829
2960
  if (Array.isArray(raw)) {
1830
- const known = new Set(order);
2961
+ const known = new Set(defaultOrder);
1831
2962
  const next = raw.filter((id) => known.has(id));
1832
- for (const id of order) if (!next.includes(id)) next.push(id);
2963
+ for (const id of defaultOrder) {
2964
+ if (next.includes(id)) continue;
2965
+ const after = defaultOrder.slice(defaultOrder.indexOf(id) + 1).find((x) => next.includes(x));
2966
+ next.splice(after ? next.indexOf(after) : next.length, 0, id);
2967
+ }
2968
+ if (!localStorage.getItem("ju.orderGen") && next[next.length - 1] === "grok") {
2969
+ const i = next.indexOf("opencode");
2970
+ if (i !== -1) {
2971
+ next.pop();
2972
+ next.splice(i, 0, "grok");
2973
+ }
2974
+ }
1833
2975
  order = next;
1834
2976
  }
1835
2977
  } catch {}
2978
+ localStorage.setItem("ju.orderGen", "2");
2979
+ localStorage.setItem("ju.order", JSON.stringify(order));
1836
2980
 
1837
2981
  const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
1838
2982
  const rich = (s) => esc(s).replace(/\`([^\`]+)\`/g, "<code>$1</code>");
1839
2983
  const sev = (p) => (p == null ? null : p >= 85 ? "crit" : p >= 60 ? "warn" : "ok");
1840
2984
  const rank = { ok: 1, warn: 2, crit: 3 };
1841
- const logo = (id) => \`<img class="logo" src="/logos/\${id}.svg?v=2" alt="" width="14" height="14">\`;
2985
+ const logo = (id) => \`<img class="logo" src="/logos/\${id}.svg?v=5" alt="" width="14" height="14">\`;
2986
+ const noSuggest = \`autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"\`;
1842
2987
  const formatPlan = (p) => String(p).replace(/\\S+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1));
1843
2988
  const GENERIC_LABELS = new Set(["", "default", "token", "go", "pending", "profile", "account", "codex"]);
1844
2989
  function isGenericLabel(label, email) {
@@ -1893,14 +3038,24 @@ var ui_default = `<!doctype html>
1893
3038
  return d === 1 ? "1 day ago" : \`\${d} days ago\`;
1894
3039
  }
1895
3040
 
1896
- function isSignedIn(p) {
1897
- return !!p?.accounts.some((a) => a.status !== "signed_out");
3041
+ function isDefaultVisible(p) {
3042
+ return !!p?.accounts.some((a) => a.status === "ok" || a.status === "error");
3043
+ }
3044
+
3045
+ /** true/false = user chose; missing = auto-show when the CLI is signed in. */
3046
+ function explicitEnabled(id) {
3047
+ if (!enabled || typeof enabled !== "object" || !Object.hasOwn(enabled, id)) return null;
3048
+ if (enabled[id] === true) return true;
3049
+ if (enabled[id] === false) return false;
3050
+ return null;
1898
3051
  }
1899
3052
 
1900
3053
  function visibleIds() {
1901
- if (enabled) return order.filter((id) => enabled[id]);
1902
- if (!report) return [];
1903
- return order.filter((id) => isSignedIn(report.providers.find((x) => x.id === id)));
3054
+ return order.filter((id) => {
3055
+ const choice = explicitEnabled(id);
3056
+ if (choice !== null) return choice;
3057
+ return isDefaultVisible(report?.providers.find((x) => x.id === id));
3058
+ });
1904
3059
  }
1905
3060
 
1906
3061
  function ensureActive() {
@@ -1915,10 +3070,8 @@ var ui_default = `<!doctype html>
1915
3070
  if (next) localStorage.setItem("ju.providers", JSON.stringify(next));
1916
3071
  else localStorage.removeItem("ju.providers");
1917
3072
  ensureActive();
1918
- if (settingsOpen) {
1919
- renderTabs();
1920
- $("opts-signed-in").hidden = !enabled;
1921
- } else render();
3073
+ if (settingsOpen) renderTabs();
3074
+ else render();
1922
3075
  }
1923
3076
 
1924
3077
  function persistOrder(next) {
@@ -1929,16 +3082,17 @@ var ui_default = `<!doctype html>
1929
3082
  else render();
1930
3083
  }
1931
3084
 
3085
+ function syncUrl() {
3086
+ const want = settingsOpen ? \`\${location.pathname}?view=settings\` : location.pathname;
3087
+ if (location.pathname + location.search !== want) history.replaceState(null, "", want);
3088
+ }
3089
+
1932
3090
  function setView(open) {
1933
3091
  settingsOpen = open;
1934
3092
  $("dash").hidden = open;
1935
3093
  $("settings").hidden = !open;
1936
- const btn = $("opts-btn");
1937
3094
  $("opts-label").textContent = open ? "Back" : "Options";
1938
- const q = new URLSearchParams();
1939
- if (open) q.set("view", "settings");
1940
- else q.set("tab", active);
1941
- history.replaceState(null, "", q.toString() ? \`?\${q}\` : location.pathname);
3095
+ syncUrl();
1942
3096
  if (open) {
1943
3097
  renderSettings();
1944
3098
  renderChrome();
@@ -1989,9 +3143,16 @@ var ui_default = `<!doctype html>
1989
3143
  return html;
1990
3144
  }
1991
3145
 
3146
+ function planBadge(provider, plan) {
3147
+ if (!plan) return "";
3148
+ const name = PROVIDERS.find(([id]) => id === provider)?.[1] || provider;
3149
+ if (plan.toLowerCase() === name.toLowerCase() || plan.toLowerCase() === provider) return "";
3150
+ return \`<span class="plan">\${esc(formatPlan(plan))}</span>\`;
3151
+ }
3152
+
1992
3153
  function renderCard(a, provider, index) {
1993
3154
  const title = accountTitle(a, index);
1994
- const plan = a.account.plan ? \`<span class="plan">\${esc(formatPlan(a.account.plan))}</span>\` : "";
3155
+ const plan = planBadge(provider, a.account.plan);
1995
3156
  let badge = "";
1996
3157
  if (a.status === "ok") {
1997
3158
  let worst = null;
@@ -2071,7 +3232,7 @@ var ui_default = `<!doctype html>
2071
3232
  }).join("");
2072
3233
  for (const input of $("opts-providers").querySelectorAll("input")) {
2073
3234
  input.onchange = () => {
2074
- const next = Object.fromEntries(order.map((id) => [id, vis.has(id)]));
3235
+ const next = { ...(enabled || {}) };
2075
3236
  next[input.dataset.id] = input.checked;
2076
3237
  persistEnabled(next);
2077
3238
  };
@@ -2084,22 +3245,48 @@ var ui_default = `<!doctype html>
2084
3245
  const row = grip.closest(".opts-row");
2085
3246
  dragging = true;
2086
3247
  row.classList.add("dragging");
3248
+ document.body.style.cursor = "grabbing";
3249
+ try { grip.setPointerCapture(e.pointerId); } catch {}
3250
+ const ease = "transform .36s cubic-bezier(.4, 0, .2, 1)";
2087
3251
  const place = (clientY) => {
2088
- const others = [...list.querySelectorAll(".opts-row")].filter((r) => r !== row);
2089
- const before = others.find((r) => {
2090
- const b = r.getBoundingClientRect();
2091
- return clientY < b.top + b.height / 2;
2092
- });
3252
+ const rows = [...list.querySelectorAll(".opts-row")];
3253
+ const others = rows.filter((r) => r !== row);
3254
+ const y = clientY - list.getBoundingClientRect().top + list.scrollTop;
3255
+ const before = others.find((r) => y < r.offsetTop + r.offsetHeight / 2 - 6);
3256
+ if ((before && row.nextElementSibling === before) || (!before && list.lastElementChild === row)) return;
3257
+ const first = new Map(rows.map((r) => {
3258
+ r.style.transition = "none";
3259
+ return [r, r.getBoundingClientRect()];
3260
+ }));
2093
3261
  if (before) list.insertBefore(row, before);
2094
3262
  else list.appendChild(row);
3263
+ for (const r of rows) {
3264
+ r.style.transform = "";
3265
+ const from = first.get(r);
3266
+ const dy = from.top - r.getBoundingClientRect().top;
3267
+ r.style.transform = dy ? \`translateY(\${dy}px)\` : "";
3268
+ }
3269
+ list.getBoundingClientRect();
3270
+ for (const r of rows) {
3271
+ if (!r.style.transform) continue;
3272
+ r.style.transition = ease;
3273
+ r.style.transform = "";
3274
+ }
2095
3275
  };
2096
3276
  const move = (ev) => place(ev.clientY);
2097
3277
  const stop = () => {
2098
3278
  document.removeEventListener("pointermove", move);
2099
3279
  document.removeEventListener("pointerup", stop);
2100
3280
  document.removeEventListener("pointercancel", stop);
3281
+ document.body.style.cursor = "";
2101
3282
  dragging = false;
2102
3283
  row.classList.remove("dragging");
3284
+ window.setTimeout(() => {
3285
+ for (const r of list.querySelectorAll(".opts-row")) {
3286
+ r.style.transition = "";
3287
+ r.style.transform = "";
3288
+ }
3289
+ }, 380);
2103
3290
  const next = [...list.querySelectorAll(".opts-row")].map((r) => r.dataset.id);
2104
3291
  if (next.join() !== order.join()) persistOrder(next);
2105
3292
  };
@@ -2108,7 +3295,6 @@ var ui_default = `<!doctype html>
2108
3295
  document.addEventListener("pointercancel", stop);
2109
3296
  };
2110
3297
  }
2111
- $("opts-signed-in").hidden = !enabled;
2112
3298
  for (const input of $("opts-meter").querySelectorAll("input")) {
2113
3299
  input.checked = (input.value === "left") === showLeft;
2114
3300
  }
@@ -2139,7 +3325,7 @@ var ui_default = `<!doctype html>
2139
3325
  const extra = a.account.kind === "default" ? "from CLI" : a.account.kind;
2140
3326
  if (renaming === a.account.id) {
2141
3327
  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)}">
3328
+ <span class="acct-rename"><input class="field acct-rename-input" data-id="\${esc(a.account.id)}" value="\${esc(title)}" \${noSuggest}>
2143
3329
  <button class="refresh acct-rename-save" type="button" data-id="\${esc(a.account.id)}">Save</button></span>
2144
3330
  <button class="opts-link acct-rename-cancel" type="button">Cancel</button>
2145
3331
  </div>\`;
@@ -2225,25 +3411,30 @@ var ui_default = `<!doctype html>
2225
3411
  body = \`<div class="pick">
2226
3412
  <button class="pick-btn" type="button" data-id="claude">\${logo("claude")} Claude</button>
2227
3413
  <button class="pick-btn" type="button" data-id="codex">\${logo("codex")} Codex</button>
3414
+ <button class="pick-btn" type="button" data-id="antigravity">\${logo("antigravity")} Antigravity</button>
2228
3415
  <button class="pick-btn" type="button" data-id="opencode">\${logo("opencode")} OpenCode Go</button>
2229
3416
  </div>
2230
- <div class="acct-note">One Cursor account for now — whatever <code>cursor-agent</code> is signed in as.</div>\`;
3417
+ <div class="acct-note">One Cursor account for now — whatever <code>cursor-agent</code> is signed in as.</div>
3418
+ <div class="acct-note">One Grok account for now — whatever <code>grok</code> is signed in as.</div>\`;
2231
3419
  } else if (wizard.step === "label") {
2232
3420
  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)}">
3421
+ <input class="field" id="wiz-label" placeholder="Label (optional)" value="\${esc(wizard.label)}" \${noSuggest}>
2234
3422
  <div class="acct-actions"><button class="opts-link" id="wiz-back" type="button">Back</button>
2235
3423
  <button class="refresh" id="wiz-next" type="button">Continue</button></div>\`;
2236
- } else if (wizard.provider === "codex") {
2237
- body = \`<div class="acct-note">Sign in with ChatGPT. If you are not on this machine, paste the localhost callback after the redirect.</div>
3424
+ } else if (wizard.provider === "codex" || wizard.provider === "antigravity") {
3425
+ const oauth = wizard.provider === "codex"
3426
+ ? { 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" }
3427
+ : { 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" };
3428
+ body = \`<div class="acct-note">\${oauth.note}</div>
2238
3429
  \${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="http://localhost:… callback" value="\${esc(wizard.callback)}">
2240
- <button class="refresh" id="wiz-callback-go" type="button">Submit callback</button>\`
2241
- : \`<button class="refresh" id="wiz-codex" type="button">Sign in with ChatGPT</button>\`}
3430
+ <input class="field" id="wiz-callback" placeholder="\${esc(oauth.ph)}" value="\${esc(wizard.callback)}" \${noSuggest}>
3431
+ <button class="refresh" id="wiz-callback-go" type="button">\${oauth.submit || "Submit callback"}</button>\`
3432
+ : \`<button class="refresh" id="\${oauth.start}" type="button">\${oauth.action}</button>\`}
2242
3433
  <button class="opts-link" id="wiz-back" type="button">Back</button>\`;
2243
3434
  } else {
2244
3435
  const ph = wizard.provider === "claude" ? "Setup token from <code>claude setup-token</code>" : "OpenCode Go API key";
2245
3436
  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)}" autocomplete="off">
3437
+ <input class="field" id="wiz-secret" type="password" placeholder="\${wizard.provider === "claude" ? "Setup token" : "API key"}" value="\${esc(wizard.secret)}" \${noSuggest}>
2247
3438
  <div class="acct-actions"><button class="opts-link" id="wiz-back" type="button">Back</button>
2248
3439
  <button class="refresh" id="wiz-save" type="button">Add</button></div>\`;
2249
3440
  }
@@ -2271,8 +3462,10 @@ var ui_default = `<!doctype html>
2271
3462
  if (save) save.onclick = addToken;
2272
3463
  const start = $("wiz-codex");
2273
3464
  if (start) start.onclick = startCodex;
3465
+ const startAgy = $("wiz-agy");
3466
+ if (startAgy) startAgy.onclick = startAntigravity;
2274
3467
  const submit = $("wiz-callback-go");
2275
- if (submit) submit.onclick = submitCodexCallback;
3468
+ if (submit) submit.onclick = wizard.provider === "antigravity" ? submitAntigravityCallback : submitCodexCallback;
2276
3469
  }
2277
3470
 
2278
3471
  async function addToken() {
@@ -2329,6 +3522,56 @@ var ui_default = `<!doctype html>
2329
3522
  }
2330
3523
  }
2331
3524
 
3525
+ async function startAntigravity() {
3526
+ if (busy) return;
3527
+ const popup = window.open("about:blank", "_blank");
3528
+ busy = true;
3529
+ wizardMsg("Starting sign-in…", false);
3530
+ renderWizard();
3531
+ try {
3532
+ const body = await api("/api/accounts/antigravity/start", {
3533
+ method: "POST",
3534
+ headers: { "Content-Type": "application/json" },
3535
+ body: JSON.stringify({ label: wizard.label }),
3536
+ });
3537
+ wizard.sessionId = body.sessionId;
3538
+ wizard.authUrl = body.authUrl;
3539
+ if (body.authUrl) {
3540
+ if (popup) popup.location.replace(body.authUrl);
3541
+ else window.location.assign(body.authUrl);
3542
+ } else {
3543
+ popup?.close();
3544
+ }
3545
+ wizardMsg("Copy the code from the Antigravity page and paste it here.", false);
3546
+ } catch (e) {
3547
+ popup?.close();
3548
+ wizardMsg(e.message, true);
3549
+ } finally {
3550
+ busy = false;
3551
+ renderWizard();
3552
+ }
3553
+ }
3554
+
3555
+ async function submitAntigravityCallback() {
3556
+ if (!wizard.sessionId) return;
3557
+ if (busy) return;
3558
+ busy = true;
3559
+ try {
3560
+ await api("/api/accounts/antigravity/callback", {
3561
+ method: "POST",
3562
+ headers: { "Content-Type": "application/json" },
3563
+ body: JSON.stringify({ sessionId: wizard.sessionId, url: wizard.callback }),
3564
+ });
3565
+ closeWizard();
3566
+ await load(true);
3567
+ } catch (e) {
3568
+ wizardMsg(e.message, true);
3569
+ renderWizard();
3570
+ } finally {
3571
+ busy = false;
3572
+ }
3573
+ }
3574
+
2332
3575
  async function submitCodexCallback() {
2333
3576
  if (!wizard.sessionId) return;
2334
3577
  try {
@@ -2422,7 +3665,10 @@ var ui_default = `<!doctype html>
2422
3665
  for (const a of p.accounts || []) {
2423
3666
  const old = prev?.accounts.find((x) => x.account.id === a.account.id);
2424
3667
  if (!a.account.email && old?.account.email) a.account.email = old.account.email;
2425
- if (!a.account.plan && old?.account.plan) a.account.plan = old.account.plan;
3668
+ if (!a.account.plan && old?.account.plan) {
3669
+ const name = PROVIDERS.find(([id]) => id === p.id)?.[1] || p.id;
3670
+ if (old.account.plan.toLowerCase() !== name.toLowerCase()) a.account.plan = old.account.plan;
3671
+ }
2426
3672
  }
2427
3673
  }
2428
3674
  }
@@ -2465,7 +3711,6 @@ var ui_default = `<!doctype html>
2465
3711
 
2466
3712
  $("opts-btn").onclick = () => setView(!settingsOpen);
2467
3713
  $("refresh").onclick = () => load(true, true);
2468
- $("opts-signed-in").onclick = () => persistEnabled(null);
2469
3714
  $("opts-email").onchange = () => {
2470
3715
  showEmail = $("opts-email").checked;
2471
3716
  localStorage.setItem("ju.showEmail", showEmail ? "1" : "0");
@@ -2508,6 +3753,7 @@ var ui_default = `<!doctype html>
2508
3753
  document.addEventListener("visibilitychange", () => {
2509
3754
  if (document.visibilityState === "visible") maybeAutoRefresh();
2510
3755
  });
3756
+ syncUrl();
2511
3757
  if (report) render();
2512
3758
  load(false);
2513
3759
  })();
@@ -2516,6 +3762,13 @@ var ui_default = `<!doctype html>
2516
3762
  </html>
2517
3763
  `;
2518
3764
 
3765
+ // src/ui/logos/antigravity.svg
3766
+ var antigravity_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" role="img">
3767
+ <title>Antigravity</title>
3768
+ <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"/>
3769
+ </svg>
3770
+ `;
3771
+
2519
3772
  // src/ui/logos/claude.svg
2520
3773
  var claude_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" role="img">
2521
3774
  <title>Claude</title>
@@ -2537,6 +3790,14 @@ var cursor_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24
2537
3790
  </svg>
2538
3791
  `;
2539
3792
 
3793
+ // src/ui/logos/grok.svg
3794
+ var grok_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#ececec" role="img">
3795
+ <title>Grok</title>
3796
+ <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"/>
3797
+ <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"/>
3798
+ </svg>
3799
+ `;
3800
+
2540
3801
  // src/ui/logos/opencode.svg
2541
3802
  var opencode_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" fill-rule="evenodd" role="img">
2542
3803
  <title>OpenCode</title>
@@ -2549,7 +3810,9 @@ var LOGOS = {
2549
3810
  claude: claude_default,
2550
3811
  codex: codex_default,
2551
3812
  cursor: cursor_default,
2552
- opencode: opencode_default
3813
+ opencode: opencode_default,
3814
+ antigravity: antigravity_default,
3815
+ grok: grok_default
2553
3816
  };
2554
3817
  function json(res, status, body) {
2555
3818
  const text = JSON.stringify(body);
@@ -2598,11 +3861,30 @@ async function startServer(opts) {
2598
3861
  cache.invalidate();
2599
3862
  return out;
2600
3863
  };
3864
+ const quietPath = (path) => path === "/favicon.svg" || path === "/favicon.ico" || path.startsWith("/logos/");
2601
3865
  const handler = async (req, res) => {
2602
3866
  const url = new URL(req.url ?? "/", "http://localhost");
3867
+ const started = Date.now();
2603
3868
  res.setHeader("X-Content-Type-Options", "nosniff");
3869
+ if (!quietPath(url.pathname)) {
3870
+ res.on("finish", () => {
3871
+ const status = res.statusCode;
3872
+ log(status >= 500 ? "error" : status >= 400 ? "warn" : "info", "http", {
3873
+ method: req.method ?? "GET",
3874
+ path: url.pathname,
3875
+ refresh: url.searchParams.get("refresh") === "1" || undefined,
3876
+ status,
3877
+ ms: Date.now() - started
3878
+ });
3879
+ });
3880
+ }
2604
3881
  try {
2605
3882
  const method = req.method ?? "GET";
3883
+ if ((url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico") && (method === "GET" || method === "HEAD")) {
3884
+ res.writeHead(200, { "Content-Type": "image/svg+xml; charset=utf-8", "Cache-Control": "public, max-age=86400" });
3885
+ res.end(favicon_default);
3886
+ return;
3887
+ }
2606
3888
  if (url.pathname === "/" && (method === "GET" || method === "HEAD")) {
2607
3889
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
2608
3890
  res.end(ui_default);
@@ -2625,7 +3907,7 @@ async function startServer(opts) {
2625
3907
  return;
2626
3908
  }
2627
3909
  if (url.pathname === "/api/health" && (method === "GET" || method === "HEAD")) {
2628
- json(res, 200, { ok: true, version: VERSION });
3910
+ json(res, 200, { ok: true, name: PACKAGE_NAME, version: VERSION, pid: process.pid });
2629
3911
  return;
2630
3912
  }
2631
3913
  if (url.pathname === "/api/accounts" && method === "POST") {
@@ -2641,7 +3923,23 @@ async function startServer(opts) {
2641
3923
  json(res, 200, await mutated(() => addOpenCodeKey(secret, label)));
2642
3924
  return;
2643
3925
  }
2644
- json(res, 400, { error: "Add Claude with a setup-token, OpenCode with an API key, or start a Codex sign-in." });
3926
+ json(res, 400, { error: "Add Claude with a setup-token, OpenCode with an API key, or start a Codex / Antigravity sign-in." });
3927
+ return;
3928
+ }
3929
+ if (url.pathname === "/api/accounts/antigravity/start" && method === "POST") {
3930
+ const body = await readJson(req);
3931
+ const accountId = str(body.accountId);
3932
+ json(res, 200, accountId ? await beginAntigravityRelogin(accountId) : await beginAntigravityAdd(str(body.label) || undefined));
3933
+ return;
3934
+ }
3935
+ if (url.pathname === "/api/accounts/antigravity/callback" && method === "POST") {
3936
+ const body = await readJson(req);
3937
+ json(res, 200, { ok: true, account: await mutated(() => submitAntigravityCallback(str(body.sessionId), str(body.url))) });
3938
+ return;
3939
+ }
3940
+ const agySession = url.pathname.match(/^\/api\/accounts\/antigravity\/session\/([^/]+)$/);
3941
+ if (agySession && (method === "GET" || method === "HEAD")) {
3942
+ json(res, 200, antigravitySessionStatus(decodeURIComponent(agySession[1])));
2645
3943
  return;
2646
3944
  }
2647
3945
  if (url.pathname === "/api/accounts/codex/start" && method === "POST") {
@@ -2681,6 +3979,7 @@ async function startServer(opts) {
2681
3979
  json(res, 404, { error: "not found" });
2682
3980
  } catch (e) {
2683
3981
  if (e instanceof AccountError) {
3982
+ log("warn", "http.error", { path: url.pathname, status: e.status, message: e.message });
2684
3983
  json(res, e.status, { error: e.message });
2685
3984
  return;
2686
3985
  }
@@ -2688,6 +3987,7 @@ async function startServer(opts) {
2688
3987
  json(res, 400, { error: "Invalid JSON." });
2689
3988
  return;
2690
3989
  }
3990
+ logError("http.error", e, { path: url.pathname, status: 500 });
2691
3991
  json(res, 500, { error: e instanceof Error ? e.message : String(e) });
2692
3992
  }
2693
3993
  };
@@ -2695,7 +3995,17 @@ async function startServer(opts) {
2695
3995
  return new Promise((resolve, reject) => {
2696
3996
  server.once("error", reject);
2697
3997
  server.listen(opts.port, opts.host, () => {
2698
- resolve({ close: () => server.close(), urls: reachableUrls(opts.host, opts.port, tailscaleIp) });
3998
+ writeRunRecord({ port: opts.port, host: opts.host });
3999
+ const urls = reachableUrls(opts.host, opts.port, tailscaleIp);
4000
+ log("info", "serve.start", { host: opts.host, port: opts.port, urls: urls.map((u) => u.url) });
4001
+ resolve({
4002
+ close: () => {
4003
+ log("info", "serve.stop", { port: opts.port });
4004
+ removeRunRecord(opts.port);
4005
+ server.close();
4006
+ },
4007
+ urls
4008
+ });
2699
4009
  });
2700
4010
  });
2701
4011
  }
@@ -2703,7 +4013,6 @@ function isTailscale(ip) {
2703
4013
  const [a, b] = ip.split(".").map(Number);
2704
4014
  return a === 100 && b !== undefined && b >= 64 && b <= 127;
2705
4015
  }
2706
- var TAILSCALE_NOTE = "Tailscale only. Not same-Wi-Fi. Other devices need Tailscale too.";
2707
4016
  async function detectTailscaleIp() {
2708
4017
  const res = await run("tailscale", ["status", "--json"], { timeoutMs: 4000 });
2709
4018
  if (res.code !== 0)
@@ -2744,7 +4053,7 @@ function reachableUrls(host, port, tailscaleIp = null) {
2744
4053
  }
2745
4054
  }
2746
4055
  if (tailscaleIp) {
2747
- urls.push({ kind: "tailscale", url: `http://${tailscaleIp}:${port}`, note: TAILSCALE_NOTE });
4056
+ urls.push({ kind: "tailscale", url: `http://${tailscaleIp}:${port}` });
2748
4057
  }
2749
4058
  return urls;
2750
4059
  }
@@ -2809,15 +4118,15 @@ function renderReport(report, now = Date.now()) {
2809
4118
  }
2810
4119
 
2811
4120
  // src/update.ts
2812
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
4121
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
2813
4122
  import { dirname as dirname3 } from "node:path";
2814
4123
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
2815
4124
  function readCache() {
2816
4125
  const file = paths.updateCache();
2817
- if (!existsSync6(file))
4126
+ if (!existsSync11(file))
2818
4127
  return null;
2819
4128
  try {
2820
- const parsed = JSON.parse(readFileSync6(file, "utf8"));
4129
+ const parsed = JSON.parse(readFileSync9(file, "utf8"));
2821
4130
  if (typeof parsed.checkedAt === "string" && typeof parsed.latest === "string")
2822
4131
  return parsed;
2823
4132
  } catch {}
@@ -2826,7 +4135,7 @@ function readCache() {
2826
4135
  function writeCache(c) {
2827
4136
  try {
2828
4137
  ensureDir(dirname3(paths.updateCache()));
2829
- writeFileSync3(paths.updateCache(), JSON.stringify(c) + `
4138
+ writeFileSync4(paths.updateCache(), JSON.stringify(c) + `
2830
4139
  `);
2831
4140
  } catch {}
2832
4141
  }
@@ -2871,11 +4180,15 @@ async function checkForUpdate(force = false) {
2871
4180
  latest = null;
2872
4181
  }
2873
4182
  }
2874
- if (!latest)
4183
+ if (!latest) {
4184
+ log("warn", "update.check", { ok: false });
2875
4185
  return cached ? toInfo(cached) : null;
4186
+ }
2876
4187
  const entry = { checkedAt: new Date().toISOString(), latest };
2877
4188
  writeCache(entry);
2878
- return toInfo(entry);
4189
+ const info = toInfo(entry);
4190
+ log("info", "update.check", { latest: info.latest, available: info.available });
4191
+ return info;
2879
4192
  }
2880
4193
  function toInfo(c) {
2881
4194
  return { current: VERSION, latest: c.latest, available: semverGt(c.latest, VERSION), checkedAt: c.checkedAt };
@@ -2906,7 +4219,10 @@ function upgradeCommand(pm, version = "latest") {
2906
4219
  async function runUpgrade(pm, version = "latest") {
2907
4220
  const [cmd, args] = upgradeCommand(pm, version);
2908
4221
  console.log(`$ ${cmd} ${args.join(" ")}`);
2909
- return runInteractive(cmd, args);
4222
+ log("info", "update.upgrade", { pm, version });
4223
+ const code = await runInteractive(cmd, args);
4224
+ log(code === 0 ? "info" : "error", "update.upgrade.done", { pm, version, code: code ?? undefined });
4225
+ return code;
2910
4226
  }
2911
4227
 
2912
4228
  // src/cli.ts
@@ -2919,20 +4235,74 @@ Usage
2919
4235
  --port <n> Port (default ${DEFAULT_PORT})
2920
4236
  --host <addr> Bind address (default ${DEFAULT_HOST}; use 127.0.0.1 for local only)
2921
4237
  --no-open Don't open a browser
4238
+ just-usage stop [options] Stop a running just-usage server
4239
+ --port <n> Port (default ${DEFAULT_PORT})
4240
+ --all Stop every just-usage server this CLI started
2922
4241
  just-usage status [--json] Print quotas in the terminal
2923
4242
  just-usage accounts List accounts
2924
- just-usage add <provider> Add an account (codex | claude | opencode)
4243
+ just-usage add <provider> Add an account (codex | claude | opencode | antigravity)
2925
4244
  --label <name> Friendly name
2926
4245
  --token Claude only: paste a \`claude setup-token\` instead of a profile login
2927
4246
  just-usage login <account-id> Re-authenticate a profile account
2928
4247
  just-usage remove <account-id> Remove an account and anything we stored for it
2929
4248
  just-usage upgrade [--check] Update to the latest release
2930
4249
  just-usage --version
4250
+ just-usage stop --help More on stopping a running server
2931
4251
 
2932
4252
  Providers: ${PROVIDERS.map((p) => p.name).join(", ")}
2933
4253
  Cursor uses whatever \`cursor-agent\` is logged in as (single account).
4254
+ Grok uses whatever \`grok login --oauth\` stored (single account).
4255
+ Antigravity extras are extra Google logins; they do not replace \`agy\`'s signed-in account.
4256
+
4257
+ Logs are appended to ~/.just-usage/logs (one JSON line per action).
4258
+
4259
+ Stop
4260
+ \`just-usage stop\` asks the server to exit — the same as Ctrl+C in the terminal
4261
+ that started it. Use this when that terminal is gone or another instance is
4262
+ still holding the port.
4263
+
4264
+ just-usage stop Stop the default server (port ${DEFAULT_PORT})
4265
+ just-usage stop --port 5758 Stop a server you started on another port
4266
+ just-usage stop --all Stop every just-usage server recorded here
4267
+
4268
+ It only stops a process it can confirm is just-usage (pid file, health route,
4269
+ or command line). An unrelated process on the same port is left alone.
4270
+ `;
4271
+ var STOP_HELP = `${PACKAGE_NAME} stop
4272
+ Stop a running just-usage server.
4273
+
4274
+ Usage
4275
+ just-usage stop
4276
+ just-usage stop --port <n>
4277
+ just-usage stop --all
4278
+
4279
+ What it does
4280
+ The server writes a small pid file under ~/.config/just-usage/run/ when it
4281
+ starts. \`stop\` reads that file and asks the process to exit (SIGTERM, then
4282
+ SIGKILL if it ignores the first signal).
4283
+
4284
+ If the pid file is missing or stale — for example after a crash — \`stop\`
4285
+ looks for a listener on the port and checks GET /api/health. Only a confirmed
4286
+ just-usage server is stopped.
4287
+
4288
+ Options
4289
+ --port <n> Port the server is bound to (default ${DEFAULT_PORT})
4290
+ --all Stop every just-usage server this CLI has a pid file for,
4291
+ plus the default port if that is still running
4292
+ --help, -h Show this help
4293
+
4294
+ Examples
4295
+ just-usage stop
4296
+ just-usage serve --port 5758
4297
+ just-usage stop --port 5758
4298
+ just-usage stop --all
4299
+
4300
+ See also
4301
+ just-usage serve Start the server
4302
+ just-usage --help All commands
2934
4303
  `;
2935
4304
  function fail(msg, code = 1) {
4305
+ log("error", "cli.fail", { message: msg, code });
2936
4306
  console.error(msg);
2937
4307
  process.exit(code);
2938
4308
  }
@@ -2994,17 +4364,22 @@ Update available: v${u.current} → v${u.latest}. Run: just-usage upgrade
2994
4364
  server = await startServer({ host, port, getUpdate: () => update });
2995
4365
  } catch (e) {
2996
4366
  const code = e.code;
2997
- if (code === "EADDRINUSE")
2998
- fail(`Port ${port} is already in use. Try: just-usage serve --port ${port + 1}`);
4367
+ if (code === "EADDRINUSE") {
4368
+ const stop = port === DEFAULT_PORT ? "just-usage stop" : `just-usage stop --port ${port}`;
4369
+ fail(`Port ${port} is already in use. Stop it with: ${stop}
4370
+ Or start another: just-usage serve --port ${port + 1}`);
4371
+ }
4372
+ logError("serve.error", e, { host, port });
2999
4373
  throw e;
3000
4374
  }
3001
4375
  console.log(`${PACKAGE_NAME} v${VERSION}`);
3002
4376
  for (const u of server.urls) {
3003
4377
  const tag = u.kind === "local" ? "local " : u.kind === "tailscale" ? "tailscale" : "network ";
3004
- console.log(` ${tag} ${u.url}${u.note ? ` — ${u.note}` : ""}`);
4378
+ console.log(` ${tag} ${u.url}`);
3005
4379
  }
4380
+ const stopHint = port === DEFAULT_PORT ? "just-usage stop" : `just-usage stop --port ${port}`;
3006
4381
  console.log(`
3007
- Press Ctrl+C to stop.`);
4382
+ Press Ctrl+C to stop. From another terminal: ${stopHint}`);
3008
4383
  if (shouldOpen)
3009
4384
  openInBrowser(server.urls[0].url);
3010
4385
  const shutdown = () => {
@@ -3016,6 +4391,7 @@ Press Ctrl+C to stop.`);
3016
4391
  }
3017
4392
  async function cmdStatus(argv) {
3018
4393
  const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" } }, allowPositionals: true, strict: false });
4394
+ log("info", "cli.status", { json: values.json === true });
3019
4395
  const [report, update] = await Promise.all([collectReport(null), checkForUpdate().catch(() => null)]);
3020
4396
  report.update = update;
3021
4397
  if (values.json) {
@@ -3026,10 +4402,11 @@ async function cmdStatus(argv) {
3026
4402
  }
3027
4403
  function cmdAccounts() {
3028
4404
  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).");
4405
+ log("info", "cli.accounts", { extra: rows.length });
4406
+ 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
4407
  if (rows.length === 0) {
3031
4408
  console.log(`
3032
- No extra accounts. Add one with: just-usage add codex | claude | opencode`);
4409
+ No extra accounts. Add one with: just-usage add codex | claude | opencode | antigravity`);
3033
4410
  return;
3034
4411
  }
3035
4412
  console.log("");
@@ -3077,9 +4454,11 @@ async function addClaudeProfile(label) {
3077
4454
  if (!status?.loggedIn) {
3078
4455
  const { rmSync } = await import("node:fs");
3079
4456
  rmSync(dir, { recursive: true, force: true });
4457
+ log("error", "account.login.error", { provider: "claude", message: "login did not complete" });
3080
4458
  fail("Login did not complete; nothing was saved.");
3081
4459
  }
3082
4460
  saveAccount({ id, provider: "claude", label: label ?? id.split(":")[1], kind: "profile", path: dir, email: null, createdAt: new Date().toISOString() });
4461
+ log("info", "account.add", { account: id, provider: "claude", kind: "profile" });
3083
4462
  console.log(`
3084
4463
  Added ${id}. Note: on macOS the first read may trigger a Keychain prompt — choose "Always Allow".`);
3085
4464
  }
@@ -3103,7 +4482,7 @@ async function cmdAdd(argv) {
3103
4482
  });
3104
4483
  const provider = positionals[0];
3105
4484
  if (!isProvider(provider))
3106
- fail(`Usage: just-usage add <codex|claude|opencode> [--label name] [--token]`);
4485
+ fail(`Usage: just-usage add <codex|claude|opencode|antigravity> [--label name] [--token]`);
3107
4486
  switch (provider) {
3108
4487
  case "codex":
3109
4488
  return addCodex(values.label);
@@ -3111,8 +4490,28 @@ async function cmdAdd(argv) {
3111
4490
  return values.token ? addClaudeTokenCli(values.label) : addClaudeProfile(values.label);
3112
4491
  case "opencode":
3113
4492
  return addOpenCodeCli(values.label);
4493
+ case "antigravity":
4494
+ return addAntigravityCli(values.label);
3114
4495
  case "cursor":
3115
4496
  fail("Cursor is single-account: just-usage shows whatever `cursor-agent` is logged in as.");
4497
+ case "grok":
4498
+ fail("Grok is single-account: just-usage shows whatever `grok` is logged in as.");
4499
+ }
4500
+ }
4501
+ async function addAntigravityCli(label) {
4502
+ const { sessionId, authUrl } = await beginAntigravityAdd(label);
4503
+ console.log(`
4504
+ Open this URL to sign in (opening your browser):
4505
+ ${authUrl}
4506
+ `);
4507
+ openInBrowser(authUrl);
4508
+ console.log("After Google sign-in, copy the code from the Antigravity page (or paste the callback URL).");
4509
+ const callback = await prompt("Code: ");
4510
+ try {
4511
+ const account = await submitAntigravityCallback(sessionId, callback);
4512
+ console.log(`Added ${account.id}${account.email ? ` (${account.email})` : ""}.`);
4513
+ } catch (e) {
4514
+ fail(e instanceof Error ? e.message : String(e));
3116
4515
  }
3117
4516
  }
3118
4517
  async function cmdLogin(argv) {
@@ -3124,6 +4523,7 @@ async function cmdLogin(argv) {
3124
4523
  fail(`Unknown account: ${id}`);
3125
4524
  if (rec.kind !== "profile" || !rec.path)
3126
4525
  fail(`${id} is a ${rec.kind} account; remove and re-add it instead.`);
4526
+ log("info", "account.login.start", { account: id, provider: rec.provider, kind: "relogin" });
3127
4527
  if (rec.provider === "codex") {
3128
4528
  const info = await codexLogin(rec.path, (url) => {
3129
4529
  console.log(`
@@ -3133,10 +4533,15 @@ ${url}
3133
4533
  openInBrowser(url);
3134
4534
  });
3135
4535
  updateAccount(id, { email: info.email });
4536
+ log("info", "account.login.done", { account: id, provider: "codex" });
3136
4537
  console.log(`Re-authenticated ${id}${info.email ? ` (${info.email})` : ""}.`);
3137
4538
  } else if (rec.provider === "claude") {
3138
4539
  await runInteractive("claude", ["auth", "login"], { CLAUDE_CONFIG_DIR: rec.path });
3139
4540
  const status = await claudeAuthStatus(rec.path);
4541
+ log(status?.loggedIn ? "info" : "error", status?.loggedIn ? "account.login.done" : "account.login.error", {
4542
+ account: id,
4543
+ provider: "claude"
4544
+ });
3140
4545
  console.log(status?.loggedIn ? `Re-authenticated ${id}.` : "Login did not complete.");
3141
4546
  } else {
3142
4547
  fail(`${providerName(rec.provider)} accounts cannot be re-authenticated this way.`);
@@ -3155,6 +4560,28 @@ async function cmdRemove(argv) {
3155
4560
  fail(e instanceof Error ? e.message : String(e));
3156
4561
  }
3157
4562
  }
4563
+ async function cmdStop(argv) {
4564
+ if (argv.includes("--help") || argv.includes("-h")) {
4565
+ console.log(STOP_HELP);
4566
+ return;
4567
+ }
4568
+ const { values } = parseArgs({
4569
+ args: argv,
4570
+ options: {
4571
+ port: { type: "string", short: "p" },
4572
+ all: { type: "boolean" }
4573
+ },
4574
+ allowPositionals: true,
4575
+ strict: false
4576
+ });
4577
+ const port = values.port ? Number(values.port) : undefined;
4578
+ if (values.port && (!Number.isInteger(port) || port <= 0 || port > 65535))
4579
+ fail(`invalid port: ${values.port}`);
4580
+ const { text, code } = formatStopResults(await stopServers({ port, all: values.all === true }));
4581
+ console.log(text);
4582
+ if (code !== 0)
4583
+ process.exit(code);
4584
+ }
3158
4585
  async function cmdUpgrade(argv) {
3159
4586
  const { values } = parseArgs({ args: argv, options: { check: { type: "boolean" }, yes: { type: "boolean", short: "y" } }, allowPositionals: true, strict: false });
3160
4587
  const info = await checkForUpdate(true);
@@ -3181,6 +4608,7 @@ async function cmdUpgrade(argv) {
3181
4608
  async function main() {
3182
4609
  const argv = process.argv.slice(2);
3183
4610
  const cmd = argv[0];
4611
+ log("info", "cli", { command: cmd ?? "serve" });
3184
4612
  if (cmd === "--version" || cmd === "-v" || cmd === "version") {
3185
4613
  console.log(VERSION);
3186
4614
  return;
@@ -3194,6 +4622,9 @@ async function main() {
3194
4622
  return cmdServe([]);
3195
4623
  case "serve":
3196
4624
  return cmdServe(argv.slice(1));
4625
+ case "stop":
4626
+ case "quit":
4627
+ return cmdStop(argv.slice(1));
3197
4628
  case "status":
3198
4629
  return cmdStatus(argv.slice(1));
3199
4630
  case "accounts":
@@ -3217,6 +4648,7 @@ ${HELP}`);
3217
4648
  }
3218
4649
  }
3219
4650
  main().catch((e) => {
4651
+ logError("cli.crash", e);
3220
4652
  console.error(e instanceof Error ? e.message : String(e));
3221
4653
  process.exit(1);
3222
4654
  });