Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import * as https from "https";
import { TeamsResponse, TeamDetails, SpendData, UserMeResponse, UserUsageResponse } from "./models";
import {
TeamsResponse,
TeamDetails,
SpendData,
UserMeResponse,
UserUsageResponse,
UsageSummaryResponse,
HardLimitResponse,
} from "./models";

const BASE_URL = "https://cursor.com/api";

Expand Down Expand Up @@ -120,3 +128,13 @@ export async function fetchUserMe(cookie: string): Promise<UserMeResponse> {
export async function fetchUserUsage(userId: string, cookie: string): Promise<UserUsageResponse> {
return get<UserUsageResponse>(`usage?user=${userId}`, cookie);
}

/** Fetches the aggregate usage summary, including on-demand USD usage. */
export async function fetchUsageSummary(cookie: string): Promise<UsageSummaryResponse> {
return get<UsageSummaryResponse>("usage-summary", cookie);
}

/** Fetches the current hard limit for on-demand spending. */
export async function fetchHardLimit(cookie: string): Promise<HardLimitResponse> {
return get<HardLimitResponse>("dashboard/get-hard-limit", cookie);
}
111 changes: 106 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import * as vscode from "vscode";
import * as api from "./api";
import * as statusBar from "./statusBar";
import * as config from "./configuration";
import { TeamMemberSpend } from "./models";
import { SpendData, TeamMemberSpend } from "./models";

let refreshTimer: NodeJS.Timeout | undefined;
const CENTS_PER_DOLLAR = 100;

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CENTS_PER_DOLLAR constant is defined but never used. It should be used in the dollarsToCents function (line 91) for the conversion: return Math.round(amount * CENTS_PER_DOLLAR); and could also be used in resolveHardLimitDollars (line 76) for clarity: return candidate / CENTS_PER_DOLLAR;

Copilot uses AI. Check for mistakes.

/**
* Calculates the next reset date based on the start of month date.
Expand Down Expand Up @@ -33,6 +34,63 @@ function calculateResetInfo(startOfMonth: string): {
return { resetDate, daysRemaining, resetDateStr };
}

function resolveHardLimitDollars(
spendData?: SpendData,
memberSpend?: TeamMemberSpend
): number | undefined {
const memberCandidates = [
memberSpend?.hardLimitOverrideDollars,
memberSpend?.hardLimitDollars,
];

for (const candidate of memberCandidates) {
if (isValidNumber(candidate)) {
return candidate;
}
}

if (!spendData) {
return undefined;
}

const teamDollarCandidates = [
spendData.teamHardLimitDollars,
spendData.hardLimitDollars,
spendData.defaultHardLimitDollars,
];

for (const candidate of teamDollarCandidates) {
if (isValidNumber(candidate)) {
return candidate;
}
}

const teamCentCandidates = [
spendData.teamHardLimitCents,
spendData.hardLimitCents,
spendData.defaultHardLimitCents,
];

for (const candidate of teamCentCandidates) {
if (isValidNumber(candidate)) {
return candidate / 100;
}
}

return undefined;
}

function isValidNumber(value?: number | null): value is number {
return typeof value === "number" && Number.isFinite(value);
}

function dollarsToCents(amount?: number | null): number | undefined {
if (!isValidNumber(amount)) {
return undefined;
}
return Math.round(amount );

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dollarsToCents function is missing multiplication by 100. It should convert dollars to cents by multiplying the amount by 100 (or using the CENTS_PER_DOLLAR constant defined on line 8). Currently, it just rounds the dollar amount without conversion.

Example: dollarsToCents(1.50) should return 150, but currently returns 2.

Suggested change
return Math.round(amount );
return Math.round(amount * CENTS_PER_DOLLAR);

Copilot uses AI. Check for mistakes.
}

/**
* This is the main activation function for the extension.
* It's called by VS Code when the extension is activated.
Expand Down Expand Up @@ -191,12 +249,14 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {

// TEAM FLOW: Try to get team-based usage data if user has related team from api/sets a team ID manually
// If we couldn't get team spend data, we'll show a simplified view with just the individual user data
if (teamId) {
let teamSpendData: SpendData | undefined;

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line should be indented with 4 spaces to align with the surrounding code block, not 2 spaces.

Suggested change
let teamSpendData: SpendData | undefined;
let teamSpendData: SpendData | undefined;

Copilot uses AI. Check for mistakes.

if (teamId) {
try {
const userDetails = await api.fetchTeamDetails(teamId, cookie);
const spendData = await api.fetchTeamSpend(teamId, cookie);
teamSpendData = await api.fetchTeamSpend(teamId, cookie);

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line should be indented with 6 spaces to align with the code inside the try block, not 4 spaces.

Suggested change
teamSpendData = await api.fetchTeamSpend(teamId, cookie);
teamSpendData = await api.fetchTeamSpend(teamId, cookie);

Copilot uses AI. Check for mistakes.

mySpend = spendData.teamMemberSpend.find(
mySpend = teamSpendData.teamMemberSpend.find(

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line should be indented with 6 spaces to align with the code inside the try block, not 4 spaces.

Suggested change
mySpend = teamSpendData.teamMemberSpend.find(
mySpend = teamSpendData.teamMemberSpend.find(

Copilot uses AI. Check for mistakes.
(member) => member.userId === userDetails.userId
);
} catch (teamError: any) {
Expand All @@ -221,7 +281,47 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {
// TEAM FLOW: Use team-based data when available
usedRequests = mySpend.fastPremiumRequests;
spendCents = mySpend.spendCents;
hardLimitDollars = mySpend.hardLimitOverrideDollars;
hardLimitDollars = resolveHardLimitDollars(teamSpendData, mySpend);
}

if (spendCents === undefined || hardLimitDollars === undefined) {
try {
const usageSummary = await api.fetchUsageSummary(cookie);
const onDemandUsage = usageSummary.individualUsage?.onDemand;

if (onDemandUsage?.enabled) {
if (spendCents === undefined) {
const usageCents = dollarsToCents(onDemandUsage.used);
if (usageCents !== undefined) {
spendCents = usageCents;
}
}

if (hardLimitDollars === undefined && isValidNumber(onDemandUsage.limit)) {
hardLimitDollars = dollarsToCents(onDemandUsage.limit);
if (hardLimitDollars !== undefined) {
hardLimitDollars = hardLimitDollars / 100;
}
Comment on lines +301 to +304

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is incorrect. dollarsToCents(onDemandUsage.limit) returns a value in cents, but then divides by 100 again on line 303. This results in hardLimitDollars being set to the wrong value (off by a factor of 100).

Since onDemandUsage.limit is already in dollars and we want hardLimitDollars in dollars, we should either:

  1. Use hardLimitDollars = onDemandUsage.limit directly, or
  2. Fix the dollarsToCents function to properly multiply by 100, then keep this division

The current code will set hardLimitDollars to 1/100th of the intended value.

Suggested change
hardLimitDollars = dollarsToCents(onDemandUsage.limit);
if (hardLimitDollars !== undefined) {
hardLimitDollars = hardLimitDollars / 100;
}
hardLimitDollars = onDemandUsage.limit;

Copilot uses AI. Check for mistakes.
}
}
} catch (summaryError: any) {
console.warn(
`[Cursor Usage] Failed to fetch usage summary: ${summaryError.message}`
);
}
}

if (hardLimitDollars === undefined) {
try {
const hardLimitResponse = await api.fetchHardLimit(cookie);
if (isValidNumber(hardLimitResponse.hardLimit)) {
hardLimitDollars = hardLimitResponse.hardLimit;
}
} catch (hardLimitError: any) {
console.warn(
`[Cursor Usage] Failed to fetch hard limit: ${hardLimitError.message}`
);
}
}

// Calculate final values and update status bar
Expand All @@ -231,6 +331,7 @@ async function refreshUsage(context: vscode.ExtensionContext): Promise<void> {
statusBar.updateStatusBar(
remainingRequests,
maxRequests,
usedRequests,
spendCents,
hardLimitDollars,
resetInfo
Expand Down
38 changes: 38 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface TeamMemberSpend {
userId?: number;
spendCents?: number;
hardLimitOverrideDollars?: number;
hardLimitDollars?: number;
name?: string;
role?: string;
}
Expand All @@ -40,6 +41,43 @@ export interface TeamMemberSpend {
*/
export interface SpendData {
teamMemberSpend: TeamMemberSpend[];
hardLimitDollars?: number;
hardLimitCents?: number;
defaultHardLimitDollars?: number;
defaultHardLimitCents?: number;
teamHardLimitDollars?: number;
teamHardLimitCents?: number;
}

interface UsageSummaryBreakdown {
included?: number;
bonus?: number;
total?: number;
}
Comment on lines +52 to +56

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation comment. For consistency with other interfaces in this file, this interface should have a JSDoc comment explaining what it represents. For example:

/**
 * Represents the breakdown of usage limits in a usage summary metric.
 */
interface UsageSummaryBreakdown {

Copilot uses AI. Check for mistakes.

export interface UsageSummaryMetric {
enabled: boolean;
used: number;
limit: number;
remaining: number;
breakdown?: UsageSummaryBreakdown;
}
Comment on lines +58 to +64

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation comment. For consistency with other interfaces in this file, this interface should have a JSDoc comment explaining what it represents. For example:

/**
 * Represents a metric with usage, limit, and remaining values.
 */
export interface UsageSummaryMetric {

Copilot uses AI. Check for mistakes.

export interface UsageSummaryResponse {
billingCycleStart: string;
billingCycleEnd: string;
membershipType?: string;
limitType?: string;
isUnlimited?: boolean;
individualUsage?: {
plan?: UsageSummaryMetric;
onDemand?: UsageSummaryMetric;
};
teamUsage?: Record<string, unknown>;
}
Comment on lines +66 to +77

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation comment. For consistency with other interfaces in this file, this interface should have a JSDoc comment explaining what it represents and which endpoint returns it. For example:

/**
 * Represents the aggregate usage summary response.
 * Returned by the /api/usage-summary endpoint.
 */
export interface UsageSummaryResponse {

Copilot uses AI. Check for mistakes.

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation comment. For consistency with other interfaces in this file, this interface should have a JSDoc comment explaining what it represents and which endpoint returns it. For example:

/**
 * Represents the hard limit response for on-demand spending.
 * Returned by the /api/dashboard/get-hard-limit endpoint.
 */
export interface HardLimitResponse {
Suggested change
/**
* Represents the hard limit response for on-demand spending.
* Returned by the /api/dashboard/get-hard-limit endpoint.
*/

Copilot uses AI. Check for mistakes.
export interface HardLimitResponse {
hardLimit: number;
}

/**
Expand Down
28 changes: 21 additions & 7 deletions src/statusBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export function createStatusBarItem() {
* Updates the status bar with the remaining requests, spending info, reset info, and appropriate color/icon.
* @param remainingRequests The number of requests left.
* @param totalRequests The total number of requests allowed in the cycle.
* @param usedRequests The number of requests already used in the cycle.
* @param spendCents The amount spent in cents (optional).
* @param hardLimitDollars The hard limit in dollars (optional).
* @param resetInfo Information about when the usage resets (optional).
*/
export function updateStatusBar(
remainingRequests: number,
totalRequests: number,
usedRequests: number,
spendCents?: number,
hardLimitDollars?: number,
resetInfo?: ResetInfo
Expand Down Expand Up @@ -92,15 +94,22 @@ export function updateStatusBar(
// Only show spending when there are 0 requests left
let statusText: string;

const normalizedUsedRequests = Math.max(0, usedRequests);
const usageSummary = `${normalizedUsedRequests}/${totalRequests}`;
const spendSummary =
spendCents !== undefined && hardLimitDollars !== undefined
? `$${(spendCents / 100).toFixed(2)}/$${hardLimitDollars.toFixed(2)}`
: undefined;

if (remainingRequests > 0) {
// Show remaining requests
statusText = `${icon} ${remainingRequests}`;
statusText = spendSummary
? `${icon} ${remainingRequests} · ${spendSummary}`
: `${icon} ${remainingRequests} · ${usageSummary}`;
} else {
// No requests left - show spending instead (if available)
if (spendCents !== undefined && hardLimitDollars !== undefined) {
const spendDollars = (spendCents / 100).toFixed(2);
const limitDollars = hardLimitDollars.toFixed(2);
statusText = `${icon} $${spendDollars}/$${limitDollars}`;
if (spendSummary) {
statusText = `${icon} ${spendSummary}`;
} else {
// No spending data available, just show 0
statusText = `${icon} 0`;
Expand All @@ -112,6 +121,7 @@ export function updateStatusBar(
// Update tooltip with detailed information
updateTooltip(
remainingRequests,
normalizedUsedRequests,
totalRequests,
spendCents,
hardLimitDollars,
Expand All @@ -122,13 +132,15 @@ export function updateStatusBar(
/**
* Updates the tooltip with comprehensive usage, spending, and reset information.
* @param remainingRequests The number of requests left.
* @param usedRequests The number of requests already used in the cycle.
* @param totalRequests The total number of requests allowed in the cycle.
* @param spendCents The amount spent in cents (optional).
* @param hardLimitDollars The hard limit in dollars (optional).
* @param resetInfo Information about when the usage resets (optional).
*/
function updateTooltip(
remainingRequests: number,
usedRequests: number,
totalRequests: number,
spendCents?: number,
hardLimitDollars?: number,
Expand All @@ -138,8 +150,10 @@ function updateTooltip(
return;
}

const usedRequests = totalRequests - remainingRequests;
const requestPercentage = ((usedRequests / totalRequests) * 100).toFixed(1);
const usageForDisplay = Math.max(0, usedRequests);
const requestPercentage = totalRequests
? ((usageForDisplay / totalRequests) * 100).toFixed(1)
: "0.0";

// Calculate cycle information once if resetInfo is available
let daysElapsed = 0;
Expand Down
Loading