tokentracker-cli 0.96.2 → 0.96.4
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/README.de.md +34 -25
- package/README.ja.md +38 -27
- package/README.ko.md +39 -28
- package/README.md +36 -28
- package/README.zh-CN.md +36 -25
- package/dashboard/dist/assets/{AchievementBadge-CgEitoX1.js → AchievementBadge-CCcvmz6R.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-dnnea0JH.js → AchievementsPage-C5jdktOK.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CJqG0UP7.js → AchievementsSection-BFMtUzn1.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BLV6SJoI.js → BadgeDetailModal-Be8gM7tM.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-Cm78yEbz.js → BotAnimated-CXTGR71z.js} +1 -1
- package/dashboard/dist/assets/{Card-IPGSYT6O.js → Card-CsUsp3CV.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-04w1U1sm.js → ClawdAnimated-BEjpZjEE.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-gSSa1V_s.js → CommandPalette-CHc_Z1r8.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CICwUz14.js → CommunityStatsModal-Gs6kZ_Mp.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DaWC0cY7.js → ConfirmModal-niHkpv2O.js} +1 -1
- package/dashboard/dist/assets/{Controls-BDihA03F.js → Controls-X2Z1UNUx.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-B66s3Exi.js +1 -0
- package/dashboard/dist/assets/{DevicePage-BPpo_eiY.js → DevicePage-DQdLdEI8.js} +1 -1
- package/dashboard/dist/assets/DialogClose-CCUjTxl8.js +1 -0
- package/dashboard/dist/assets/DialogDescription-CjMjYG3N.js +1 -0
- package/dashboard/dist/assets/DialogTitle-CgkUGAag.js +1 -0
- package/dashboard/dist/assets/{FadeIn-DmI9PRLa.js → FadeIn-D4QD1bPo.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CBO4cY7i.js → HeaderGithubStar-D0my-Z3a.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CPFGhi9K.js → HoverTooltip-nSWgrKlK.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Do7w_6z6.js → IpCheckPage-4hfqHAgi.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-at_Ti1ZE.js → LandingPage-wIc4tVRI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BH1cnKPp.js → LeaderboardAvatar-DGlN8Vy8.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Cd30aXwL.js → LeaderboardPage-CybuzE87.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CVsti_HN.js → LeaderboardProfileModal-D9la3tNd.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-BfSpDM9e.js → LeaderboardProfilePage-gxXSeJYb.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-CZH3G54Y.js +8 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-9-vRgsdn.js → LocalOnlyNotice-Ckc7zohC.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DM_ZdFNl.js → LoginCard-BqeRHqWK.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BaNMznwh.js → LoginModal-BjH6aIBG.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BMMthUf4.js → LoginPage-BOyPLNU3.js} +1 -1
- package/dashboard/dist/assets/PetPage-BXRXG40D.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-Dad1ZYVd.js +1 -0
- package/dashboard/dist/assets/ProviderIcon-BDmrNJSl.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-Ca4SULSV.js → ResetPasswordPage-CZpE6qfa.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-Bbsn-qvZ.js → SegmentedControl-CE1hrQFi.js} +1 -1
- package/dashboard/dist/assets/Select-YVj7u6Nv.js +1 -0
- package/dashboard/dist/assets/SelectItemText-BMYGgQ3o.js +1 -0
- package/dashboard/dist/assets/{ServiceStatusPage-BPS8KJ3a.js → ServiceStatusPage-HehjDbZg.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-C1yt3TMZ.js → SessionsPage-Chvu4YqZ.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-BXjS7u9_.js +2 -0
- package/dashboard/dist/assets/SkillsPage-q3Rjlj9h.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-DKvbJbnv.js → TokenGalaxy-BqZyj82r.js} +1 -1
- package/dashboard/dist/assets/ToolbarRootContext-Ch8ueI_C.js +1 -0
- package/dashboard/dist/assets/{TrendMonitor-YF5C_-L1.js → TrendMonitor-C2aTL9p1.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BKV3DgXg.js → WidgetsPage-DYaIai8j.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-s4IRoLf7.js → WrappedPage-51B0v-Sr.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CvMMIjhi.css → __vite-browser-external-5P7me0Qq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Cp7dNuCm.js +45 -0
- package/dashboard/dist/assets/{agent-logos-Hbt-wyl2.js → agent-logos-DXrPSAdX.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-KeIpKlw3.js → arrow-up-right-BFkf7l0U.js} +1 -1
- package/dashboard/dist/assets/{check-_QKyY1v_.js → check--N8wmgIJ.js} +1 -1
- package/dashboard/dist/assets/composite-zF5Pj94e.js +1 -0
- package/dashboard/dist/assets/{copy-DpLM3QMD.js → copy-BG0JMr6A.js} +1 -1
- package/dashboard/dist/assets/{download-C4NX-JnW.js → download-CPFJf-Sf.js} +1 -1
- package/dashboard/dist/assets/{external-link-CG-7FcOF.js → external-link-D2yF0kgP.js} +1 -1
- package/dashboard/dist/assets/{flame-CkWid7Ji.js → flame-l460AFx9.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CbwWhioZ.js → format-tokens-DKO4ljuH.js} +1 -1
- package/dashboard/dist/assets/{icons-BZq17rU3.js → icons-RoxRGkz4.js} +1 -1
- package/dashboard/dist/assets/{index-CR9tDiiC.js → index--ouT5L4N.js} +1 -1
- package/dashboard/dist/assets/{index-DDGLE0dz.js → index-DhyLCvvv.js} +1 -1
- package/dashboard/dist/assets/{index-CSwEiUn4.js → index-PTJ4368-.js} +1 -1
- package/dashboard/dist/assets/{info-mkeRjzqc.js → info-DNWMO4Ca.js} +1 -1
- package/dashboard/dist/assets/{link-2-BjMw8csC.js → link-2-DzWXAz1h.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-cGWOJnWn.js → loader-circle-BzGMqXNu.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Cg_S73tD.js → maximize-2-Dkpyk4x3.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DNxTWxId.js → modifiers.esm-lggHUQpW.js} +1 -1
- package/dashboard/dist/assets/module-DUSXqJtA.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-BvE9Tt2U.js → monitor-smartphone-Ck0-gz9l.js} +1 -1
- package/dashboard/dist/assets/{plus-BC2wdPRr.js → plus-qTJpnn43.js} +1 -1
- package/dashboard/dist/assets/provider-display-psY26qmp.js +42 -0
- package/dashboard/dist/assets/{react-D0kcslTz.js → react-CYS24KmX.js} +1 -1
- package/dashboard/dist/assets/{search-BuBNF3E3.js → search-CUzE6zxr.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DxAN5pNx.js → skills-api-D9SzxPFG.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DoqMcEX-.js → sortable.esm-Bl2hAUyR.js} +1 -1
- package/dashboard/dist/assets/store-mHSJAJtE.js +1 -0
- package/dashboard/dist/assets/{terminal-COWFUJdU.js → terminal-BlQNEvn1.js} +1 -1
- package/dashboard/dist/assets/{trash-2-Cqjki3RI.js → trash-CHdgmOyk.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DsrlOPDM.js → use-community-stats-DUiCs91N.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-ryRFL2gx.js → use-latest-request-guard-ehXfkvuU.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-CcwwQ0Fq.js +1 -0
- package/dashboard/dist/assets/{use-ordered-list-D-Q86R1b.js → use-ordered-list-CCOtI3Wa.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CDlahPqe.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-CrNz45y7.js → use-session-efficiency-pref-CYRDpcc_.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CoGM21px.js → use-transform-btLlTJpj.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-BGr5sR-c.js +1 -0
- package/dashboard/dist/assets/{useCurrency-CgCfDktb.js → useCurrency-CNutuEKd.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-QKtzCKTL.js → useTokenFormat-C0bAJFMC.js} +1 -1
- package/dashboard/dist/assets/{zap-CTIobzgU.js → zap-B2fbyjbc.js} +1 -1
- package/dashboard/dist/brand-logos/acode-dark.png +0 -0
- package/dashboard/dist/brand-logos/acode.png +0 -0
- package/dashboard/dist/brand-logos/devin.svg +6 -0
- package/dashboard/dist/index.html +20 -16
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +114 -4
- package/src/commands/status.js +37 -0
- package/src/commands/sync.js +92 -10
- package/src/commands/uninstall.js +23 -0
- package/src/lib/codex-config.js +42 -0
- package/src/lib/devin-limits.js +249 -0
- package/src/lib/devin-usage.js +212 -0
- package/src/lib/local-api.js +19 -0
- package/src/lib/passive-mode.js +13 -1
- package/src/lib/pricing/curated-overrides.json +67 -1
- package/src/lib/pricing/index.js +5 -2
- package/src/lib/pricing/matcher.js +28 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1310 -144
- package/src/lib/skills-manager.js +231 -30
- package/src/lib/subscriptions.js +70 -16
- package/src/lib/usage-limits.js +199 -49
- package/dashboard/dist/assets/DashboardPage-hV7wncKF.js +0 -1
- package/dashboard/dist/assets/DialogClose-C_dF-r3s.js +0 -1
- package/dashboard/dist/assets/DialogDescription-GClOSCaE.js +0 -1
- package/dashboard/dist/assets/DialogTitle-Buh_5688.js +0 -1
- package/dashboard/dist/assets/LimitsPage-_-BadDAj.js +0 -8
- package/dashboard/dist/assets/PetPage-Iq8Hswai.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-DXr3JxVV.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-sb2keSY2.js +0 -1
- package/dashboard/dist/assets/Select-vD-yj-qa.js +0 -1
- package/dashboard/dist/assets/SelectItemText-CRtFEuvg.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CWVHrMer.js +0 -2
- package/dashboard/dist/assets/SkillsPage-DGWEH1xm.js +0 -1
- package/dashboard/dist/assets/ToolbarRootContext-D8yov1dW.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-_LUatzwU.js +0 -45
- package/dashboard/dist/assets/composite-BidfcMeb.js +0 -1
- package/dashboard/dist/assets/limits-providers-DRkcWQup.js +0 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +0 -3
- package/dashboard/dist/assets/provider-display-B7GggHQy.js +0 -42
- package/dashboard/dist/assets/store-Cg1HeUFM.js +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-DkSuu12m.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-D7NNqavo.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-C_6L1sO5.js +0 -1
|
@@ -15,6 +15,7 @@ const DEFAULT_REPOS = [
|
|
|
15
15
|
const TARGETS = {
|
|
16
16
|
claude: { id: "claude", label: "Claude", dir: () => path.join(os.homedir(), ".claude", "skills") },
|
|
17
17
|
codex: { id: "codex", label: "Codex", dir: () => path.join(resolveCodexHome(), "skills") },
|
|
18
|
+
acode: { id: "acode", label: "AStudio", dir: () => path.join(resolveAcodeHome(), "skills") },
|
|
18
19
|
grok: { id: "grok", label: "Grok", dir: () => path.join(resolveGrokHome(process.env), "skills") },
|
|
19
20
|
antigravity: { id: "antigravity", label: "Antigravity", dirs: () => resolveAntigravitySkillDirs(process.env) },
|
|
20
21
|
gemini: { id: "gemini", label: "Gemini", dir: () => path.join(os.homedir(), ".gemini", "skills") },
|
|
@@ -32,6 +33,10 @@ function resolveCodexHome() {
|
|
|
32
33
|
return String(process.env.CODEX_HOME || "").trim() || path.join(os.homedir(), ".codex");
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function resolveAcodeHome() {
|
|
37
|
+
return String(process.env.TOKENTRACKER_ACODE_HOME || "").trim() || path.join(os.homedir(), ".acode");
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
function resolveZcodeHome() {
|
|
36
41
|
return String(process.env.ZCODE_HOME || "").trim() || path.join(os.homedir(), ".zcode");
|
|
37
42
|
}
|
|
@@ -904,7 +909,12 @@ function listInstalledSkills() {
|
|
|
904
909
|
});
|
|
905
910
|
}
|
|
906
911
|
|
|
907
|
-
async function installSkill(
|
|
912
|
+
async function installSkill(
|
|
913
|
+
skillInput,
|
|
914
|
+
targetIds = ["claude", "codex"],
|
|
915
|
+
prefetchedTree = null,
|
|
916
|
+
{ expectExistingId = null } = {},
|
|
917
|
+
) {
|
|
908
918
|
const skill = {
|
|
909
919
|
key: String(skillInput?.key || ""),
|
|
910
920
|
name: String(skillInput?.name || ""),
|
|
@@ -934,7 +944,9 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
|
|
|
934
944
|
);
|
|
935
945
|
}
|
|
936
946
|
|
|
937
|
-
|
|
947
|
+
// Bulk update hands in the repo tree it already fetched, so N skills from one
|
|
948
|
+
// repo cost one GitHub call instead of N.
|
|
949
|
+
const { branch, tree } = prefetchedTree || await getRepoTree({
|
|
938
950
|
owner: skill.repoOwner,
|
|
939
951
|
name: skill.repoName,
|
|
940
952
|
branch: skill.repoBranch,
|
|
@@ -945,9 +957,10 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
|
|
|
945
957
|
if (!files.some((entry) => /(^|\/)SKILL\.md$/i.test(entry.path))) throw new Error("SKILL.md not found in selected directory");
|
|
946
958
|
|
|
947
959
|
const dest = managedSkillPath(installName);
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
ensureDir(
|
|
960
|
+
let selectedTargets = targetIds.filter((id) => TARGETS[id]);
|
|
961
|
+
const tempRoot = path.join(dataDir(), "tmp");
|
|
962
|
+
ensureDir(tempRoot);
|
|
963
|
+
const temp = fs.mkdtempSync(path.join(tempRoot, `${installName}-`));
|
|
951
964
|
try {
|
|
952
965
|
for (const entry of files) {
|
|
953
966
|
const relative = entry.path === sourceDir ? path.basename(entry.path) : entry.path.slice(sourceDir.length + 1);
|
|
@@ -957,6 +970,17 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
|
|
|
957
970
|
ensureDir(path.dirname(out));
|
|
958
971
|
fs.writeFileSync(out, await fetchText(githubRawUrl(skill.repoOwner, skill.repoName, branch, entry.path)));
|
|
959
972
|
}
|
|
973
|
+
// An update must not recreate a skill that was uninstalled while we were
|
|
974
|
+
// downloading, nor overwrite whatever took its install directory in the
|
|
975
|
+
// meantime. Checked here because everything from this point to
|
|
976
|
+
// saveRegistry() is synchronous — no await can interleave a change we would
|
|
977
|
+
// then clobber.
|
|
978
|
+
if (expectExistingId) {
|
|
979
|
+
const current = readRegistry().skills.find((entry) => !entry.trashedAt && entry.id === expectExistingId);
|
|
980
|
+
if (!current) throw new Error("Skill is no longer installed");
|
|
981
|
+
// Another page or API caller may have changed target intent during a download.
|
|
982
|
+
selectedTargets = (current.targets || []).filter((id) => TARGETS[id]);
|
|
983
|
+
}
|
|
960
984
|
removePath(dest);
|
|
961
985
|
ensureDir(path.dirname(dest));
|
|
962
986
|
fs.renameSync(temp, dest);
|
|
@@ -968,7 +992,6 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
|
|
|
968
992
|
const skillMarker = findSkillMarker(dest);
|
|
969
993
|
const skillMd = skillMarker ? fs.readFileSync(skillMarker, "utf8") : "";
|
|
970
994
|
const metadata = readSkillMetadata(skillMd, skill.name || installName);
|
|
971
|
-
const selectedTargets = targetIds.filter((id) => TARGETS[id]);
|
|
972
995
|
const installed = {
|
|
973
996
|
id: `${skill.repoOwner}/${skill.repoName}:${sourceDir}`,
|
|
974
997
|
key: `${skill.repoOwner}/${skill.repoName}:${sourceDir}`,
|
|
@@ -987,9 +1010,13 @@ async function installSkill(skillInput, targetIds = ["claude", "codex"]) {
|
|
|
987
1010
|
targets: selectedTargets,
|
|
988
1011
|
};
|
|
989
1012
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1013
|
+
// Re-read: the downloads above can take minutes and the registry is shared
|
|
1014
|
+
// with every other mutation path, so the copy fetched before them is stale by
|
|
1015
|
+
// now. Reading here keeps the read/modify/write window down to this block.
|
|
1016
|
+
const latest = readRegistry();
|
|
1017
|
+
latest.skills = latest.skills.filter((entry) => entry.id !== installed.id && entry.directory.toLowerCase() !== installName.toLowerCase());
|
|
1018
|
+
latest.skills.push(installed);
|
|
1019
|
+
saveRegistry(latest);
|
|
993
1020
|
|
|
994
1021
|
for (const id of selectedTargets) syncSkillToTarget(installName, id);
|
|
995
1022
|
appendActivity({ action: "install", name: installed.name, directory: installName, targets: selectedTargets, source: `${skill.repoOwner}/${skill.repoName}` });
|
|
@@ -1239,20 +1266,89 @@ function updateCachePath() {
|
|
|
1239
1266
|
return path.join(dataDir(), "updates-cache.json");
|
|
1240
1267
|
}
|
|
1241
1268
|
|
|
1269
|
+
// checkUpdates() and updateSkills() have to agree on three things: which skills
|
|
1270
|
+
// are candidates, how a cached verdict is keyed, and which tree a skill's
|
|
1271
|
+
// signature comes from. They share these so the badge and the update can never
|
|
1272
|
+
// disagree about the same skill.
|
|
1273
|
+
function managedUpdateCandidates(registry) {
|
|
1274
|
+
return registry.skills.filter(
|
|
1275
|
+
(skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && skill.sourceSignature,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function updateFingerprint(managed) {
|
|
1280
|
+
return managed
|
|
1281
|
+
.map((skill) => `${skill.id}@${skill.sourceSignature}`)
|
|
1282
|
+
.sort()
|
|
1283
|
+
.join("|");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Write the verdicts a round derived, carrying forward the previous map for
|
|
1287
|
+
// anything it could not derive — a repo whose tree failed, everything past a
|
|
1288
|
+
// rate-limited stop. "We could not check" must not render as "nothing is
|
|
1289
|
+
// stale": that clears every badge and with it the way to retry. Returns the
|
|
1290
|
+
// merged map. Entries for skills that are no longer installed are dropped.
|
|
1291
|
+
//
|
|
1292
|
+
// `full` says the round attempted the whole library, which only checkUpdates()
|
|
1293
|
+
// does. An update attempts the ids it was handed, so it may only publish an
|
|
1294
|
+
// entry when the carried map fills in the rest — see the guard below.
|
|
1295
|
+
function writeUpdateCache(verdicts, { full = false } = {}) {
|
|
1296
|
+
const managed = managedUpdateCandidates(readRegistry());
|
|
1297
|
+
const live = new Set(managed.map((skill) => skill.id));
|
|
1298
|
+
const cached = readJson(updateCachePath(), null);
|
|
1299
|
+
const carriedFresh = Boolean(
|
|
1300
|
+
cached &&
|
|
1301
|
+
cached.updates &&
|
|
1302
|
+
typeof cached.updates === "object" &&
|
|
1303
|
+
Number.isFinite(cached.checkedAt) &&
|
|
1304
|
+
Date.now() - cached.checkedAt < UPDATE_CACHE_TTL_MS,
|
|
1305
|
+
);
|
|
1306
|
+
const updates = {};
|
|
1307
|
+
if (carriedFresh) {
|
|
1308
|
+
for (const [id, stale] of Object.entries(cached.updates)) if (live.has(id)) updates[id] = Boolean(stale);
|
|
1309
|
+
}
|
|
1310
|
+
for (const [id, stale] of Object.entries(verdicts)) if (live.has(id)) updates[id] = Boolean(stale);
|
|
1311
|
+
|
|
1312
|
+
// The entry is read back as a verdict for the entire library, and a missing
|
|
1313
|
+
// key reads as "not stale". A partial round that stamped the library
|
|
1314
|
+
// fingerprint would therefore publish silence about skills nobody looked at,
|
|
1315
|
+
// and the next check would hit it. Leave the cache alone instead and let that
|
|
1316
|
+
// check do the work.
|
|
1317
|
+
if (!full && !managed.every((skill) => Object.prototype.hasOwnProperty.call(updates, skill.id))) {
|
|
1318
|
+
return updates;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
writeJson(updateCachePath(), {
|
|
1322
|
+
fingerprint: updateFingerprint(managed),
|
|
1323
|
+
// A prime inherits the window of the check it built on: it re-derived some
|
|
1324
|
+
// ids, not the library, so it must not restart the hour for the rest.
|
|
1325
|
+
checkedAt: full || !carriedFresh ? Date.now() : cached.checkedAt,
|
|
1326
|
+
updates,
|
|
1327
|
+
});
|
|
1328
|
+
return updates;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function groupSkillsByRepo(skills) {
|
|
1332
|
+
const byRepo = new Map();
|
|
1333
|
+
for (const skill of skills) {
|
|
1334
|
+
const branch = skill.repoBranch || "main";
|
|
1335
|
+
const key = `${skill.repoOwner}/${skill.repoName}@${branch}`.toLowerCase();
|
|
1336
|
+
if (!byRepo.has(key)) {
|
|
1337
|
+
byRepo.set(key, { owner: skill.repoOwner, name: skill.repoName, branch, skills: [] });
|
|
1338
|
+
}
|
|
1339
|
+
byRepo.get(key).skills.push(skill);
|
|
1340
|
+
}
|
|
1341
|
+
return Array.from(byRepo.values());
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1242
1344
|
// Compare each managed GitHub/skills.sh skill's stored source signature against a
|
|
1243
1345
|
// freshly fetched repo tree. One tree call per repo (skills from the same repo
|
|
1244
1346
|
// share it), concurrency-limited and cached for an hour so a background check
|
|
1245
1347
|
// can't trip GitHub's unauthenticated rate limit. Returns { updates: {id:bool} }.
|
|
1246
1348
|
// Read-only: never mutates the registry or the on-disk skills.
|
|
1247
1349
|
async function checkUpdates({ force = false } = {}) {
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
(skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && skill.sourceSignature,
|
|
1251
|
-
);
|
|
1252
|
-
const fingerprint = managed
|
|
1253
|
-
.map((skill) => `${skill.id}@${skill.sourceSignature}`)
|
|
1254
|
-
.sort()
|
|
1255
|
-
.join("|");
|
|
1350
|
+
const managed = managedUpdateCandidates(readRegistry());
|
|
1351
|
+
const fingerprint = updateFingerprint(managed);
|
|
1256
1352
|
|
|
1257
1353
|
if (!force) {
|
|
1258
1354
|
const cached = readJson(updateCachePath(), null);
|
|
@@ -1268,18 +1364,8 @@ async function checkUpdates({ force = false } = {}) {
|
|
|
1268
1364
|
}
|
|
1269
1365
|
}
|
|
1270
1366
|
|
|
1271
|
-
const byRepo = new Map();
|
|
1272
|
-
for (const skill of managed) {
|
|
1273
|
-
const branch = skill.repoBranch || "main";
|
|
1274
|
-
const key = `${skill.repoOwner}/${skill.repoName}@${branch}`.toLowerCase();
|
|
1275
|
-
if (!byRepo.has(key)) {
|
|
1276
|
-
byRepo.set(key, { owner: skill.repoOwner, name: skill.repoName, branch, skills: [] });
|
|
1277
|
-
}
|
|
1278
|
-
byRepo.get(key).skills.push(skill);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1367
|
const updates = {};
|
|
1282
|
-
await mapWithConcurrency(
|
|
1368
|
+
await mapWithConcurrency(groupSkillsByRepo(managed), UPDATE_CHECK_CONCURRENCY, async (repo) => {
|
|
1283
1369
|
let tree;
|
|
1284
1370
|
try {
|
|
1285
1371
|
({ tree } = await getRepoTree(repo));
|
|
@@ -1294,8 +1380,122 @@ async function checkUpdates({ force = false } = {}) {
|
|
|
1294
1380
|
});
|
|
1295
1381
|
|
|
1296
1382
|
const checkedAt = Date.now();
|
|
1297
|
-
|
|
1298
|
-
|
|
1383
|
+
return { updates: writeUpdateCache(updates, { full: true }), checkedAt, cached: false };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Re-install managed skills from upstream, grouped by repo like checkUpdates()
|
|
1387
|
+
// so N skills from one repo cost one tree call. Sequential by necessity:
|
|
1388
|
+
// installSkill() read/modify/writes the registry, so parallel installs would
|
|
1389
|
+
// drop each other's entries. Every requested id gets a result row — including
|
|
1390
|
+
// ones that are no longer installed — so updated + skipped + failed always
|
|
1391
|
+
// reconciles with what the caller asked for.
|
|
1392
|
+
async function updateSkills(ids = []) {
|
|
1393
|
+
// An empty or malformed list means "nothing was asked for", never "every
|
|
1394
|
+
// managed skill" — the permissive reading turned one bad request field into a
|
|
1395
|
+
// full re-install of the library.
|
|
1396
|
+
const wanted = new Set(
|
|
1397
|
+
(Array.isArray(ids) ? ids : []).map((id) => String(id || "")).filter(Boolean),
|
|
1398
|
+
);
|
|
1399
|
+
if (!wanted.size) return { results: [], updated: 0, skipped: 0, failed: 0, rateLimited: null };
|
|
1400
|
+
const managed = readRegistry().skills.filter(
|
|
1401
|
+
(skill) => !skill.trashedAt && skill.repoOwner && skill.repoName && wanted.has(skill.id),
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
const results = [];
|
|
1405
|
+
const verdicts = {};
|
|
1406
|
+
let rateLimited = null;
|
|
1407
|
+
|
|
1408
|
+
const known = new Set(managed.map((skill) => skill.id));
|
|
1409
|
+
for (const id of wanted) {
|
|
1410
|
+
if (!known.has(id)) results.push({ id, name: null, ok: false, error: "Skill is no longer installed" });
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
for (const repo of groupSkillsByRepo(managed)) {
|
|
1414
|
+
let branch;
|
|
1415
|
+
let tree;
|
|
1416
|
+
try {
|
|
1417
|
+
({ branch, tree } = await getRepoTree(repo));
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
if (error instanceof RateLimitError) {
|
|
1420
|
+
rateLimited = error;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
for (const skill of repo.skills) {
|
|
1424
|
+
results.push({ id: skill.id, name: skill.name, ok: false, error: error?.message || "Unable to read repository" });
|
|
1425
|
+
}
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
for (const skill of repo.skills) {
|
|
1430
|
+
const sourceDir = skill.sourceDirectory || skill.directory;
|
|
1431
|
+
const fresh = sourceSignatureFromTree(tree, sourceDir);
|
|
1432
|
+
if (skill.sourceSignature && fresh === skill.sourceSignature) {
|
|
1433
|
+
results.push({ id: skill.id, name: skill.name, ok: true, skipped: true });
|
|
1434
|
+
verdicts[skill.id] = false;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
// This loop runs for as long as the downloads take, and the registry is
|
|
1438
|
+
// shared with every other mutation path — re-check rather than write back
|
|
1439
|
+
// a skill that was uninstalled while we were working.
|
|
1440
|
+
if (!readRegistry().skills.some((entry) => !entry.trashedAt && entry.id === skill.id)) {
|
|
1441
|
+
results.push({ id: skill.id, name: skill.name, ok: false, error: "Skill is no longer installed" });
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
await installSkill(
|
|
1446
|
+
{
|
|
1447
|
+
key: skill.key,
|
|
1448
|
+
name: skill.name,
|
|
1449
|
+
description: skill.description,
|
|
1450
|
+
// installSkill() wants the repo path, not the flat install name.
|
|
1451
|
+
directory: sourceDir,
|
|
1452
|
+
readmeUrl: skill.readmeUrl,
|
|
1453
|
+
repoOwner: skill.repoOwner,
|
|
1454
|
+
repoName: skill.repoName,
|
|
1455
|
+
repoBranch: branch,
|
|
1456
|
+
},
|
|
1457
|
+
skill.targets || [],
|
|
1458
|
+
{ branch, tree },
|
|
1459
|
+
{ expectExistingId: skill.id },
|
|
1460
|
+
);
|
|
1461
|
+
results.push({ id: skill.id, name: skill.name, ok: true, skipped: false });
|
|
1462
|
+
if (fresh) verdicts[skill.id] = false;
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
if (error instanceof RateLimitError) {
|
|
1465
|
+
rateLimited = error;
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
results.push({ id: skill.id, name: skill.name, ok: false, error: error?.message || "Update failed" });
|
|
1469
|
+
if (fresh) verdicts[skill.id] = true;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (rateLimited) break;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// A rate-limited stop breaks out of both loops, leaving the skill it happened
|
|
1476
|
+
// on and everything after it unreported. Backfill so every requested id comes
|
|
1477
|
+
// back with a row and the counts still reconcile with what was asked for.
|
|
1478
|
+
if (rateLimited) {
|
|
1479
|
+
const reported = new Set(results.map((row) => row.id));
|
|
1480
|
+
const nameById = new Map(managed.map((skill) => [skill.id, skill.name]));
|
|
1481
|
+
for (const id of wanted) {
|
|
1482
|
+
if (reported.has(id)) continue;
|
|
1483
|
+
results.push({ id, name: nameById.get(id) || null, ok: false, error: rateLimited.message });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Prime the cache from the trees this run already holds, so the refresh that
|
|
1488
|
+
// follows is a cache hit rather than a second tree call per repo. Skipped when
|
|
1489
|
+
// the run derived nothing — there is no fresh verdict to record.
|
|
1490
|
+
if (Object.keys(verdicts).length) writeUpdateCache(verdicts);
|
|
1491
|
+
|
|
1492
|
+
return {
|
|
1493
|
+
results,
|
|
1494
|
+
updated: results.filter((entry) => entry.ok && !entry.skipped).length,
|
|
1495
|
+
skipped: results.filter((entry) => entry.skipped).length,
|
|
1496
|
+
failed: results.filter((entry) => !entry.ok).length,
|
|
1497
|
+
rateLimited: rateLimited ? rateLimited.message : null,
|
|
1498
|
+
};
|
|
1299
1499
|
}
|
|
1300
1500
|
|
|
1301
1501
|
// skills.sh exposes only /api/search (no leaderboard endpoint), so "Popular" is
|
|
@@ -1380,4 +1580,5 @@ module.exports = {
|
|
|
1380
1580
|
sourceSignatureFromTree,
|
|
1381
1581
|
targetList,
|
|
1382
1582
|
uninstallSkill,
|
|
1583
|
+
updateSkills,
|
|
1383
1584
|
};
|
package/src/lib/subscriptions.js
CHANGED
|
@@ -19,6 +19,9 @@ const CLAUDE_CODE_CREDENTIALS_FILE = ".credentials.json";
|
|
|
19
19
|
// Platforms where Claude Code stores credentials in the plain JSON file above
|
|
20
20
|
// rather than the macOS Keychain.
|
|
21
21
|
const CLAUDE_CODE_CREDENTIALS_FILE_PLATFORMS = new Set(["linux", "win32"]);
|
|
22
|
+
// Refresh slightly before wall-clock expiry so Limits does not spend a request
|
|
23
|
+
// that Anthropic answers with 429 for a real-but-expired Claude Code token.
|
|
24
|
+
const CLAUDE_TOKEN_EXPIRY_SKEW_MS = 60_000;
|
|
22
25
|
|
|
23
26
|
function usesClaudeCodeCredentialsFile(platform) {
|
|
24
27
|
return CLAUDE_CODE_CREDENTIALS_FILE_PLATFORMS.has(platform);
|
|
@@ -164,14 +167,23 @@ async function detectOpencodeChatgptSubscription({ home, env }) {
|
|
|
164
167
|
};
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
// Match Claude Code 2.1.267's account derivation. A service-only lookup may
|
|
171
|
+
// select credentials from an older installation before the current account.
|
|
172
|
+
function resolveClaudeKeychainAccount({ env = process.env, userInfo = os.userInfo } = {}) {
|
|
173
|
+
let account;
|
|
174
|
+
try { account = env.USER || userInfo().username; } catch (_error) { return "claude-code-user"; }
|
|
175
|
+
return typeof account === "string" && /^[a-zA-Z0-9._-]+$/.test(account)
|
|
176
|
+
? account : "claude-code-user";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function probeMacosKeychainGenericPassword({ service, securityRunner, timeoutMs, env } = {}) {
|
|
168
180
|
const svc = normalizeString(service);
|
|
169
181
|
if (!svc) return false;
|
|
170
182
|
|
|
171
183
|
const runner = typeof securityRunner === "function" ? securityRunner : cp.spawnSync;
|
|
172
184
|
if (runner === cp.spawnSync && !fs.existsSync(MACOS_SECURITY_BIN)) return false;
|
|
173
185
|
|
|
174
|
-
const result = runner(MACOS_SECURITY_BIN, ["find-generic-password", "-s", svc], {
|
|
186
|
+
const result = runner(MACOS_SECURITY_BIN, ["find-generic-password", "-s", svc, "-a", resolveClaudeKeychainAccount({ env })], {
|
|
175
187
|
stdio: "ignore",
|
|
176
188
|
timeout: Number.isFinite(timeoutMs) ? timeoutMs : 2000,
|
|
177
189
|
});
|
|
@@ -180,14 +192,14 @@ function probeMacosKeychainGenericPassword({ service, securityRunner, timeoutMs
|
|
|
180
192
|
return result.status === 0;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
function readMacosKeychainPassword({ service, securityRunner, timeoutMs } = {}) {
|
|
195
|
+
function readMacosKeychainPassword({ service, securityRunner, timeoutMs, env } = {}) {
|
|
184
196
|
const svc = normalizeString(service);
|
|
185
197
|
if (!svc) return null;
|
|
186
198
|
|
|
187
199
|
const runner = typeof securityRunner === "function" ? securityRunner : cp.spawnSync;
|
|
188
200
|
if (runner === cp.spawnSync && !fs.existsSync(MACOS_SECURITY_BIN)) return null;
|
|
189
201
|
|
|
190
|
-
const result = runner(MACOS_SECURITY_BIN, ["find-generic-password", "-s", svc, "-w"], {
|
|
202
|
+
const result = runner(MACOS_SECURITY_BIN, ["find-generic-password", "-s", svc, "-a", resolveClaudeKeychainAccount({ env }), "-w"], {
|
|
191
203
|
stdio: ["ignore", "pipe", "ignore"],
|
|
192
204
|
timeout: Number.isFinite(timeoutMs) ? timeoutMs : 2000,
|
|
193
205
|
encoding: "utf8",
|
|
@@ -217,12 +229,13 @@ function readClaudeCodeCredentialsFile({ home, fsReader } = {}) {
|
|
|
217
229
|
}
|
|
218
230
|
}
|
|
219
231
|
|
|
220
|
-
function detectClaudeCodeCredentialsPresence({ platform = process.platform, securityRunner, home, fsReader } = {}) {
|
|
232
|
+
function detectClaudeCodeCredentialsPresence({ platform = process.platform, securityRunner, home, fsReader, env } = {}) {
|
|
221
233
|
if (platform === "darwin") {
|
|
222
234
|
for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
|
|
223
235
|
const present = probeMacosKeychainGenericPassword({
|
|
224
236
|
service,
|
|
225
237
|
securityRunner,
|
|
238
|
+
env,
|
|
226
239
|
});
|
|
227
240
|
if (!present) continue;
|
|
228
241
|
|
|
@@ -260,6 +273,31 @@ function detectClaudeCodeCredentialsPresence({ platform = process.platform, secu
|
|
|
260
273
|
return null;
|
|
261
274
|
}
|
|
262
275
|
|
|
276
|
+
function parseClaudeOauthExpiryMs(value) {
|
|
277
|
+
if (typeof value === "number") {
|
|
278
|
+
if (!Number.isFinite(value)) return null;
|
|
279
|
+
if (value <= 0) return 0;
|
|
280
|
+
return value < 1e12 ? value * 1000 : value;
|
|
281
|
+
}
|
|
282
|
+
if (typeof value === "string") {
|
|
283
|
+
const trimmed = value.trim();
|
|
284
|
+
if (!trimmed) return null;
|
|
285
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return parseClaudeOauthExpiryMs(Number(trimmed));
|
|
286
|
+
const ms = Date.parse(trimmed);
|
|
287
|
+
return Number.isFinite(ms) ? ms : null;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function extractClaudeCodeOauth(payload, nowMs = Date.now()) {
|
|
293
|
+
const oauth = payload?.claudeAiOauth;
|
|
294
|
+
const accessToken = normalizeString(oauth?.accessToken);
|
|
295
|
+
if (!accessToken) return null;
|
|
296
|
+
const expiresAtMs = parseClaudeOauthExpiryMs(oauth?.expiresAt);
|
|
297
|
+
if (expiresAtMs != null && expiresAtMs <= nowMs + CLAUDE_TOKEN_EXPIRY_SKEW_MS) return null;
|
|
298
|
+
return { accessToken, expiresAtMs };
|
|
299
|
+
}
|
|
300
|
+
|
|
263
301
|
function extractClaudeKeychainSubscription(payload) {
|
|
264
302
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
265
303
|
|
|
@@ -273,11 +311,11 @@ function extractClaudeKeychainSubscription(payload) {
|
|
|
273
311
|
return { subscriptionType, rateLimitTier };
|
|
274
312
|
}
|
|
275
313
|
|
|
276
|
-
function detectClaudeCodeSubscriptionDetails({ platform = process.platform, securityRunner, home, fsReader } = {}) {
|
|
314
|
+
function detectClaudeCodeSubscriptionDetails({ platform = process.platform, securityRunner, home, fsReader, env } = {}) {
|
|
277
315
|
const rawPayloads = [];
|
|
278
316
|
if (platform === "darwin") {
|
|
279
317
|
for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
|
|
280
|
-
const raw = readMacosKeychainPassword({ service, securityRunner });
|
|
318
|
+
const raw = readMacosKeychainPassword({ service, securityRunner, env });
|
|
281
319
|
if (raw) rawPayloads.push(raw);
|
|
282
320
|
}
|
|
283
321
|
} else if (usesClaudeCodeCredentialsFile(platform)) {
|
|
@@ -327,14 +365,14 @@ async function collectLocalSubscriptions({
|
|
|
327
365
|
if (opencode) out.push(opencode);
|
|
328
366
|
|
|
329
367
|
if (probeKeychainDetails) {
|
|
330
|
-
const claude = detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home });
|
|
368
|
+
const claude = detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home, env });
|
|
331
369
|
if (claude) out.push(claude);
|
|
332
370
|
else if (probeKeychain) {
|
|
333
|
-
const present = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
|
|
371
|
+
const present = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home, env });
|
|
334
372
|
if (present) out.push(present);
|
|
335
373
|
}
|
|
336
374
|
} else if (probeKeychain) {
|
|
337
|
-
const claude = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
|
|
375
|
+
const claude = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home, env });
|
|
338
376
|
if (claude) out.push(claude);
|
|
339
377
|
}
|
|
340
378
|
|
|
@@ -364,14 +402,25 @@ async function detectOpenclawSessionIntegration({ home, env }) {
|
|
|
364
402
|
};
|
|
365
403
|
}
|
|
366
404
|
|
|
367
|
-
|
|
405
|
+
// Returns the live OAuth token plus its expiry stamp. The expiry doubles as a
|
|
406
|
+
// rotation marker for the 429 cool-down: it changes on every refresh and, unlike
|
|
407
|
+
// the token itself, is not a secret, so nothing derived from a credential has to
|
|
408
|
+
// be written to disk.
|
|
409
|
+
function readClaudeCodeOauthToken({
|
|
410
|
+
platform = process.platform,
|
|
411
|
+
securityRunner,
|
|
412
|
+
home,
|
|
413
|
+
fsReader,
|
|
414
|
+
env,
|
|
415
|
+
nowMs = Date.now(),
|
|
416
|
+
} = {}) {
|
|
368
417
|
if (platform === "darwin") {
|
|
369
418
|
for (const service of CLAUDE_CODE_KEYCHAIN_SERVICES) {
|
|
370
419
|
try {
|
|
371
|
-
const raw = readMacosKeychainPassword({ service, securityRunner });
|
|
420
|
+
const raw = readMacosKeychainPassword({ service, securityRunner, env });
|
|
372
421
|
if (!raw) continue;
|
|
373
|
-
const
|
|
374
|
-
|
|
422
|
+
const oauth = extractClaudeCodeOauth(JSON.parse(raw), nowMs);
|
|
423
|
+
if (oauth) return oauth;
|
|
375
424
|
} catch (_e) {
|
|
376
425
|
continue;
|
|
377
426
|
}
|
|
@@ -386,13 +435,16 @@ function readClaudeCodeAccessToken({ platform = process.platform, securityRunner
|
|
|
386
435
|
const raw = readClaudeCodeCredentialsFile({ home, fsReader });
|
|
387
436
|
if (!raw) return null;
|
|
388
437
|
try {
|
|
389
|
-
|
|
390
|
-
return normalizeString(payload?.claudeAiOauth?.accessToken);
|
|
438
|
+
return extractClaudeCodeOauth(JSON.parse(raw), nowMs);
|
|
391
439
|
} catch (_e) {
|
|
392
440
|
return null;
|
|
393
441
|
}
|
|
394
442
|
}
|
|
395
443
|
|
|
444
|
+
function readClaudeCodeAccessToken(options = {}) {
|
|
445
|
+
return readClaudeCodeOauthToken(options)?.accessToken ?? null;
|
|
446
|
+
}
|
|
447
|
+
|
|
396
448
|
async function readCodexAccessToken({ home, env } = {}) {
|
|
397
449
|
try {
|
|
398
450
|
const codexHome = resolveCodexHome({ home, env });
|
|
@@ -445,10 +497,12 @@ async function readCodexAuthBundle({ home, env } = {}) {
|
|
|
445
497
|
}
|
|
446
498
|
|
|
447
499
|
module.exports = {
|
|
500
|
+
resolveClaudeKeychainAccount,
|
|
448
501
|
collectLocalSubscriptions,
|
|
449
502
|
detectClaudeCodeCredentialsPresence,
|
|
450
503
|
detectClaudeCodeSubscriptionDetails,
|
|
451
504
|
readClaudeCodeAccessToken,
|
|
505
|
+
readClaudeCodeOauthToken,
|
|
452
506
|
readCodexAccessToken,
|
|
453
507
|
readCodexAuthBundle,
|
|
454
508
|
};
|