tokentracker-cli 0.72.1 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +5 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{ActivityHeatmap-CtDL5OPO.js → ActivityHeatmap-DkoSqxrd.js} +1 -1
  7. package/dashboard/dist/assets/{Card-C5lKqKNT.js → Card-DleS-r54.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-XnEef7Ra.js → CommunityStatsModal-DV41-F9D.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-CkZvn-sV.js → ConfirmModal-Dgp07Uka.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-TmZBLaB9.js → DashboardPage-EKy2BzUx.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-BhDh3VIP.js → DevicePage-eJInQ2UX.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-CddI1gcT.js → DialogClose-DEfesqFq.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-D6txiEc4.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-Cvpx9Lfv.js → DialogTitle-v1JrTvr4.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-BojLJcFD.js → FadeIn-hqIBbKaJ.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BrSt1RIj.js → HeaderGithubStar-C4zauuP2.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-DxuF-1tF.js → IpCheckPage-CiRcccks.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-DmCjJVwh.js → LandingPage-CbOHycnd.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-B8SGUidW.js → LeaderboardAvatar-CQL1iOTS.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-D7CQCdDr.js → LeaderboardPage-BuEp247d.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-Dv7CQVqB.js → LeaderboardProfileModal-DT8hrPkI.js} +1 -1
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-BtGLbQnT.js → LeaderboardProfilePage-B-M0AXMb.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-DRf6h5_2.js → LimitsPage-Df2es3Xs.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-BnlODmxA.js → LocalOnlyNotice-DJIfKTKo.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CYXdV0X0.js → LoginPage-DtaaIy_8.js} +1 -1
  26. package/dashboard/dist/assets/{PopoverPopup-x5vYN3uF.js → PopoverPopup-CqWH6aB9.js} +1 -1
  27. package/dashboard/dist/assets/{ResetPasswordPage-D8ifLVwK.js → ResetPasswordPage-DCH5GU1c.js} +1 -1
  28. package/dashboard/dist/assets/{Select-CaaIuo5M.js → Select-BwyliVKJ.js} +1 -1
  29. package/dashboard/dist/assets/{SelectItemText-CdJ-E-d1.js → SelectItemText-B5_DKKEv.js} +1 -1
  30. package/dashboard/dist/assets/{SettingsPage-Dc7v5PEC.js → SettingsPage-BoAFdOUa.js} +1 -1
  31. package/dashboard/dist/assets/{SkillsPage-CRbEMatQ.js → SkillsPage-BVdre-kF.js} +1 -1
  32. package/dashboard/dist/assets/{WidgetsPage-xgy8mLMV.js → WidgetsPage-CtjV405P.js} +1 -1
  33. package/dashboard/dist/assets/{WrappedPage-BdpfOWJu.js → WrappedPage-DGmPdtHt.js} +1 -1
  34. package/dashboard/dist/assets/{agent-logos-CrxAAMko.js → agent-logos-DqevdWvx.js} +1 -1
  35. package/dashboard/dist/assets/{arrow-up-right-D23bZC75.js → arrow-up-right-DA5Pybq1.js} +1 -1
  36. package/dashboard/dist/assets/check-BWzAT1oX.js +1 -0
  37. package/dashboard/dist/assets/{copy-hiVk9JEl.js → copy-CsQRTd89.js} +1 -1
  38. package/dashboard/dist/assets/{download-CqLiBDG-.js → download-TEHkAKT0.js} +1 -1
  39. package/dashboard/dist/assets/{format-tokens-Ck5lRlFd.js → format-tokens-CtFO_jkz.js} +1 -1
  40. package/dashboard/dist/assets/{info-CxDHBJXB.js → info-Dxn3fiaX.js} +1 -1
  41. package/dashboard/dist/assets/main-Dxi4sSXf.js +1128 -0
  42. package/dashboard/dist/assets/{use-community-stats-CeaLHlCk.js → use-community-stats-BLNxr-bQ.js} +1 -1
  43. package/dashboard/dist/assets/{use-limits-display-prefs-CvDWYfjp.js → use-limits-display-prefs-CC7gb303.js} +1 -1
  44. package/dashboard/dist/assets/{use-native-settings-HuxDwmn6.js → use-native-settings-SYtjLlxZ.js} +1 -1
  45. package/dashboard/dist/assets/{use-ordered-list-KIV2M5tf.js → use-ordered-list-_O8onJV7.js} +1 -1
  46. package/dashboard/dist/assets/{use-quality-per-dollar-pref-BbsMhRJt.js → use-quality-per-dollar-pref-tuspH_6n.js} +1 -1
  47. package/dashboard/dist/assets/{use-usage-limits-COBOepfU.js → use-usage-limits-whCVzQbf.js} +1 -1
  48. package/dashboard/dist/assets/{useCurrency-DbBq0Fal.js → useCurrency-BiDjYxfm.js} +1 -1
  49. package/dashboard/dist/assets/{useScrollLock-C4eBSnib.js → useScrollLock-DK-ndHcw.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dashboard/dist/share.html +1 -1
  52. package/package.json +1 -1
  53. package/src/commands/status.js +47 -4
  54. package/src/commands/sync.js +72 -18
  55. package/src/lib/local-api.js +24 -2
  56. package/src/lib/pricing/curated-overrides.json +7 -2
  57. package/src/lib/pricing/seed-snapshot.json +1 -1
  58. package/src/lib/rollout.js +385 -0
  59. package/src/lib/sqlite-reader.js +24 -6
  60. package/src/lib/usage-limits.js +200 -29
  61. package/src/lib/zcode-limits.js +375 -40
  62. package/dashboard/dist/assets/DialogDescription-DeEet5bz.js +0 -1
  63. package/dashboard/dist/assets/check-fkZxgcVQ.js +0 -1
  64. package/dashboard/dist/assets/main-B0LYKUIY.js +0 -1127
@@ -6,6 +6,7 @@ const path = require("node:path");
6
6
  const http = require("node:http");
7
7
  const https = require("node:https");
8
8
  const { performance } = require("node:perf_hooks");
9
+ const { promisify } = require("node:util");
9
10
 
10
11
  const {
11
12
  detectClaudeCodeSubscriptionDetails,
@@ -26,7 +27,9 @@ const {
26
27
  const { fetchGrokLimits } = require("./grok-limits");
27
28
  const { fetchZcodeLimits } = require("./zcode-limits");
28
29
  const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
29
- const { readSqliteJsonRows } = require("./sqlite-reader");
30
+ const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
31
+
32
+ const execFileAsync = promisify(cp.execFile);
30
33
 
31
34
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
32
35
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
@@ -1418,11 +1421,11 @@ function parseKiroUsageOutput(output, { now = new Date() } = {}) {
1418
1421
  // ─────────────────────────────────────────────────────────────────────────────
1419
1422
  // GitHub Copilot — `GET https://api.github.com/copilot_internal/user`
1420
1423
  // Reuses the OAuth token from the user's existing Copilot install. Older clients
1421
- // keep it in plaintext (`~/.config/github-copilot/{apps,hosts}.json`); recent
1422
- // copilot-language-server builds (Zed, copilot.vim) migrate it into an encrypted
1423
- // SQLite store (`auth.db`) and leave the plaintext files behind as stale legacy
1424
- // copies. Read the plaintext first, then fall back to the encrypted store. No
1425
- // device flow needed either way.
1424
+ // keep it in plaintext (`~/.config/github-copilot/{apps,hosts}.json`); newer
1425
+ // copilot-language-server builds (Zed, copilot.vim) migrate it into SQLite
1426
+ // `auth.db`. Schema v0 stores the OAuth token as a plaintext BLOB, while later
1427
+ // schemas use AES-GCM ciphertext with the key in the OS credential store. Read
1428
+ // legacy plaintext first, then auth.db. No device flow needed either way.
1426
1429
  // ─────────────────────────────────────────────────────────────────────────────
1427
1430
 
1428
1431
  const MACOS_SECURITY_BIN = "/usr/bin/security";
@@ -1433,7 +1436,7 @@ const MACOS_SECURITY_BIN = "/usr/bin/security";
1433
1436
  const COPILOT_LS_KEYCHAIN_SERVICE = "copilot-language-server";
1434
1437
  const COPILOT_LS_KEYCHAIN_ACCOUNT = "oauth-token-key";
1435
1438
  const COPILOT_AUTH_DB_SQL =
1436
- "SELECT user_login, auth_authority, scopes, hex(token_ciphertext) AS token_hex, "
1439
+ "SELECT user_login, auth_authority, scopes, token_schema_version, hex(token_ciphertext) AS token_hex, "
1437
1440
  + "last_used_at, updated_at FROM oauth_tokens ORDER BY last_used_at DESC, updated_at DESC";
1438
1441
 
1439
1442
  function readPlaintextCopilotOauthToken({ home }) {
@@ -1489,6 +1492,32 @@ function readMacosKeychainGenericPassword({ service, account, securityRunner } =
1489
1492
  return trimmed.length > 0 ? trimmed : null;
1490
1493
  }
1491
1494
 
1495
+ async function readMacosKeychainGenericPasswordAsync({ service, account, securityRunner } = {}) {
1496
+ if (typeof securityRunner === "function") {
1497
+ return readMacosKeychainGenericPassword({ service, account, securityRunner });
1498
+ }
1499
+ if (!fs.existsSync(MACOS_SECURITY_BIN)) return null;
1500
+ const args = ["find-generic-password", "-s", service];
1501
+ if (account) args.push("-a", account);
1502
+ args.push("-w");
1503
+ try {
1504
+ const result = await execFileAsync(MACOS_SECURITY_BIN, args, {
1505
+ timeout: 2000,
1506
+ encoding: "utf8",
1507
+ });
1508
+ const stdout =
1509
+ typeof result?.stdout === "string"
1510
+ ? result.stdout
1511
+ : Buffer.isBuffer(result?.stdout)
1512
+ ? result.stdout.toString("utf8")
1513
+ : "";
1514
+ const trimmed = stdout.trim();
1515
+ return trimmed.length > 0 ? trimmed : null;
1516
+ } catch (_e) {
1517
+ return null;
1518
+ }
1519
+ }
1520
+
1492
1521
  function decryptCopilotAuthDbToken(keyBase64, ciphertextHex) {
1493
1522
  if (typeof keyBase64 !== "string" || typeof ciphertextHex !== "string") return null;
1494
1523
  let key;
@@ -1515,34 +1544,163 @@ function decryptCopilotAuthDbToken(keyBase64, ciphertextHex) {
1515
1544
  }
1516
1545
  }
1517
1546
 
1518
- function readCopilotAuthDbToken({ home, platform = process.platform, securityRunner, sqliteReader } = {}) {
1519
- // The decryption key lives in the macOS Keychain. On Linux/Windows the
1520
- // copilot-language-server uses libsecret / Credential Manager instead, which we
1521
- // don't read yet — callers fall back to the plaintext path there.
1522
- if (platform !== "darwin") return null;
1523
- const resolvedHome = home || os.homedir();
1524
- const dbPath = path.join(resolvedHome, ".config", "github-copilot", "auth.db");
1525
- const reader = typeof sqliteReader === "function" ? sqliteReader : readSqliteJsonRows;
1526
- let rows;
1547
+ function isLikelyGithubOauthToken(token) {
1548
+ if (typeof token !== "string") return false;
1549
+ const trimmed = token.trim();
1550
+ return /^gh[opsur]_[A-Za-z0-9]{20,}$/.test(trimmed)
1551
+ || /^github_pat_[A-Za-z0-9_]{20,}$/.test(trimmed);
1552
+ }
1553
+
1554
+ function parseCopilotAuthDbSchemaVersion(value) {
1555
+ if (value === null || value === undefined || value === "") return null;
1556
+ const version = Number(value);
1557
+ return Number.isInteger(version) ? version : null;
1558
+ }
1559
+
1560
+ function decodeUtf8HexString(value) {
1561
+ if (typeof value !== "string" || !/^(?:[0-9a-f]{2})+$/i.test(value)) return null;
1527
1562
  try {
1528
- rows = reader(dbPath, COPILOT_AUTH_DB_SQL, { label: "GitHub Copilot" });
1563
+ return Buffer.from(value, "hex").toString("utf8").trim();
1529
1564
  } catch (_e) {
1530
1565
  return null;
1531
1566
  }
1567
+ }
1568
+
1569
+ function decodeCopilotSchemaV0Token(row) {
1570
+ const candidates = [];
1571
+ const tokenHex = typeof row?.token_hex === "string" ? row.token_hex : null;
1572
+ if (tokenHex) candidates.push(decodeUtf8HexString(tokenHex));
1573
+
1574
+ const rawCiphertext = row?.token_ciphertext;
1575
+ if (Buffer.isBuffer(rawCiphertext)) {
1576
+ candidates.push(rawCiphertext.toString("utf8").trim());
1577
+ } else if (typeof rawCiphertext === "string") {
1578
+ candidates.push(rawCiphertext.trim());
1579
+ candidates.push(decodeUtf8HexString(rawCiphertext));
1580
+ }
1581
+
1582
+ for (const candidate of candidates) {
1583
+ if (isLikelyGithubOauthToken(candidate)) return candidate.trim();
1584
+ }
1585
+ return null;
1586
+ }
1587
+
1588
+ function orderCopilotAuthDbRows(rows) {
1589
+ const githubRows = [];
1590
+ const fallbackRows = [];
1591
+ for (const row of rows) {
1592
+ const host = String(row?.auth_authority || "").split(":")[0];
1593
+ if (host === "github.com") {
1594
+ githubRows.push(row);
1595
+ } else {
1596
+ fallbackRows.push(row);
1597
+ }
1598
+ }
1599
+ return githubRows.concat(fallbackRows);
1600
+ }
1601
+
1602
+ function readCopilotAuthDbRows({ home, sqliteReader, asyncReader = false } = {}) {
1603
+ const resolvedHome = home || os.homedir();
1604
+ const dbPath = path.join(resolvedHome, ".config", "github-copilot", "auth.db");
1605
+ const reader = typeof sqliteReader === "function"
1606
+ ? sqliteReader
1607
+ : asyncReader
1608
+ ? readSqliteJsonRowsAsync
1609
+ : readSqliteJsonRows;
1610
+ return reader(dbPath, COPILOT_AUTH_DB_SQL, { label: "GitHub Copilot" });
1611
+ }
1612
+
1613
+ function resolveCopilotAuthDbTokenFromRows(rows, {
1614
+ platform = process.platform,
1615
+ securityRunner,
1616
+ keychainReader = readMacosKeychainGenericPassword,
1617
+ } = {}) {
1532
1618
  if (!Array.isArray(rows) || rows.length === 0) return null;
1533
1619
  // Prefer the public github.com host (mirrors the plaintext reader); rows are
1534
1620
  // already ordered most-recently-used first.
1535
- const row =
1536
- rows.find((r) => String(r?.auth_authority || "").split(":")[0] === "github.com") || rows[0];
1537
- const ciphertextHex = typeof row?.token_hex === "string" ? row.token_hex : null;
1538
- if (!ciphertextHex) return null;
1539
- const keyBase64 = readMacosKeychainGenericPassword({
1540
- service: COPILOT_LS_KEYCHAIN_SERVICE,
1541
- account: COPILOT_LS_KEYCHAIN_ACCOUNT,
1542
- securityRunner,
1543
- });
1544
- if (!keyBase64) return null;
1545
- return decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
1621
+ const orderedRows = orderCopilotAuthDbRows(rows);
1622
+ let keyBase64 = null;
1623
+ let attemptedKeychain = false;
1624
+ for (const row of orderedRows) {
1625
+ const schemaVersion = parseCopilotAuthDbSchemaVersion(row?.token_schema_version);
1626
+ if (schemaVersion === 0) {
1627
+ const plaintextToken = decodeCopilotSchemaV0Token(row);
1628
+ if (plaintextToken) return plaintextToken;
1629
+ continue;
1630
+ }
1631
+
1632
+ const ciphertextHex = typeof row?.token_hex === "string" ? row.token_hex : null;
1633
+ if (!ciphertextHex || platform !== "darwin") continue;
1634
+ // The decryption key lives in the macOS Keychain. On Linux/Windows the
1635
+ // copilot-language-server uses libsecret / Credential Manager instead, which
1636
+ // we don't read yet.
1637
+ if (!attemptedKeychain) {
1638
+ keyBase64 = keychainReader({
1639
+ service: COPILOT_LS_KEYCHAIN_SERVICE,
1640
+ account: COPILOT_LS_KEYCHAIN_ACCOUNT,
1641
+ securityRunner,
1642
+ });
1643
+ attemptedKeychain = true;
1644
+ }
1645
+ if (!keyBase64) continue;
1646
+ const token = decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
1647
+ if (token) return token;
1648
+ }
1649
+ return null;
1650
+ }
1651
+
1652
+ async function resolveCopilotAuthDbTokenFromRowsAsync(rows, {
1653
+ platform = process.platform,
1654
+ securityRunner,
1655
+ keychainReader = readMacosKeychainGenericPasswordAsync,
1656
+ } = {}) {
1657
+ if (!Array.isArray(rows) || rows.length === 0) return null;
1658
+ const orderedRows = orderCopilotAuthDbRows(rows);
1659
+ let keyBase64 = null;
1660
+ let attemptedKeychain = false;
1661
+ for (const row of orderedRows) {
1662
+ const schemaVersion = parseCopilotAuthDbSchemaVersion(row?.token_schema_version);
1663
+ if (schemaVersion === 0) {
1664
+ const plaintextToken = decodeCopilotSchemaV0Token(row);
1665
+ if (plaintextToken) return plaintextToken;
1666
+ continue;
1667
+ }
1668
+
1669
+ const ciphertextHex = typeof row?.token_hex === "string" ? row.token_hex : null;
1670
+ if (!ciphertextHex || platform !== "darwin") continue;
1671
+ if (!attemptedKeychain) {
1672
+ keyBase64 = await keychainReader({
1673
+ service: COPILOT_LS_KEYCHAIN_SERVICE,
1674
+ account: COPILOT_LS_KEYCHAIN_ACCOUNT,
1675
+ securityRunner,
1676
+ });
1677
+ attemptedKeychain = true;
1678
+ }
1679
+ if (!keyBase64) continue;
1680
+ const token = decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
1681
+ if (token) return token;
1682
+ }
1683
+ return null;
1684
+ }
1685
+
1686
+ function readCopilotAuthDbToken({ home, platform = process.platform, securityRunner, sqliteReader } = {}) {
1687
+ let rows;
1688
+ try {
1689
+ rows = readCopilotAuthDbRows({ home, sqliteReader });
1690
+ } catch (_e) {
1691
+ return null;
1692
+ }
1693
+ return resolveCopilotAuthDbTokenFromRows(rows, { platform, securityRunner });
1694
+ }
1695
+
1696
+ async function readCopilotAuthDbTokenAsync({ home, platform = process.platform, securityRunner, sqliteReader } = {}) {
1697
+ let rows;
1698
+ try {
1699
+ rows = await readCopilotAuthDbRows({ home, sqliteReader, asyncReader: true });
1700
+ } catch (_e) {
1701
+ return null;
1702
+ }
1703
+ return resolveCopilotAuthDbTokenFromRowsAsync(rows, { platform, securityRunner });
1546
1704
  }
1547
1705
 
1548
1706
  function readCopilotOauthToken({
@@ -1557,6 +1715,17 @@ function readCopilotOauthToken({
1557
1715
  return readCopilotAuthDbToken({ home, platform, securityRunner, sqliteReader });
1558
1716
  }
1559
1717
 
1718
+ async function readCopilotOauthTokenAsync({
1719
+ home = os.homedir(),
1720
+ platform = process.platform,
1721
+ securityRunner,
1722
+ sqliteReader,
1723
+ } = {}) {
1724
+ const plaintext = readPlaintextCopilotOauthToken({ home });
1725
+ if (plaintext) return plaintext;
1726
+ return readCopilotAuthDbTokenAsync({ home, platform, securityRunner, sqliteReader });
1727
+ }
1728
+
1560
1729
  function copilotRequestHeaders(token) {
1561
1730
  return {
1562
1731
  Authorization: `token ${token}`,
@@ -1629,7 +1798,7 @@ async function fetchCopilotLimits({
1629
1798
  sqliteReader,
1630
1799
  } = {}) {
1631
1800
  const otel = describeCopilotOtelStatus({ home, env });
1632
- const token = readCopilotOauthToken({
1801
+ const token = await readCopilotOauthTokenAsync({
1633
1802
  home: home || (env.HOME || os.homedir()),
1634
1803
  platform,
1635
1804
  securityRunner,
@@ -2866,7 +3035,9 @@ module.exports = {
2866
3035
  fetchAntigravityLimits,
2867
3036
  fetchCopilotLimits,
2868
3037
  readCopilotOauthToken,
3038
+ readCopilotOauthTokenAsync,
2869
3039
  readCopilotAuthDbToken,
3040
+ readCopilotAuthDbTokenAsync,
2870
3041
  decryptCopilotAuthDbToken,
2871
3042
  describeCopilotOtelStatus,
2872
3043
  fetchGrokLimits,