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/CHANGELOG.md +19 -9
- package/README.md +5 -1
- package/dist/cli.js +1571 -139
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -15,8 +15,8 @@ import { mkdirSync } from "node:fs";
|
|
|
15
15
|
// package.json
|
|
16
16
|
var package_default = {
|
|
17
17
|
name: "just-usage",
|
|
18
|
-
version: "0.0.
|
|
19
|
-
description: "One local page for your coding-CLI subscription quotas: Claude, Codex, Cursor, OpenCode Go.",
|
|
18
|
+
version: "0.0.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
|
|
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
|
|
481
|
-
import { homedir as
|
|
482
|
-
import { join as
|
|
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:
|
|
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:
|
|
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
|
|
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 = (
|
|
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 (!
|
|
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 =
|
|
680
|
-
if (
|
|
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
|
|
833
|
-
import { homedir as
|
|
834
|
-
import { join as
|
|
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 :
|
|
841
|
-
return
|
|
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 (!
|
|
1264
|
+
if (!existsSync5(file))
|
|
846
1265
|
return null;
|
|
847
1266
|
try {
|
|
848
|
-
const parsed = JSON.parse(
|
|
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
|
|
909
|
-
import { dirname as dirname2, join as
|
|
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 (!
|
|
1389
|
+
if (!existsSync6(file))
|
|
913
1390
|
return { version: 1, accounts: [] };
|
|
914
1391
|
try {
|
|
915
|
-
const parsed = JSON.parse(
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
1185
|
-
import { homedir as
|
|
1186
|
-
import { join as
|
|
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
|
|
1191
|
-
var
|
|
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 (!
|
|
1793
|
+
if (!existsSync7(file))
|
|
1199
1794
|
return null;
|
|
1200
1795
|
try {
|
|
1201
|
-
return tokenFromAuthJson(JSON.parse(
|
|
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 =
|
|
1802
|
+
const home = homedir7();
|
|
1208
1803
|
const out = [];
|
|
1209
1804
|
if (process.platform === "win32") {
|
|
1210
|
-
const roaming = process.env.APPDATA ||
|
|
1211
|
-
out.push(
|
|
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 ||
|
|
1214
|
-
out.push(
|
|
1808
|
+
const xdg = process.env.XDG_CONFIG_HOME || join8(home, ".config");
|
|
1809
|
+
out.push(join8(xdg, "cursor", "auth.json"));
|
|
1215
1810
|
}
|
|
1216
|
-
out.push(
|
|
1811
|
+
out.push(join8(home, ".cursor", "auth.json"));
|
|
1217
1812
|
return out;
|
|
1218
1813
|
}
|
|
1219
|
-
async function
|
|
1814
|
+
async function tokenFromKeychain2() {
|
|
1220
1815
|
if (process.platform !== "darwin")
|
|
1221
1816
|
return null;
|
|
1222
|
-
const res = await run("security", ["find-generic-password", "-a",
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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:
|
|
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:
|
|
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);
|
|
1625
|
-
.credits-tip-left { color: var(--dim); font-variant-numeric: tabular-nums;
|
|
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
|
-
|
|
1631
|
-
.opts-row.
|
|
1632
|
-
|
|
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
|
-
<
|
|
1778
|
-
<
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
-
|
|
1806
|
-
|
|
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
|
-
|
|
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(
|
|
2961
|
+
const known = new Set(defaultOrder);
|
|
1831
2962
|
const next = raw.filter((id) => known.has(id));
|
|
1832
|
-
for (const id of
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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=
|
|
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
|
|
1897
|
-
return !!p?.accounts.some((a) => a.status
|
|
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
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
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="
|
|
2240
|
-
<button class="refresh" id="wiz-callback-go" type="button"
|
|
2241
|
-
: \`<button class="refresh" id="
|
|
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)}"
|
|
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)
|
|
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
|
-
|
|
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}
|
|
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
|
|
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 (!
|
|
4126
|
+
if (!existsSync11(file))
|
|
2818
4127
|
return null;
|
|
2819
4128
|
try {
|
|
2820
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
});
|