- Add CommandCode usage handler mirroring the official CLI /usage
(whoami → credits/subscriptions → summary) with 5h/weekly/monthly
quota rows, registered in services/usage.js and registry config
- Parse commandcode quota rows in ProviderLimits (remainingPercentage + $ unit)
- Match official CLI request shape: x-command-code-version 1.10.0,
User-Agent cli, and config.environment '${platform}-${arch}, Node.js ${version}'
- Fix connect timeout unit confusion: both profile and provider pages
now use ms with a 1s minimum guard (prevents 60ms footgun)
- Fix fetchT0 ReferenceError in base.js error path and log fetch
diagnostics only on upstream failure
- Quota Tracker defaults to the Active account filter
- Ignore .commandcode/ CLI local state
204 lines
6.0 KiB
JavaScript
204 lines
6.0 KiB
JavaScript
/**
|
|
* CommandCode usage handler
|
|
*
|
|
* Mirrors the official command-code CLI /usage command: it calls the alpha API
|
|
* to surface the 5-hour + weekly usage windows, the subscription plan, and the
|
|
* credits consumed in the current billing period.
|
|
*
|
|
* GET /alpha/whoami → org.id (org-scoped billing; null for personal)
|
|
* GET /alpha/billing/credits → { credits: { monthlyCredits, purchasedCredits,
|
|
* freeCredits }, windowLimits: { fiveHour, weekly } }
|
|
* GET /alpha/billing/subscriptions → { data: { planId, currentPeriodStart, ... } }
|
|
* GET /alpha/usage/summary?since= → period token/cost totals
|
|
*
|
|
* The CLI fetches whoami first (for orgId), then credits + subscription in
|
|
* parallel, then the summary with since = currentPeriodStart. We keep the same
|
|
* order/dependencies: window limits live on credits, and the plan period start
|
|
* determines the summary window.
|
|
*/
|
|
|
|
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
|
import { U, parseResetTime } from "./shared.js";
|
|
|
|
const USAGE = U("commandcode");
|
|
const BASE = USAGE.baseUrl || "https://api.commandcode.ai";
|
|
const WHOAMI_URL = BASE + (USAGE.whoamiUrl || "/alpha/whoami");
|
|
const CREDITS_URL = BASE + (USAGE.creditsUrl || "/alpha/billing/credits");
|
|
const SUBSCRIPTIONS_URL =
|
|
BASE + (USAGE.subscriptionsUrl || "/alpha/billing/subscriptions");
|
|
const SUMMARY_URL = BASE + (USAGE.summaryUrl || "/alpha/usage/summary");
|
|
|
|
function buildHeaders(token) {
|
|
return {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: "application/json",
|
|
};
|
|
}
|
|
|
|
/** Build a normalized quota row. `unit` is "$" — the API reports currency credits. */
|
|
function makeQuota({ used, total, resetAt, unlimited = false, unit = "$" }) {
|
|
const safeTotal = Math.max(0, Number(total) || 0);
|
|
const safeUsed = Math.max(0, Number(used) || 0);
|
|
if (unlimited || safeTotal === 0) {
|
|
return {
|
|
used: safeUsed,
|
|
total: 0,
|
|
remainingPercentage: unlimited ? 100 : 0,
|
|
resetAt: resetAt || null,
|
|
unit,
|
|
unlimited: true,
|
|
};
|
|
}
|
|
const remaining = Math.max(0, safeTotal - safeUsed);
|
|
const remainingPercentage = (remaining / safeTotal) * 100;
|
|
return {
|
|
used: safeUsed,
|
|
total: safeTotal,
|
|
remainingPercentage,
|
|
resetAt: resetAt || null,
|
|
unit,
|
|
unlimited: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} apiKey - commandcode API key (user_...)
|
|
* @param {object|null} proxyOptions
|
|
*/
|
|
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
|
|
if (!apiKey) {
|
|
return { message: "CommandCode credential not available." };
|
|
}
|
|
|
|
const headers = buildHeaders(apiKey);
|
|
|
|
try {
|
|
// whoami resolves the org id (billing is org-scoped; null for personal).
|
|
const whoamiRes = await proxyAwareFetch(
|
|
WHOAMI_URL,
|
|
{ method: "GET", headers },
|
|
proxyOptions,
|
|
);
|
|
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
|
|
return { message: "CommandCode credential invalid or expired." };
|
|
}
|
|
if (!whoamiRes.ok) {
|
|
return { message: `CommandCode whoami API error (${whoamiRes.status}).` };
|
|
}
|
|
const whoami = await whoamiRes.json().catch(() => null);
|
|
const orgId = whoami?.org?.id ?? null;
|
|
|
|
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
|
|
|
const [creditsRes, subsRes] = await Promise.all([
|
|
proxyAwareFetch(
|
|
CREDITS_URL + orgQuery,
|
|
{ method: "GET", headers },
|
|
proxyOptions,
|
|
),
|
|
proxyAwareFetch(
|
|
SUBSCRIPTIONS_URL + orgQuery,
|
|
{ method: "GET", headers },
|
|
proxyOptions,
|
|
),
|
|
]);
|
|
|
|
if (
|
|
creditsRes.status === 401 ||
|
|
creditsRes.status === 403 ||
|
|
subsRes.status === 401 ||
|
|
subsRes.status === 403
|
|
) {
|
|
return { message: "CommandCode credential invalid or expired." };
|
|
}
|
|
if (!creditsRes.ok) {
|
|
return {
|
|
message: `CommandCode credits API error (${creditsRes.status}).`,
|
|
};
|
|
}
|
|
|
|
const credits = await creditsRes.json().catch(() => null);
|
|
const subs = await subsRes.json().catch(() => null);
|
|
|
|
const subData = subs?.data;
|
|
const planId = subData?.planId ?? null;
|
|
const periodStart = subData?.currentPeriodStart ?? null;
|
|
|
|
// Summary needs `since`; the CLI falls back to first-of-month when the
|
|
// subscription period start is unavailable.
|
|
const since = periodStart || firstOfMonth();
|
|
const summaryRes = await proxyAwareFetch(
|
|
`${SUMMARY_URL}?since=${encodeURIComponent(since)}`,
|
|
{ method: "GET", headers },
|
|
proxyOptions,
|
|
);
|
|
const summary = summaryRes.ok
|
|
? await summaryRes.json().catch(() => null)
|
|
: null;
|
|
|
|
const quotas = {};
|
|
const windowLimits = credits?.windowLimits || {};
|
|
|
|
const fiveHour = windowLimits.fiveHour;
|
|
if (fiveHour && Number(fiveHour.cap) > 0) {
|
|
quotas["5-hour window"] = makeQuota({
|
|
used: fiveHour.used,
|
|
total: fiveHour.cap,
|
|
resetAt: parseResetTime(fiveHour.resetAt),
|
|
});
|
|
}
|
|
|
|
const weekly = windowLimits.weekly;
|
|
if (weekly && Number(weekly.cap) > 0) {
|
|
quotas["Weekly window"] = makeQuota({
|
|
used: weekly.used,
|
|
total: weekly.cap,
|
|
resetAt: parseResetTime(weekly.resetAt),
|
|
});
|
|
}
|
|
|
|
// Monthly credits consumed this billing period (from summary when present,
|
|
// else the credits object's monthlyCredits as a fallback).
|
|
const monthlyUsed =
|
|
typeof summary?.totalCredits === "number"
|
|
? summary.totalCredits
|
|
: typeof credits?.credits?.monthlyCredits === "number"
|
|
? credits.credits.monthlyCredits
|
|
: 0;
|
|
|
|
const monthlyTotal =
|
|
typeof credits?.credits?.monthlyCredits === "number"
|
|
? credits.credits.monthlyCredits
|
|
: 0;
|
|
|
|
if (monthlyTotal > 0 || monthlyUsed > 0) {
|
|
quotas["Monthly credits"] = makeQuota({
|
|
used: monthlyUsed,
|
|
total: monthlyTotal,
|
|
resetAt: periodStart ? undefined : null,
|
|
});
|
|
}
|
|
|
|
if (Object.keys(quotas).length === 0) {
|
|
return {
|
|
plan: planId || "CommandCode",
|
|
message: "CommandCode connected, but no quota was reported.",
|
|
quotas: {},
|
|
};
|
|
}
|
|
|
|
return {
|
|
plan: planId || "CommandCode",
|
|
quotas,
|
|
periodBasis: summary?.periodBasis || "billing-period",
|
|
};
|
|
} catch (error) {
|
|
return { message: `CommandCode usage error: ${error.message}` };
|
|
}
|
|
}
|
|
|
|
function firstOfMonth() {
|
|
const now = new Date();
|
|
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
|
}
|