just-usage 0.0.5 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli.js +340 -78
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.6
4
+
5
+ ## What's Changed
6
+ Detect newly installed CLIs on refresh, keep provider tabs out of the URL, and write action logs. in [#3b3f92c](https://github.com/spheceo/just-usage/commit/3b3f92c086e55533da48ebac2023b41ce32d21a3)
7
+ Remove obsolete architecture plan in [#23d08e9](https://github.com/spheceo/just-usage/commit/23d08e96a8a485692062802d3e01886857573af9)
8
+
3
9
  ## v0.0.5
4
10
 
5
11
  ## What's Changed
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { mkdirSync } from "node:fs";
15
15
  // package.json
16
16
  var package_default = {
17
17
  name: "just-usage",
18
- version: "0.0.5",
18
+ version: "0.0.6",
19
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",
@@ -481,12 +481,15 @@ async function codexLogin(codexHome, onAuthUrl, timeoutMs = 5 * 60000) {
481
481
 
482
482
  // src/adapters/claude.ts
483
483
  import { createHash } from "node:crypto";
484
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
485
- import { homedir as homedir2 } from "node:os";
486
- import { join as join2 } from "node:path";
484
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
485
+ import { homedir as homedir3 } from "node:os";
486
+ import { join as join3 } from "node:path";
487
487
 
488
488
  // src/proc.ts
489
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";
490
493
  function run(cmd, args, opts = {}) {
491
494
  return new Promise((resolve) => {
492
495
  let stdout = "";
@@ -500,7 +503,7 @@ function run(cmd, args, opts = {}) {
500
503
  resolve({ code, stdout, stderr });
501
504
  };
502
505
  const child = spawn2(cmd, args, {
503
- env: { ...process.env, ...opts.env },
506
+ env: spawnEnv(opts.env),
504
507
  stdio: ["pipe", "pipe", "pipe"]
505
508
  });
506
509
  const timer = setTimeout(() => {
@@ -523,23 +526,70 @@ function run(cmd, args, opts = {}) {
523
526
  }
524
527
  function runInteractive(cmd, args, env) {
525
528
  return new Promise((resolve, reject) => {
526
- const child = spawn2(cmd, args, { env: { ...process.env, ...env }, stdio: "inherit" });
529
+ const child = spawn2(cmd, args, { env: spawnEnv(env), stdio: "inherit" });
527
530
  child.on("error", reject);
528
531
  child.on("close", (code) => resolve(code));
529
532
  });
530
533
  }
531
534
  var whichCache = new Map;
532
- function which(bin) {
535
+ function extraBinDirs() {
536
+ const home = homedir2();
537
+ const dirs = [
538
+ join2(home, ".local", "bin"),
539
+ join2(home, "bin"),
540
+ "/opt/homebrew/bin",
541
+ "/usr/local/bin",
542
+ "/home/linuxbrew/.linuxbrew/bin",
543
+ join2(home, ".npm-global", "bin")
544
+ ];
545
+ if (process.platform === "win32") {
546
+ const local = process.env.LOCALAPPDATA;
547
+ if (local)
548
+ dirs.push(join2(local, "agy", "bin"));
549
+ }
550
+ return dirs.filter((d) => existsSync(d));
551
+ }
552
+ function spawnEnv(extra) {
553
+ const pathKey = process.platform === "win32" && process.env.Path && !process.env.PATH ? "Path" : "PATH";
554
+ const current = extra?.[pathKey] ?? extra?.PATH ?? process.env[pathKey] ?? process.env.PATH ?? "";
555
+ const seen = new Set;
556
+ const parts = [];
557
+ for (const dir of [...extraBinDirs(), ...current.split(delimiter)]) {
558
+ if (!dir || seen.has(dir))
559
+ continue;
560
+ seen.add(dir);
561
+ parts.push(dir);
562
+ }
563
+ return { ...process.env, ...extra, [pathKey]: parts.join(delimiter) };
564
+ }
565
+ async function resolveBin(bin) {
566
+ const finder = process.platform === "win32" ? "where" : "which";
567
+ const res = await run(finder, [bin], { timeoutMs: 5000 });
568
+ if (res.code === 0) {
569
+ const first = res.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
570
+ if (first && existsSync(first))
571
+ return first;
572
+ }
573
+ const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`] : [bin];
574
+ for (const dir of extraBinDirs()) {
575
+ for (const name of names) {
576
+ const candidate = join2(dir, name);
577
+ if (existsSync(candidate))
578
+ return candidate;
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+ function clearBinCache() {
584
+ whichCache.clear();
585
+ versionCache.clear();
586
+ }
587
+ function which(bin, opts = {}) {
588
+ if (opts.fresh)
589
+ whichCache.delete(bin);
533
590
  let p = whichCache.get(bin);
534
591
  if (!p) {
535
- p = (async () => {
536
- const finder = process.platform === "win32" ? "where" : "which";
537
- const res = await run(finder, [bin], { timeoutMs: 5000 });
538
- if (res.code !== 0)
539
- return null;
540
- const first = res.stdout.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
541
- return first ?? null;
542
- })();
592
+ p = resolveBin(bin);
543
593
  whichCache.set(bin, p);
544
594
  }
545
595
  return p;
@@ -581,7 +631,7 @@ function withTimeout(p, ms, label) {
581
631
  }
582
632
 
583
633
  // src/secrets.ts
584
- import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
634
+ import { existsSync as existsSync2, readFileSync, writeFileSync, chmodSync } from "node:fs";
585
635
  import { dirname } from "node:path";
586
636
  var SERVICE = "just-usage";
587
637
 
@@ -608,7 +658,7 @@ class FileStore {
608
658
  kind = "file";
609
659
  read() {
610
660
  const file = paths.secrets();
611
- if (!existsSync(file))
661
+ if (!existsSync2(file))
612
662
  return {};
613
663
  try {
614
664
  return JSON.parse(readFileSync(file, "utf8"));
@@ -680,8 +730,8 @@ async function readClaudeCredentials(configDir) {
680
730
  return creds;
681
731
  }
682
732
  }
683
- const file = join2(configDir ?? join2(homedir2(), ".claude"), ".credentials.json");
684
- if (existsSync2(file))
733
+ const file = join3(configDir ?? join3(homedir3(), ".claude"), ".credentials.json");
734
+ if (existsSync3(file))
685
735
  return parseCreds(readFileSync2(file, "utf8"));
686
736
  return null;
687
737
  }
@@ -834,9 +884,9 @@ import { renameSync, rmSync as rmSync2 } from "node:fs";
834
884
 
835
885
  // src/adapters/antigravity.ts
836
886
  import { createHash as createHash2, randomBytes } from "node:crypto";
837
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
838
- import { homedir as homedir3 } from "node:os";
839
- import { join as join3 } from "node:path";
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";
840
890
  var AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
841
891
  var AGY_CLIENT_SECRET = "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";
842
892
  var AGY_REDIRECT_URI = "https://antigravity.google/oauth-callback";
@@ -853,11 +903,11 @@ var KEYCHAIN_SERVICE = "gemini";
853
903
  var KEYCHAIN_ACCOUNT = "antigravity";
854
904
  var FALLBACK_CLI_VERSION2 = "1.1.26";
855
905
  function settingsFile() {
856
- return join3(homedir3(), ".gemini", "antigravity-cli", "settings.json");
906
+ return join4(homedir4(), ".gemini", "antigravity-cli", "settings.json");
857
907
  }
858
908
  function usesGeminiApiKey() {
859
909
  try {
860
- if (!existsSync3(settingsFile()))
910
+ if (!existsSync4(settingsFile()))
861
911
  return false;
862
912
  const parsed = JSON.parse(readFileSync3(settingsFile(), "utf8"));
863
913
  return parsed.modelProvider === "gemini" && Boolean(process.env.GEMINI_API_KEY?.trim());
@@ -923,8 +973,18 @@ async function tokenFromSecretTool() {
923
973
  return null;
924
974
  return parseAgyKeyringBlob(res.stdout);
925
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
+ }
926
986
  async function readDefaultAgyToken() {
927
- return await tokenFromKeychain() ?? await tokenFromSecretTool();
987
+ return await tokenFromKeychain() ?? await tokenFromSecretTool() ?? tokenFromOauthFile();
928
988
  }
929
989
  function createPkce() {
930
990
  const verifier = randomBytes(32).toString("base64url");
@@ -1188,20 +1248,20 @@ async function fetchAntigravity(account) {
1188
1248
  }
1189
1249
 
1190
1250
  // src/adapters/opencode.ts
1191
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
1192
- import { homedir as homedir4 } from "node:os";
1193
- import { join as join4 } from "node:path";
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";
1194
1254
  var USAGE_URL2 = "https://opencode.ai/zen/go/v1/usage";
1195
1255
  function openCodeAuthFile() {
1196
1256
  if (process.env.OPENCODE_AUTH_FILE)
1197
1257
  return process.env.OPENCODE_AUTH_FILE;
1198
1258
  const xdg = process.env.XDG_DATA_HOME;
1199
- const base = xdg && xdg.trim() ? xdg : join4(homedir4(), ".local", "share");
1200
- return join4(base, "opencode", "auth.json");
1259
+ const base = xdg && xdg.trim() ? xdg : join5(homedir5(), ".local", "share");
1260
+ return join5(base, "opencode", "auth.json");
1201
1261
  }
1202
1262
  function readOpenCodeGoKey() {
1203
1263
  const file = openCodeAuthFile();
1204
- if (!existsSync4(file))
1264
+ if (!existsSync5(file))
1205
1265
  return null;
1206
1266
  try {
1207
1267
  const parsed = JSON.parse(readFileSync4(file, "utf8"));
@@ -1263,12 +1323,70 @@ async function fetchOpenCode(account) {
1263
1323
  }
1264
1324
  }
1265
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
+
1266
1384
  // src/registry.ts
1267
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "node:fs";
1268
- import { dirname as dirname2, join as join5 } from "node:path";
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";
1269
1387
  function readRegistry() {
1270
1388
  const file = paths.registry();
1271
- if (!existsSync5(file))
1389
+ if (!existsSync6(file))
1272
1390
  return { version: 1, accounts: [] };
1273
1391
  try {
1274
1392
  const parsed = JSON.parse(readFileSync5(file, "utf8"));
@@ -1304,7 +1422,7 @@ function newAccountId(provider, hint) {
1304
1422
  }
1305
1423
  }
1306
1424
  function profileDirFor(provider, id) {
1307
- return join5(paths.profiles(provider), id.split(":")[1] ?? "account");
1425
+ return join7(paths.profiles(provider), id.split(":")[1] ?? "account");
1308
1426
  }
1309
1427
  function saveAccount(record) {
1310
1428
  const reg = readRegistry();
@@ -1361,6 +1479,7 @@ async function addClaudeToken(token, label) {
1361
1479
  createdAt: new Date().toISOString()
1362
1480
  };
1363
1481
  saveAccount(account);
1482
+ log("info", "account.add", { account: account.id, provider: account.provider, kind: account.kind });
1364
1483
  return { account };
1365
1484
  }
1366
1485
  async function addOpenCodeKey(key, label) {
@@ -1383,6 +1502,12 @@ async function addOpenCodeKey(key, label) {
1383
1502
  createdAt: new Date().toISOString()
1384
1503
  };
1385
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
+ });
1386
1511
  return {
1387
1512
  account,
1388
1513
  warning: status === 403 ? "Key is valid but has no active OpenCode Go subscription." : undefined
@@ -1394,7 +1519,9 @@ function renameExtraAccount(id, label) {
1394
1519
  if (!getAccount(id))
1395
1520
  throw new AccountError(`Unknown account: ${id}`, 404);
1396
1521
  updateAccount(id, { label: label.trim() });
1397
- return getAccount(id);
1522
+ const account = getAccount(id);
1523
+ log("info", "account.rename", { account: account.id, provider: account.provider });
1524
+ return account;
1398
1525
  }
1399
1526
  async function removeExtraAccount(id) {
1400
1527
  if (id.endsWith(":default"))
@@ -1404,6 +1531,7 @@ async function removeExtraAccount(id) {
1404
1531
  throw new AccountError(`Unknown account: ${id}`, 404);
1405
1532
  if (rec.kind === "token")
1406
1533
  await secretStore().delete(id);
1534
+ log("info", "account.remove", { account: rec.id, provider: rec.provider, kind: rec.kind });
1407
1535
  return rec;
1408
1536
  }
1409
1537
  function saveCodexProfile(opts) {
@@ -1425,6 +1553,7 @@ function saveCodexProfile(opts) {
1425
1553
  createdAt: new Date().toISOString()
1426
1554
  };
1427
1555
  saveAccount(account);
1556
+ log("info", "account.add", { account: account.id, provider: account.provider, kind: account.kind });
1428
1557
  return account;
1429
1558
  }
1430
1559
  function isLocalCallbackUrl(raw) {
@@ -1464,7 +1593,7 @@ function dropCodexSession(id, removeDir) {
1464
1593
  }
1465
1594
  }
1466
1595
  async function beginCodexAdd(label) {
1467
- if (!await which("codex"))
1596
+ if (!await which("codex", { fresh: true }))
1468
1597
  throw new AccountError("codex is not installed (npm i -g @openai/codex).");
1469
1598
  const tmpId = newAccountId("codex", label?.trim() || "pending");
1470
1599
  const dir = ensureDir(profileDirFor("codex", tmpId));
@@ -1483,11 +1612,14 @@ async function beginCodexAdd(label) {
1483
1612
  handle.completed.then((info) => {
1484
1613
  rec.account = saveCodexProfile({ label: rec.label, tmpId, dir, email: info.email });
1485
1614
  rec.status = "done";
1615
+ log("info", "account.login.done", { account: rec.account.id, provider: "codex" });
1486
1616
  }).catch((e) => {
1487
1617
  rec.status = "error";
1488
1618
  rec.error = e instanceof Error ? e.message : String(e);
1619
+ logError("account.login.error", e, { provider: "codex" });
1489
1620
  });
1490
1621
  codexSessions.set(id, rec);
1622
+ log("info", "account.login.start", { provider: "codex", kind: "add" });
1491
1623
  return { sessionId: id, authUrl: handle.authUrl };
1492
1624
  }
1493
1625
  async function beginCodexRelogin(accountId) {
@@ -1512,11 +1644,14 @@ async function beginCodexRelogin(accountId) {
1512
1644
  updateAccount(existing.id, { email: info.email });
1513
1645
  rec.account = { ...existing, email: info.email };
1514
1646
  rec.status = "done";
1647
+ log("info", "account.login.done", { account: existing.id, provider: "codex" });
1515
1648
  }).catch((e) => {
1516
1649
  rec.status = "error";
1517
1650
  rec.error = e instanceof Error ? e.message : String(e);
1651
+ logError("account.login.error", e, { account: existing.id, provider: "codex" });
1518
1652
  });
1519
1653
  codexSessions.set(id, rec);
1654
+ log("info", "account.login.start", { account: existing.id, provider: "codex", kind: "relogin" });
1520
1655
  return { sessionId: id, authUrl: handle.authUrl };
1521
1656
  }
1522
1657
  function codexSessionStatus(sessionId) {
@@ -1563,9 +1698,11 @@ function startAgySession(opts) {
1563
1698
  return { sessionId: id, authUrl };
1564
1699
  }
1565
1700
  async function beginAntigravityAdd(label) {
1566
- if (!await which("agy"))
1701
+ if (!await which("agy", { fresh: true }))
1567
1702
  throw new AccountError("agy is not installed (https://antigravity.google/docs/cli/install).");
1568
- return startAgySession({ label });
1703
+ const started = startAgySession({ label });
1704
+ log("info", "account.login.start", { provider: "antigravity", kind: "add" });
1705
+ return started;
1569
1706
  }
1570
1707
  async function beginAntigravityRelogin(accountId) {
1571
1708
  const existing = getAccount(accountId);
@@ -1574,7 +1711,9 @@ async function beginAntigravityRelogin(accountId) {
1574
1711
  if (existing.provider !== "antigravity" || existing.kind !== "token") {
1575
1712
  throw new AccountError(`${accountId} cannot be re-authenticated this way.`);
1576
1713
  }
1577
- return startAgySession({ accountId });
1714
+ const started = startAgySession({ accountId });
1715
+ log("info", "account.login.start", { account: accountId, provider: "antigravity", kind: "relogin" });
1716
+ return started;
1578
1717
  }
1579
1718
  function antigravitySessionStatus(sessionId) {
1580
1719
  const rec = agySessions.get(sessionId);
@@ -1617,21 +1756,29 @@ async function submitAntigravityCallback(sessionId, raw) {
1617
1756
  saveAccount(rec.account);
1618
1757
  }
1619
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
+ });
1620
1764
  return rec.account;
1621
1765
  } catch (e) {
1622
1766
  rec.status = "error";
1623
1767
  rec.error = e instanceof Error ? e.message : String(e);
1768
+ logError("account.login.error", e, { provider: "antigravity", account: rec.accountId });
1624
1769
  throw new AccountError(rec.error);
1625
1770
  }
1626
1771
  }
1627
1772
 
1628
1773
  // src/collect.ts
1629
- import { hostname } from "node:os";
1774
+ import { existsSync as existsSync9 } from "node:fs";
1775
+ import { homedir as homedir9, hostname } from "node:os";
1776
+ import { join as join10 } from "node:path";
1630
1777
 
1631
1778
  // src/adapters/cursor.ts
1632
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1633
- import { homedir as homedir5 } from "node:os";
1634
- import { join as join6 } from "node:path";
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";
1635
1782
  var USAGE_URL3 = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage";
1636
1783
  var GROK_BOT_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetSandUsageStatus";
1637
1784
  var PLAN_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetPlanInfo";
@@ -1643,7 +1790,7 @@ function tokenFromAuthJson(parsed) {
1643
1790
  return typeof parsed.accessToken === "string" && parsed.accessToken ? parsed.accessToken : null;
1644
1791
  }
1645
1792
  function tokenFromAuthFile(file) {
1646
- if (!existsSync6(file))
1793
+ if (!existsSync7(file))
1647
1794
  return null;
1648
1795
  try {
1649
1796
  return tokenFromAuthJson(JSON.parse(readFileSync6(file, "utf8")));
@@ -1652,16 +1799,16 @@ function tokenFromAuthFile(file) {
1652
1799
  }
1653
1800
  }
1654
1801
  function authFiles() {
1655
- const home = homedir5();
1802
+ const home = homedir7();
1656
1803
  const out = [];
1657
1804
  if (process.platform === "win32") {
1658
- const roaming = process.env.APPDATA || join6(home, "AppData", "Roaming");
1659
- out.push(join6(roaming, "Cursor", "auth.json"));
1805
+ const roaming = process.env.APPDATA || join8(home, "AppData", "Roaming");
1806
+ out.push(join8(roaming, "Cursor", "auth.json"));
1660
1807
  } else if (process.platform !== "darwin") {
1661
- const xdg = process.env.XDG_CONFIG_HOME || join6(home, ".config");
1662
- out.push(join6(xdg, "cursor", "auth.json"));
1808
+ const xdg = process.env.XDG_CONFIG_HOME || join8(home, ".config");
1809
+ out.push(join8(xdg, "cursor", "auth.json"));
1663
1810
  }
1664
- out.push(join6(home, ".cursor", "auth.json"));
1811
+ out.push(join8(home, ".cursor", "auth.json"));
1665
1812
  return out;
1666
1813
  }
1667
1814
  async function tokenFromKeychain2() {
@@ -1841,9 +1988,9 @@ async function fetchCursor(account) {
1841
1988
  }
1842
1989
 
1843
1990
  // src/adapters/grok.ts
1844
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
1845
- import { homedir as homedir6 } from "node:os";
1846
- import { join as join7 } from "node:path";
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";
1847
1994
  var DEFAULT_PROXY = "https://cli-chat-proxy.grok.com/v1";
1848
1995
  var TOKEN_URL = "https://auth.x.ai/oauth2/token";
1849
1996
  var FALLBACK_CLI_VERSION3 = "1.0.13";
@@ -1858,7 +2005,7 @@ var TIER_NAMES = {
1858
2005
  function authFile() {
1859
2006
  if (process.env.GROK_AUTH_FILE)
1860
2007
  return process.env.GROK_AUTH_FILE;
1861
- return join7(homedir6(), ".grok", "auth.json");
2008
+ return join9(homedir8(), ".grok", "auth.json");
1862
2009
  }
1863
2010
  function proxyBase() {
1864
2011
  const raw = process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim();
@@ -1908,7 +2055,7 @@ function sessionFromAuthJson(parsed) {
1908
2055
  }
1909
2056
  function readGrokSession() {
1910
2057
  const file = authFile();
1911
- if (!existsSync7(file))
2058
+ if (!existsSync8(file))
1912
2059
  return null;
1913
2060
  try {
1914
2061
  return sessionFromAuthJson(JSON.parse(readFileSync7(file, "utf8")));
@@ -2102,10 +2249,53 @@ function providerName(id) {
2102
2249
  }
2103
2250
 
2104
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
+ }
2105
2286
  async function detectProviders() {
2287
+ clearBinCache();
2106
2288
  return Promise.all(PROVIDERS.map(async (p) => {
2107
- const path = await which(p.bin);
2108
- return { id: p.id, installed: path !== null, version: null };
2289
+ const path = await which(p.bin, { fresh: true });
2290
+ const homeExists = providerHomeExists(p.id);
2291
+ const installed = isProviderPresent({ binPath: path, homeExists, provider: p.id });
2292
+ log("info", "detect", {
2293
+ provider: p.id,
2294
+ installed,
2295
+ path: path ?? undefined,
2296
+ home: homeExists || undefined
2297
+ });
2298
+ return { id: p.id, installed, version: null };
2109
2299
  }));
2110
2300
  }
2111
2301
  function resolveAccounts(provider, installed) {
@@ -2120,9 +2310,24 @@ function resolveAccounts(provider, installed) {
2120
2310
  }
2121
2311
  async function fetchAccount(account) {
2122
2312
  try {
2123
- return await withTimeout(fetchSnapshot(account), FETCH_TIMEOUT_MS, `${account.provider} fetch`);
2313
+ const snap = await withTimeout(fetchSnapshot(account), FETCH_TIMEOUT_MS, `${account.provider} fetch`);
2314
+ log(snap.status === "error" ? "error" : "info", "quotas.fetch", {
2315
+ account: snap.account.id,
2316
+ provider: snap.account.provider,
2317
+ status: snap.status,
2318
+ windows: snap.windows.length,
2319
+ message: snap.message ?? undefined
2320
+ });
2321
+ return snap;
2124
2322
  } catch (e) {
2125
- return snapshot(account, "error", { message: e instanceof Error ? e.message : String(e) });
2323
+ const snap = snapshot(account, "error", { message: e instanceof Error ? e.message : String(e) });
2324
+ log("error", "quotas.fetch", {
2325
+ account: account.id,
2326
+ provider: account.provider,
2327
+ status: snap.status,
2328
+ message: snap.message ?? undefined
2329
+ });
2330
+ return snap;
2126
2331
  }
2127
2332
  }
2128
2333
  async function collectReport(update, only) {
@@ -2132,10 +2337,19 @@ async function collectReport(update, only) {
2132
2337
  const accounts = resolveAccounts(p.id, pres.installed);
2133
2338
  const [snapshots, version] = await Promise.all([
2134
2339
  Promise.all(accounts.map(fetchAccount)),
2135
- pres.installed ? binVersion(p.bin) : Promise.resolve(null)
2340
+ pres.installed ? binVersion(await which(p.bin) ?? p.bin) : Promise.resolve(null)
2136
2341
  ]);
2137
2342
  return { id: p.id, name: p.name, installed: pres.installed, version, accounts: snapshots };
2138
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
+ });
2139
2353
  return {
2140
2354
  version: VERSION,
2141
2355
  hostname: formatHostname(hostname()),
@@ -2180,7 +2394,7 @@ class ReportCache {
2180
2394
  }
2181
2395
 
2182
2396
  // src/instance.ts
2183
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2397
+ import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2184
2398
  function looksLikeJustUsageCommand(command) {
2185
2399
  const s = command.replace(/\\/g, "/").toLowerCase();
2186
2400
  return s.includes("just-usage") || /\/just-usage\/(?:src\/|dist\/)?cli\.(ts|js)\b/.test(s);
@@ -2217,7 +2431,7 @@ function writeRunRecord(opts) {
2217
2431
  }
2218
2432
  function readRunRecord(port) {
2219
2433
  const file = paths.runRecord(port);
2220
- if (!existsSync8(file))
2434
+ if (!existsSync10(file))
2221
2435
  return null;
2222
2436
  return parseRunRecord(readFileSync8(file, "utf8"));
2223
2437
  }
@@ -2229,7 +2443,7 @@ function removeRunRecord(port) {
2229
2443
  }
2230
2444
  function listRunPorts() {
2231
2445
  const dir = paths.runDir();
2232
- if (!existsSync8(dir))
2446
+ if (!existsSync10(dir))
2233
2447
  return [];
2234
2448
  const ports = [];
2235
2449
  for (const name of readdirSync(dir)) {
@@ -2350,13 +2564,17 @@ async function terminate(pids) {
2350
2564
  async function stopPort(port) {
2351
2565
  const found = await oursOnPort(port);
2352
2566
  if (found.pids.length === 0) {
2353
- if (found.listening && !isJustUsageHealth(found.health))
2567
+ if (found.listening && !isJustUsageHealth(found.health)) {
2568
+ log("warn", "stop.busy", { port });
2354
2569
  return { status: "busy", port };
2570
+ }
2355
2571
  removeRunRecord(port);
2572
+ log("info", "stop.idle", { port });
2356
2573
  return { status: "idle", port };
2357
2574
  }
2358
2575
  await terminate(found.pids);
2359
2576
  removeRunRecord(port);
2577
+ log("info", "stop.ok", { port, pids: found.pids });
2360
2578
  return { status: "stopped", port, pids: found.pids };
2361
2579
  }
2362
2580
  async function stopServers(opts) {
@@ -2714,8 +2932,8 @@ var ui_default = `<!doctype html>
2714
2932
  if (cached && Array.isArray(cached.providers)) report = cached;
2715
2933
  } catch {}
2716
2934
  const params = new URLSearchParams(location.search);
2717
- const wanted = params.get("tab");
2718
- let active = PROVIDERS.some(([id]) => id === wanted) ? wanted : localStorage.getItem("ju.tab") || "claude";
2935
+ let active = localStorage.getItem("ju.tab") || "claude";
2936
+ if (!PROVIDERS.some(([id]) => id === active)) active = "claude";
2719
2937
  let settingsOpen = params.get("view") === "settings";
2720
2938
  let loading = false;
2721
2939
  let busy = false;
@@ -2864,16 +3082,17 @@ var ui_default = `<!doctype html>
2864
3082
  else render();
2865
3083
  }
2866
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
+
2867
3090
  function setView(open) {
2868
3091
  settingsOpen = open;
2869
3092
  $("dash").hidden = open;
2870
3093
  $("settings").hidden = !open;
2871
- const btn = $("opts-btn");
2872
3094
  $("opts-label").textContent = open ? "Back" : "Options";
2873
- const q = new URLSearchParams();
2874
- if (open) q.set("view", "settings");
2875
- else q.set("tab", active);
2876
- history.replaceState(null, "", q.toString() ? \`?\${q}\` : location.pathname);
3095
+ syncUrl();
2877
3096
  if (open) {
2878
3097
  renderSettings();
2879
3098
  renderChrome();
@@ -3534,6 +3753,7 @@ var ui_default = `<!doctype html>
3534
3753
  document.addEventListener("visibilitychange", () => {
3535
3754
  if (document.visibilityState === "visible") maybeAutoRefresh();
3536
3755
  });
3756
+ syncUrl();
3537
3757
  if (report) render();
3538
3758
  load(false);
3539
3759
  })();
@@ -3641,9 +3861,23 @@ async function startServer(opts) {
3641
3861
  cache.invalidate();
3642
3862
  return out;
3643
3863
  };
3864
+ const quietPath = (path) => path === "/favicon.svg" || path === "/favicon.ico" || path.startsWith("/logos/");
3644
3865
  const handler = async (req, res) => {
3645
3866
  const url = new URL(req.url ?? "/", "http://localhost");
3867
+ const started = Date.now();
3646
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
+ }
3647
3881
  try {
3648
3882
  const method = req.method ?? "GET";
3649
3883
  if ((url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico") && (method === "GET" || method === "HEAD")) {
@@ -3745,6 +3979,7 @@ async function startServer(opts) {
3745
3979
  json(res, 404, { error: "not found" });
3746
3980
  } catch (e) {
3747
3981
  if (e instanceof AccountError) {
3982
+ log("warn", "http.error", { path: url.pathname, status: e.status, message: e.message });
3748
3983
  json(res, e.status, { error: e.message });
3749
3984
  return;
3750
3985
  }
@@ -3752,6 +3987,7 @@ async function startServer(opts) {
3752
3987
  json(res, 400, { error: "Invalid JSON." });
3753
3988
  return;
3754
3989
  }
3990
+ logError("http.error", e, { path: url.pathname, status: 500 });
3755
3991
  json(res, 500, { error: e instanceof Error ? e.message : String(e) });
3756
3992
  }
3757
3993
  };
@@ -3760,12 +3996,15 @@ async function startServer(opts) {
3760
3996
  server.once("error", reject);
3761
3997
  server.listen(opts.port, opts.host, () => {
3762
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) });
3763
4001
  resolve({
3764
4002
  close: () => {
4003
+ log("info", "serve.stop", { port: opts.port });
3765
4004
  removeRunRecord(opts.port);
3766
4005
  server.close();
3767
4006
  },
3768
- urls: reachableUrls(opts.host, opts.port, tailscaleIp)
4007
+ urls
3769
4008
  });
3770
4009
  });
3771
4010
  });
@@ -3879,12 +4118,12 @@ function renderReport(report, now = Date.now()) {
3879
4118
  }
3880
4119
 
3881
4120
  // src/update.ts
3882
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
4121
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
3883
4122
  import { dirname as dirname3 } from "node:path";
3884
4123
  var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
3885
4124
  function readCache() {
3886
4125
  const file = paths.updateCache();
3887
- if (!existsSync9(file))
4126
+ if (!existsSync11(file))
3888
4127
  return null;
3889
4128
  try {
3890
4129
  const parsed = JSON.parse(readFileSync9(file, "utf8"));
@@ -3941,11 +4180,15 @@ async function checkForUpdate(force = false) {
3941
4180
  latest = null;
3942
4181
  }
3943
4182
  }
3944
- if (!latest)
4183
+ if (!latest) {
4184
+ log("warn", "update.check", { ok: false });
3945
4185
  return cached ? toInfo(cached) : null;
4186
+ }
3946
4187
  const entry = { checkedAt: new Date().toISOString(), latest };
3947
4188
  writeCache(entry);
3948
- return toInfo(entry);
4189
+ const info = toInfo(entry);
4190
+ log("info", "update.check", { latest: info.latest, available: info.available });
4191
+ return info;
3949
4192
  }
3950
4193
  function toInfo(c) {
3951
4194
  return { current: VERSION, latest: c.latest, available: semverGt(c.latest, VERSION), checkedAt: c.checkedAt };
@@ -3976,7 +4219,10 @@ function upgradeCommand(pm, version = "latest") {
3976
4219
  async function runUpgrade(pm, version = "latest") {
3977
4220
  const [cmd, args] = upgradeCommand(pm, version);
3978
4221
  console.log(`$ ${cmd} ${args.join(" ")}`);
3979
- return runInteractive(cmd, args);
4222
+ log("info", "update.upgrade", { pm, version });
4223
+ const code = await runInteractive(cmd, args);
4224
+ log(code === 0 ? "info" : "error", "update.upgrade.done", { pm, version, code: code ?? undefined });
4225
+ return code;
3980
4226
  }
3981
4227
 
3982
4228
  // src/cli.ts
@@ -4008,6 +4254,8 @@ Cursor uses whatever \`cursor-agent\` is logged in as (single account).
4008
4254
  Grok uses whatever \`grok login --oauth\` stored (single account).
4009
4255
  Antigravity extras are extra Google logins; they do not replace \`agy\`'s signed-in account.
4010
4256
 
4257
+ Logs are appended to ~/.just-usage/logs (one JSON line per action).
4258
+
4011
4259
  Stop
4012
4260
  \`just-usage stop\` asks the server to exit — the same as Ctrl+C in the terminal
4013
4261
  that started it. Use this when that terminal is gone or another instance is
@@ -4054,6 +4302,7 @@ See also
4054
4302
  just-usage --help All commands
4055
4303
  `;
4056
4304
  function fail(msg, code = 1) {
4305
+ log("error", "cli.fail", { message: msg, code });
4057
4306
  console.error(msg);
4058
4307
  process.exit(code);
4059
4308
  }
@@ -4120,6 +4369,7 @@ Update available: v${u.current} → v${u.latest}. Run: just-usage upgrade
4120
4369
  fail(`Port ${port} is already in use. Stop it with: ${stop}
4121
4370
  Or start another: just-usage serve --port ${port + 1}`);
4122
4371
  }
4372
+ logError("serve.error", e, { host, port });
4123
4373
  throw e;
4124
4374
  }
4125
4375
  console.log(`${PACKAGE_NAME} v${VERSION}`);
@@ -4141,6 +4391,7 @@ Press Ctrl+C to stop. From another terminal: ${stopHint}`);
4141
4391
  }
4142
4392
  async function cmdStatus(argv) {
4143
4393
  const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" } }, allowPositionals: true, strict: false });
4394
+ log("info", "cli.status", { json: values.json === true });
4144
4395
  const [report, update] = await Promise.all([collectReport(null), checkForUpdate().catch(() => null)]);
4145
4396
  report.update = update;
4146
4397
  if (values.json) {
@@ -4151,6 +4402,7 @@ async function cmdStatus(argv) {
4151
4402
  }
4152
4403
  function cmdAccounts() {
4153
4404
  const rows = listAccounts();
4405
+ log("info", "cli.accounts", { extra: rows.length });
4154
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).");
4155
4407
  if (rows.length === 0) {
4156
4408
  console.log(`
@@ -4202,9 +4454,11 @@ async function addClaudeProfile(label) {
4202
4454
  if (!status?.loggedIn) {
4203
4455
  const { rmSync } = await import("node:fs");
4204
4456
  rmSync(dir, { recursive: true, force: true });
4457
+ log("error", "account.login.error", { provider: "claude", message: "login did not complete" });
4205
4458
  fail("Login did not complete; nothing was saved.");
4206
4459
  }
4207
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" });
4208
4462
  console.log(`
4209
4463
  Added ${id}. Note: on macOS the first read may trigger a Keychain prompt — choose "Always Allow".`);
4210
4464
  }
@@ -4269,6 +4523,7 @@ async function cmdLogin(argv) {
4269
4523
  fail(`Unknown account: ${id}`);
4270
4524
  if (rec.kind !== "profile" || !rec.path)
4271
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" });
4272
4527
  if (rec.provider === "codex") {
4273
4528
  const info = await codexLogin(rec.path, (url) => {
4274
4529
  console.log(`
@@ -4278,10 +4533,15 @@ ${url}
4278
4533
  openInBrowser(url);
4279
4534
  });
4280
4535
  updateAccount(id, { email: info.email });
4536
+ log("info", "account.login.done", { account: id, provider: "codex" });
4281
4537
  console.log(`Re-authenticated ${id}${info.email ? ` (${info.email})` : ""}.`);
4282
4538
  } else if (rec.provider === "claude") {
4283
4539
  await runInteractive("claude", ["auth", "login"], { CLAUDE_CONFIG_DIR: rec.path });
4284
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
+ });
4285
4545
  console.log(status?.loggedIn ? `Re-authenticated ${id}.` : "Login did not complete.");
4286
4546
  } else {
4287
4547
  fail(`${providerName(rec.provider)} accounts cannot be re-authenticated this way.`);
@@ -4348,6 +4608,7 @@ async function cmdUpgrade(argv) {
4348
4608
  async function main() {
4349
4609
  const argv = process.argv.slice(2);
4350
4610
  const cmd = argv[0];
4611
+ log("info", "cli", { command: cmd ?? "serve" });
4351
4612
  if (cmd === "--version" || cmd === "-v" || cmd === "version") {
4352
4613
  console.log(VERSION);
4353
4614
  return;
@@ -4387,6 +4648,7 @@ ${HELP}`);
4387
4648
  }
4388
4649
  }
4389
4650
  main().catch((e) => {
4651
+ logError("cli.crash", e);
4390
4652
  console.error(e instanceof Error ? e.message : String(e));
4391
4653
  process.exit(1);
4392
4654
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "just-usage",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "One local page for your coding-CLI subscription quotas: Claude, Codex, Cursor, Antigravity, Grok, OpenCode Go.",
5
5
  "type": "module",
6
6
  "license": "MIT",