tokentracker-cli 0.51.1 → 0.52.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 (40) hide show
  1. package/dashboard/dist/assets/{ActivityHeatmap-CMsa_I8A.js → ActivityHeatmap-BcMV0FE7.js} +1 -1
  2. package/dashboard/dist/assets/{Card-Dfd--KbV.js → Card-Bvqqsc0B.js} +1 -1
  3. package/dashboard/dist/assets/{DashboardPage-DOQEtJti.js → DashboardPage-C_DG-DeO.js} +1 -1
  4. package/dashboard/dist/assets/{DevicePage-CzKxKcPm.js → DevicePage-Guc-8WrQ.js} +1 -1
  5. package/dashboard/dist/assets/{DialogTitle-Ca6U7uez.js → DialogTitle-BizNFJKQ.js} +1 -1
  6. package/dashboard/dist/assets/{FadeIn-Br0mORBd.js → FadeIn-BjQhH_SZ.js} +1 -1
  7. package/dashboard/dist/assets/{HeaderGithubStar-BK2k2YcJ.js → HeaderGithubStar-Da1SxEPv.js} +1 -1
  8. package/dashboard/dist/assets/{IpCheckPage-C_P3q6vb.js → IpCheckPage-DbHlMaVi.js} +1 -1
  9. package/dashboard/dist/assets/{LandingPage-CYmHfjgg.js → LandingPage-DLcmw2Bo.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardAvatar-DNw_wzr5.js → LeaderboardAvatar-B3oILNCt.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardPage-CDEltK0c.js → LeaderboardPage-XFQ18U3o.js} +3 -3
  12. package/dashboard/dist/assets/{LeaderboardProfileModal-DoM26CEG.js → LeaderboardProfileModal-Trw1F-KR.js} +1 -1
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-xNUzUA53.js → LeaderboardProfilePage-DIhhXAcj.js} +1 -1
  14. package/dashboard/dist/assets/{LimitsPage-B_lIlXxi.js → LimitsPage-DA2St0Za.js} +1 -1
  15. package/dashboard/dist/assets/{LocalOnlyNotice-D8RXOilZ.js → LocalOnlyNotice-Cox46M8O.js} +1 -1
  16. package/dashboard/dist/assets/{LoginPage-CGA2Q4Mv.js → LoginPage-Cg3ijs3T.js} +1 -1
  17. package/dashboard/dist/assets/{PopoverPopup-CIF-V90o.js → PopoverPopup-Aykc1gVH.js} +1 -1
  18. package/dashboard/dist/assets/{ResetPasswordPage-DySyCBKt.js → ResetPasswordPage-BdRGLedN.js} +1 -1
  19. package/dashboard/dist/assets/{Select-DC0ZMkgZ.js → Select-CTwQa6bR.js} +1 -1
  20. package/dashboard/dist/assets/{SelectItemText-3uL9SYV4.js → SelectItemText-DfU4rLgi.js} +1 -1
  21. package/dashboard/dist/assets/{SettingsPage-Bw3pl_ve.js → SettingsPage--BQU1GWp.js} +1 -1
  22. package/dashboard/dist/assets/{SkillsPage-Cl03YNHa.js → SkillsPage-WzSgjy__.js} +1 -1
  23. package/dashboard/dist/assets/{WidgetsPage-Dz7GR36L.js → WidgetsPage-E33nkum-.js} +1 -1
  24. package/dashboard/dist/assets/{WrappedPage-8VPwTH_x.js → WrappedPage-OUsoubCr.js} +1 -1
  25. package/dashboard/dist/assets/{agent-logos-Ob9TX2iG.js → agent-logos-Cy0GIVS4.js} +1 -1
  26. package/dashboard/dist/assets/{arrow-up-right-CiXaJqcJ.js → arrow-up-right-BaVF5IMd.js} +1 -1
  27. package/dashboard/dist/assets/{download-Uv52RLVu.js → download-sXs7O6am.js} +1 -1
  28. package/dashboard/dist/assets/{info-t1V6swsH.js → info-BsVVZxWM.js} +1 -1
  29. package/dashboard/dist/assets/{main-MuwgnCSV.js → main-C1RI7NJO.js} +2 -2
  30. package/dashboard/dist/assets/{use-limits-display-prefs-CwnyUqYj.js → use-limits-display-prefs-BWBwVtns.js} +1 -1
  31. package/dashboard/dist/assets/{use-native-settings-Dq_pV6Kb.js → use-native-settings-CPOvVTEr.js} +1 -1
  32. package/dashboard/dist/assets/{use-usage-limits-BmbBW9ph.js → use-usage-limits-DtDRZ3CQ.js} +1 -1
  33. package/dashboard/dist/assets/{useCurrency-DSokKrsK.js → useCurrency-CelyghEe.js} +1 -1
  34. package/dashboard/dist/assets/{useScrollLock-CktMaXWj.js → useScrollLock-BWkVvjN_.js} +1 -1
  35. package/dashboard/dist/index.html +1 -1
  36. package/dashboard/dist/share.html +1 -1
  37. package/package.json +1 -1
  38. package/src/lib/opencode-config.js +4 -1
  39. package/src/lib/pricing/seed-snapshot.json +1 -1
  40. package/src/lib/usage-limits.js +128 -7
