@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
48 lines (47 loc) • 3.07 kB
JavaScript
/** Provider identifiers, not arbitrary free text or credentials. */
export function planName(value) {
if (typeof value !== "string" || !/^[a-z0-9_+ -]{1,64}$/i.test(value) || !value.trim())
return undefined;
return value.trim().replace(/^default_claude_/, "").replace(/^claude_/, "").split(/[_ -]+/)
.map(word => /^\d+x$/i.test(word) ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
}
const CLAUDE_PLANS = { pro: "Pro", max: "Max", team: "Team", enterprise: "Enterprise" };
/**
* A Claude login's plan from its oauthAccount profile, as Claude names it:
* the organization type first (a Team or Enterprise seat can carry any rate
* tier), the rate tier only for Pro and Max's 5x or 20x, then the billing
* type and Claude Code's own `subscriptionType`. Internal tier codes
* ("default_raven") are never shown; an unknown plan is undefined.
*/
export function claudePlan(profile) {
const text = (value) => typeof value === "string" && value.length <= 64 ? value.toLowerCase() : "";
const kind = (value) => /^(?:claude_)?(pro|max|team|enterprise)(?:_subscription)?$/.exec(value)?.[1];
const tier = [text(profile.organizationRateLimitTier), text(profile.userRateLimitTier)];
const multiple = tier.map(value => /^default_claude_max_(5|20)x$/.exec(value)?.[1]).find(Boolean);
const fromTier = tier.map(value => value === "default_claude_ai" || value === "default_claude_pro" ? "pro" : /^default_claude_max(?:_\d+x)?$/.test(value) ? "max" : undefined).find(Boolean);
const plan = kind(text(profile.organizationType)) ?? fromTier ?? kind(text(profile.billingType)) ?? kind(text(profile.subscriptionType));
if (!plan)
return undefined;
if (plan === "max" && multiple)
return `Max ${multiple}x`;
if (plan === "team" && text(profile.seatTier) === "team_premium")
return "Team Premium";
return CLAUDE_PLANS[plan];
}
export function subscriptionDate(value) {
const time = typeof value === "string" && /^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(value) ? Date.parse(value)
: typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value < 32_503_680_000 ? value * 1000 : NaN;
return Number.isFinite(time) && time > 0 && time < 32_503_680_000_000 ? new Date(time).toISOString() : undefined;
}
/** Next anniversary in UTC, always using the original day (Jan 31 -> Feb 28 -> Mar 31). */
export function monthlyAnniversary(startedAt, now) {
const start = new Date(startedAt);
if (!Number.isFinite(start.getTime()) || start > now)
return undefined;
const candidate = (month) => {
const lastDay = new Date(Date.UTC(now.getUTCFullYear(), month + 1, 0)).getUTCDate();
return new Date(Date.UTC(now.getUTCFullYear(), month, Math.min(start.getUTCDate(), lastDay), start.getUTCHours(), start.getUTCMinutes(), start.getUTCSeconds(), start.getUTCMilliseconds()));
};
const next = candidate(now.getUTCMonth());
return (next > now ? next : candidate(now.getUTCMonth() + 1)).toISOString();
}