@@ -1,4 +1,5 @@
1
1
  const cp = require("node:child_process");
2
+ const crypto = require("node:crypto");
2
3
  const fs = require("node:fs");
3
4
  const os = require("node:os");
4
5
  const path = require("node:path");
@@ -22,6 +23,7 @@ const {
22
23
  fetchCursorUsageSummary,
23
24
  } = require("./cursor-config");
24
25
  const { fetchGrokLimits } = require("./grok-limits");
26
+ const { readSqliteJsonRows } = require("./sqlite-reader");
25
27
 
26
28
  // 2-minute in-memory cache
27
29
  let cache = { data: null, fetchedAt: 0 };
@@ -1157,11 +1159,26 @@ function parseKiroUsageOutput(output, { now = new Date() } = {}) {
1157
1159
 
1158
1160
  // ─────────────────────────────────────────────────────────────────────────────
1159
1161
  // GitHub Copilot — `GET https://api.github.com/copilot_internal/user`
1160
- // Reuses the OAuth token from the user's existing Copilot install
1161
- // (`~/.config/github-copilot/{apps,hosts}.json`). No device flow needed.
1162
+ // Reuses the OAuth token from the user's existing Copilot install. Older clients
1163
+ // keep it in plaintext (`~/.config/github-copilot/{apps,hosts}.json`); recent
1164
+ // copilot-language-server builds (Zed, copilot.vim) migrate it into an encrypted
1165
+ // SQLite store (`auth.db`) and leave the plaintext files behind as stale legacy
1166
+ // copies. Read the plaintext first, then fall back to the encrypted store. No
1167
+ // device flow needed either way.
1162
1168
  // ─────────────────────────────────────────────────────────────────────────────
1163
1169
 
1164
- function readCopilotOauthToken({ home = require("node:os").homedir() } = {}) {
1170
+ const MACOS_SECURITY_BIN = "/usr/bin/security";
1171
+ // copilot-language-server stores the OAuth token as AES-256-GCM ciphertext in
1172
+ // auth.db (`oauth_tokens.token_ciphertext`) and the 32-byte key in the macOS
1173
+ // Keychain (service `copilot-language-server`, account `oauth-token-key`,
1174
+ // base64-encoded). Ciphertext layout: iv(12) ‖ ciphertext ‖ authTag(16).
1175
+ const COPILOT_LS_KEYCHAIN_SERVICE = "copilot-language-server";
1176
+ const COPILOT_LS_KEYCHAIN_ACCOUNT = "oauth-token-key";
1177
+ const COPILOT_AUTH_DB_SQL =
1178
+ "SELECT user_login, auth_authority, scopes, hex(token_ciphertext) AS token_hex, "
1179
+ + "last_used_at, updated_at FROM oauth_tokens ORDER BY last_used_at DESC, updated_at DESC";
1180
+
1181
+ function readPlaintextCopilotOauthToken({ home }) {
1165
1182
  const candidates = [
1166
1183
  path.join(home, ".config", "github-copilot", "apps.json"),
1167
1184
  path.join(home, ".config", "github-copilot", "hosts.json"),
@@ -1192,6 +1209,96 @@ function readCopilotOauthToken({ home = require("node:os").homedir() } = {}) {
1192
1209
  return fallback;
1193
1210
  }
1194
1211
 
1212
+ function readMacosKeychainGenericPassword({ service, account, securityRunner } = {}) {
1213
+ const runner = typeof securityRunner === "function" ? securityRunner : cp.spawnSync;
1214
+ if (runner === cp.spawnSync && !fs.existsSync(MACOS_SECURITY_BIN)) return null;
1215
+ const args = ["find-generic-password", "-s", service];
1216
+ if (account) args.push("-a", account);
1217
+ args.push("-w");
1218
+ const result = runner(MACOS_SECURITY_BIN, args, {
1219
+ stdio: ["ignore", "pipe", "ignore"],
1220
+ timeout: 2000,
1221
+ encoding: "utf8",
1222
+ });
1223
+ if (!result || result.error || result.status !== 0) return null;
1224
+ const stdout =
1225
+ typeof result.stdout === "string"
1226
+ ? result.stdout
1227
+ : Buffer.isBuffer(result.stdout)
1228
+ ? result.stdout.toString("utf8")
1229
+ : "";
1230
+ const trimmed = stdout.trim();
1231
+ return trimmed.length > 0 ? trimmed : null;
1232
+ }
1233
+
1234
+ function decryptCopilotAuthDbToken(keyBase64, ciphertextHex) {
1235
+ if (typeof keyBase64 !== "string" || typeof ciphertextHex !== "string") return null;
1236
+ let key;
1237
+ let blob;
1238
+ try {
1239
+ key = Buffer.from(keyBase64, "base64");
1240
+ blob = Buffer.from(ciphertextHex, "hex");
1241
+ } catch (_e) {
1242
+ return null;
1243
+ }
1244
+ if (key.length !== 32) return null; // AES-256 key
1245
+ if (blob.length < 12 + 16 + 1) return null; // iv(12) + authTag(16) + >=1 byte token
1246
+ const iv = blob.subarray(0, 12);
1247
+ const authTag = blob.subarray(blob.length - 16);
1248
+ const data = blob.subarray(12, blob.length - 16);
1249
+ try {
1250
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
1251
+ decipher.setAuthTag(authTag);
1252
+ const out = Buffer.concat([decipher.update(data), decipher.final()]);
1253
+ const token = out.toString("utf8").trim();
1254
+ return token.length > 0 ? token : null;
1255
+ } catch (_e) {
1256
+ return null;
1257
+ }
1258
+ }
1259
+
1260
+ function readCopilotAuthDbToken({ home, platform = process.platform, securityRunner, sqliteReader } = {}) {
1261
+ // The decryption key lives in the macOS Keychain. On Linux/Windows the
1262
+ // copilot-language-server uses libsecret / Credential Manager instead, which we
1263
+ // don't read yet — callers fall back to the plaintext path there.
1264
+ if (platform !== "darwin") return null;
1265
+ const resolvedHome = home || os.homedir();
1266
+ const dbPath = path.join(resolvedHome, ".config", "github-copilot", "auth.db");
1267
+ const reader = typeof sqliteReader === "function" ? sqliteReader : readSqliteJsonRows;
1268
+ let rows;
1269
+ try {
1270
+ rows = reader(dbPath, COPILOT_AUTH_DB_SQL, { label: "GitHub Copilot" });
1271
+ } catch (_e) {
1272
+ return null;
1273
+ }
1274
+ if (!Array.isArray(rows) || rows.length === 0) return null;
1275
+ // Prefer the public github.com host (mirrors the plaintext reader); rows are
1276
+ // already ordered most-recently-used first.
1277
+ const row =
1278
+ rows.find((r) => String(r?.auth_authority || "").split(":")[0] === "github.com") || rows[0];
1279
+ const ciphertextHex = typeof row?.token_hex === "string" ? row.token_hex : null;
1280
+ if (!ciphertextHex) return null;
1281
+ const keyBase64 = readMacosKeychainGenericPassword({
1282
+ service: COPILOT_LS_KEYCHAIN_SERVICE,
1283
+ account: COPILOT_LS_KEYCHAIN_ACCOUNT,
1284
+ securityRunner,
1285
+ });
1286
+ if (!keyBase64) return null;
1287
+ return decryptCopilotAuthDbToken(keyBase64, ciphertextHex);
1288
+ }
1289
+
1290
+ function readCopilotOauthToken({
1291
+ home = os.homedir(),
1292
+ platform = process.platform,
1293
+ securityRunner,
1294
+ sqliteReader,
1295
+ } = {}) {
1296
+ const plaintext = readPlaintextCopilotOauthToken({ home });
1297
+ if (plaintext) return plaintext;
1298
+ // No plaintext token found — recover the live one from the encrypted auth.db.
1299
+ return readCopilotAuthDbToken({ home, platform, securityRunner, sqliteReader });
1300
+ }
1301
+
1195
1302
  function copilotRequestHeaders(token) {
1196
1303
  return {
1197
1304
  Authorization: `token ${token}`,
@@ -1232,7 +1339,7 @@ function buildCopilotWindow(snapshot, resetIso) {
1232
1339
  }
1233
1340
 
1234
1341
  function describeCopilotOtelStatus({ home, env = process.env } = {}) {
1235
- const resolvedHome = home || env.HOME || require("node:os").homedir();
1342
+ const resolvedHome = home || env.HOME || os.homedir();
1236
1343
  const enabled = String(env.COPILOT_OTEL_ENABLED || "").toLowerCase() === "true";
1237
1344
  const exporterType = String(env.COPILOT_OTEL_EXPORTER_TYPE || "").toLowerCase();
1238
1345
  const explicitPath = typeof env.COPILOT_OTEL_FILE_EXPORTER_PATH === "string"
@@ -1255,9 +1362,21 @@ function describeCopilotOtelStatus({ home, env = process.env } = {}) {
1255
1362
  };
1256
1363
  }
1257
1364
 
1258
- async function fetchCopilotLimits({ home, env = process.env, fetchImpl = fetch } = {}) {
1365
+ async function fetchCopilotLimits({
1366
+ home,
1367
+ env = process.env,
1368
+ fetchImpl = fetch,
1369
+ platform = process.platform,
1370
+ securityRunner,
1371
+ sqliteReader,
1372
+ } = {}) {
1259
1373
  const otel = describeCopilotOtelStatus({ home, env });
1260
- const token = readCopilotOauthToken({ home: home || (env.HOME || require("node:os").homedir()) });
1374
+ const token = readCopilotOauthToken({
1375
+ home: home || (env.HOME || os.homedir()),
1376
+ platform,
1377
+ securityRunner,
1378
+ sqliteReader,
1379
+ });
1261
1380
  if (!token) return { configured: false, ...otel };
1262
1381
 
1263
1382
  try {
@@ -2072,7 +2191,7 @@ async function fetchUsageLimitsUncached({
2072
2191
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2073
2192
  fetchKiroLimits({ commandRunner, now }),
2074
2193
  fetchAntigravityLimits({ home, commandRunner, requestFn, nowMs }),
2075
- withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch }), "GitHub Copilot", providerTimeoutMs)
2194
+ withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
2076
2195
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2077
2196
  withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch }), "Grok Build", providerTimeoutMs)
2078
2197
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
@@ -2183,6 +2302,8 @@ module.exports = {
2183
2302
  fetchAntigravityLimits,
2184
2303
  fetchCopilotLimits,
2185
2304
  readCopilotOauthToken,
2305
+ readCopilotAuthDbToken,
2306
+ decryptCopilotAuthDbToken,
2186
2307
  describeCopilotOtelStatus,
2187
2308
  fetchGrokLimits,
2188
2309
  };