// ==UserScript==
// @name Cursor.com Usage Tracker (Enhanced)
// @author monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small, NoahBPeterson, Sonnet 3.7, Gemini 2.5 Pro
// @namespace http://monnef.eu
// @version 0.5.1
// @description Tracks and displays usage statistics, payment cycles, and detailed model costs for Premium models on Cursor.com.
// @match https://www.cursor.com/settings
// @grant GM_getValue
// @grant GM_setValue
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @license AGPL-3.0
// @icon https://www.cursor.com/favicon-48x48.png
// ==/UserScript==
(function () {
'use strict';
const $ = jQuery.noConflict();
const $c = (cls, parent) => $(`.${cls}`, parent);
const $i = (id, parent) => $(`#${id}`, parent);
$.fn.nthParent = function (n) {
return this.parents().eq(n - 1);
};
const log = (...messages) => {
console.log(`[UsageTracker]`, ...messages);
};
const error = (...messages) => {
console.error(`[UsageTracker]`, ...messages);
};
const debug = (...messages) => {
console.debug(`[UsageTracker Debug]`, ...messages);
};
const genCssId = name => `ut-${name}`;
// --- CSS Class Names ---
const sigCls = genCssId('sig');
const buttonCls = genCssId('button');
const buttonWhiteCls = genCssId('button-white');
const buttonDarkCls = genCssId('button-dark');
const mainCaptionCls = genCssId('main-caption');
const modalCls = genCssId('modal');
const modalContentCls = genCssId('modal-content');
const modalCloseCls = genCssId('modal-close');
const copyButtonCls = genCssId('copy-button');
const inputCls = genCssId('input');
const inputWithButtonCls = genCssId('input-with-button');
const errorMessageCls = genCssId('error-message');
const settingsModalCls = genCssId('settings-modal');
const hrCls = genCssId('hr');
const debugContainerCls = genCssId('debug-container');
const hSpaceSmCls = genCssId('h-space-sm');
const hSpaceMdCls = genCssId('h-space-md');
const hSpaceLgCls = genCssId('h-space-lg');
const flexCenterCls = genCssId('flex-center');
const flexRightCls = genCssId('flex-right');
const flexBetweenCls = genCssId('flex-between');
const multiBarCls = genCssId('multi-bar');
const barSegmentCls = genCssId('bar-segment');
const tooltipCls = genCssId('tooltip');
const statsContainerCls = genCssId('stats-container');
const statItemCls = genCssId('stat-item');
const colors = {
cursor: {
blue: '#3864f6',
blueDarker: '#2e53cc',
lightGray: '#e5e7eb',
gray: '#a7a9ac',
grayDark: '#333333',
green: '#63a11a', // Original green
},
segments: [ // A palette for the multi-segment bar
'#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5',
'#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50',
'#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800',
'#FF5722', '#795548', '#9E9E9E', '#607D8B'
]
};
const styles = `
.${hSpaceSmCls} { height: 5px; }
.${hSpaceMdCls} { height: 10px; }
.${hSpaceLgCls} { height: 20px; }
.${flexCenterCls} { display: flex; justify-content: center; align-items: center; }
.${flexRightCls} { display: flex; justify-content: flex-end; align-items: center; }
.${flexBetweenCls} { display: flex; justify-content: space-between; align-items: center; }
.${sigCls} { font-size: 0.75rem; color: ${colors.cursor.gray}; margin-left: 0.75rem; opacity: 0.2; transition: opacity 0.1s ease-in-out; }
.${sigCls}:hover { opacity: 1; }
.${buttonCls}, .${buttonWhiteCls}, .${buttonDarkCls} { background-color: ${colors.cursor.blue}; color: white; font-size: 14px; border: none; border-radius: 4px; cursor: pointer; padding: 4.25px 8px; font-weight: 400; }
.${buttonCls}:hover { background-color: ${colors.cursor.blueDarker}; }
.${buttonWhiteCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 3px 8px; }
.${buttonWhiteCls}:hover { background-color: ${colors.cursor.lightGray}; }
.${buttonDarkCls} { background-color: black; color: white; border: 1px solid black; padding: 3px 8px; }
.${buttonDarkCls}:hover { background-color: white; color: black; }
.${modalCls} { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px) contrast(0.5); }
.${modalContentCls} { background-color: black; color: white; margin: 15% auto; padding: 15px 20px; width: 600px; border-radius: 4px; position: relative; }
.${modalCloseCls} { color: white; position: absolute; top: 0px; right: 10px; font-size: 25px; font-weight: bold; cursor: pointer; }
.${modalCloseCls}:hover { color: ${colors.cursor.lightGray}; }
.${copyButtonCls} { margin-left: 10px; width: 5em; }
.${modalContentCls} h2 { margin-bottom: 20px; }
.${modalContentCls} hr { border: 0; height: 1px; background-color: ${colors.cursor.grayDark}; margin: 10px 0; }
.${inputCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 5px; width: 100%; border-radius: 4px; font-size: 14px; }
.${inputWithButtonCls} { width: calc(100% - 5em - 10px); }
.${errorMessageCls} { color: #ff4d4f; font-size: 14px; margin-top: 5px; }
.${hrCls} { border: 0; height: 1px; background-color: #333333; /* Darker HR */ margin: 15px 0; }
.${debugContainerCls} { background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 12px; overflow-wrap: break-word; max-height: 200px; overflow-y: auto; }
.${statsContainerCls} { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; margin: 15px 0; padding: 15px; background-color: #1a1a1a; border-radius: 8px; }
.${statItemCls} { font-size: 14px; }
.${statItemCls} .label { color: ${colors.cursor.gray}; }
.${statItemCls} .value { color: white; font-weight: bold; }
.${multiBarCls} { display: flex; width: 100%; height: 15px; background-color: ${colors.cursor.grayDark}; border-radius: 4px; /* REMOVED: overflow: hidden; */ margin: 10px 0; }
.${barSegmentCls} { height: 100%; position: relative; transition: filter 0.2s ease-in-out; }
.${barSegmentCls}:hover { filter: brightness(1.2); }
.${barSegmentCls} .${tooltipCls} {
visibility: hidden;
width: max-content;
background-color: black;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 10px;
position: absolute;
z-index: 50; /* INCREASED Z-INDEX */
bottom: 150%; /* Adjusted slightly up */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
border: 1px solid ${colors.cursor.gray};
font-size: 12px;
pointer-events: none; /* Prevent tooltip from interfering with hover */
}
.${barSegmentCls}:hover .${tooltipCls} {
visibility: visible;
opacity: 1;
}
.${barSegmentCls} .${tooltipCls}::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: black transparent transparent transparent;
}
`;
const genHr = () => $('<hr>').addClass(hrCls);
const getUsageCard = () => {
const usageHeading = $('h2:contains("Usage")');
if (usageHeading.length > 0) {
const card = usageHeading.closest('.rounded-2xl, .rounded-3xl');
debug(`Found Usage card via h2: ${card.length > 0}`);
return card.length > 0 ? card : null;
}
debug('Usage card not found.');
return null;
};
// --- Data Parsing Functions ---
/**
* Finds and parses the "Recent Usage Events" table.
* @returns {{ modelUsage: Record<string, { count: number, cost: number }>, totalPaidRequests: number, totalRequests: number }}
*/
const parseUsageEventsTable = () => {
const modelUsage = {};
let totalPaidRequests = 0;
let totalRequests = 0;
const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
debug(`Found ${table.length} rows in Recent Usage Events table.`);
if (table.length === 0) {
error("Could not find 'Recent Usage Events' table.");
return { modelUsage, totalPaidRequests, totalRequests };
}
table.each((_, row) => {
const $row = $(row);
const model = $row.find('td:eq(1)').text().trim();
const status = $row.find('td:eq(2)').text().trim();
const requestsStr = $row.find('td:eq(3)').text().trim();
const requests = parseFloat(requestsStr) || 0;
if (status !== 'Errored, Not Charged' && model) { // Ensure model name exists
totalRequests += requests;
if (!modelUsage[model]) {
modelUsage[model] = { count: 0, cost: 0 }; // Initialize cost here
}
modelUsage[model].count += requests;
if (status === 'Usage-based') {
totalPaidRequests += requests;
}
}
});
debug('Parsed Usage Events:', { modelUsage, totalPaidRequests, totalRequests });
return { modelUsage, totalPaidRequests, totalRequests };
};
/**
* Finds and parses the "Current Usage" cost summary table.
* @param {Record<string, { count: number, cost: number }>} modelUsage - The object to update with costs.
* @returns {{ totalCost: number }}
*/
const parseCurrentUsageCosts = (modelUsage) => {
let totalCost = 0;
const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr'); // Find the *cost* table
debug(`Found ${costTable.length} rows in Current Usage (Cost) table.`);
if (costTable.length === 0) {
error("Could not find 'Current Usage' (Cost) table.");
return { totalCost };
}
costTable.each((_, row) => {
const $row = $(row);
const description = $row.find('td:eq(0)').text().trim().toLowerCase();
const costStr = $row.find('td:eq(1)').text().trim().replace('$', '');
const cost = parseFloat(costStr) || 0;
totalCost += cost; // Sum all costs, including negative ones (payments)
let foundModel = false;
for (const model in modelUsage) {
if (description.includes(model.toLowerCase())) {
modelUsage[model].cost += cost; // Add cost to the model
foundModel = true;
}
}
if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) {
if (!modelUsage['Extra/Other Premium']) {
modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 };
}
modelUsage['Extra/Other Premium'].cost += cost;
foundModel = true;
}
if (!foundModel && cost > 0) {
if (!modelUsage['Other Costs']) {
modelUsage['Other Costs'] = { count: 0, cost: 0 };
}
modelUsage['Other Costs'].cost += cost;
}
});
// Ensure all models in modelUsage have a cost, even if 0
for (const model in modelUsage) {
modelUsage[model].cost = modelUsage[model].cost || 0;
}
debug('Parsed Costs & Updated Model Usage:', { modelUsage, totalCost });
return { totalCost };
};
/**
* Extracts the basic "X / Y" usage if available.
* @returns {{ used: number, total: number }}
*/
const getBaseUsageData = () => {
debug('Attempting to find base usage data...');
const premiumLabel = $('span:contains("Premium models")').first();
if (premiumLabel.length === 0) {
debug('Base Premium models label not found.');
return {};
}
const usageSpan = premiumLabel.siblings('span').last();
const usageText = usageSpan.text();
debug(`Found base usage text: "${usageText}"`);
const regex = /(\d+) \/ (\d+)/;
const matches = usageText.match(regex);
if (matches && matches.length === 3) {
const used = parseInt(matches[1], 10);
const total = parseInt(matches[2], 10);
debug(`Parsed base values - Used: ${used}, Total: ${total}`);
return { used, total };
} else {
debug('Regex did not match the base usage text.');
return {};
}
};
// --- Display Functions ---
/**
* Creates a multi-segment progress bar.
* @param {Record<string, { count: number, cost: number }>} modelUsage
* @returns {JQuery<HTMLElement>}
*/
const createMultiSegmentProgressBar = (modelUsage) => {
const barContainer = $('<div>').addClass(multiBarCls);
const totalRequests = Object.values(modelUsage).reduce((sum, model) => sum + (model.count || 0), 0);
if (totalRequests === 0) {
return barContainer.text('No usage data for bar.').css({ height: 'auto', padding: '5px' });
}
let colorIndex = 0;
// Sort models by count descending for better visualization
const sortedModels = Object.entries(modelUsage)
.filter(([_, data]) => data.count > 0)
.sort(([, a], [, b]) => b.count - a.count);
for (const [model, data] of sortedModels) {
const percentage = (data.count / totalRequests) * 100;
const cost = data.cost.toFixed(2);
const color = colors.segments[colorIndex % colors.segments.length];
const tooltipText = `${model}: ${data.count.toFixed(1)} reqs ($${cost})`;
const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText);
const segment = $('<div>')
.addClass(barSegmentCls)
.css({
width: `${percentage}%`,
backgroundColor: color,
})
.append(tooltip);
barContainer.append(segment);
colorIndex++;
}
return barContainer;
};
/**
* Displays all the new tracker data.
* @param {number} paymentDay
*/
const displayEnhancedTrackerData = (paymentDay) => {
const tracker = $c(mainCaptionCls);
if (tracker.length === 0) {
error('Main caption not found for displaying enhanced data.');
return false; // Indicate failure
}
// Clear previous tracker data before parsing again
tracker.siblings(`.${genCssId('enhanced-tracker')}`).remove();
const { modelUsage, totalPaidRequests, totalRequests } = parseUsageEventsTable();
const { totalCost } = parseCurrentUsageCosts(modelUsage); // Update modelUsage with costs
const baseUsage = getBaseUsageData();
const container = $('<div>').addClass(genCssId('enhanced-tracker'));
const statsContainer = $('<div>').addClass(statsContainerCls);
const addStat = (label, value) => {
statsContainer.append(
$('<div>').addClass(statItemCls).append(
$('<span>').addClass('label').text(`${label}: `),
$('<span>').addClass('value').text(value)
)
);
}
addStat('Total Premium Requests', baseUsage.used !== undefined ? baseUsage.used : 'N/A');
addStat('Plan Limit', baseUsage.total !== undefined ? baseUsage.total : 'N/A');
addStat('Total Usage-Based Requests', totalPaidRequests.toFixed(1));
addStat('Total Usage Cost (This Cycle)', `$${totalCost.toFixed(2)}`);
addStat('Grand Total Requests (Events)', totalRequests.toFixed(1));
if (paymentDay) {
const daysInfo = calculateDaysPassed({ today: new Date(), paymentDay });
if (daysInfo) {
addStat('Billing Cycle Progress', `${daysInfo.daysPassed} / ${daysInfo.totalDays} days`);
}
} else {
addStat('Billing Cycle Progress', 'Set payment day');
}
container.append(
statsContainer,
$('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '10px' }).text('Model Usage Breakdown (Hover for details):'),
createMultiSegmentProgressBar(modelUsage)
);
tracker.after(container);
debug('Enhanced tracker data displayed.');
return true; // Indicate success
};
// --- Original Script Functions (Mostly Unchanged or Slightly Modified) ---
const decorateUsageCard = () => {
if ($c(mainCaptionCls).length > 0) {
return true;
}
const usageHeading = $('h2:contains("Usage")');
if (usageHeading.length > 0) {
const caption = $('<div>')
.addClass('font-medium gt-standard-mono text-xl/[1.375rem] font-semibold -tracking-4 md:text-2xl/[1.875rem]')
.addClass(mainCaptionCls)
.text('Usage Tracker');
usageHeading.after(genHr(), caption);
addSettingsButton(caption); // Add settings button here
debug("Added tracker after Usage heading");
return true;
}
return false;
};
const addUsageTracker = () => {
const paymentDay = GM_getValue('paymentDay');
return displayEnhancedTrackerData(paymentDay);
};
const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => {
if (!paymentDay) return null;
const currentMonth = today.getMonth();
const currentYear = today.getFullYear();
const lastPaymentDate = new Date(currentYear, currentMonth, paymentDay);
if (today < lastPaymentDate) {
lastPaymentDate.setMonth(lastPaymentDate.getMonth() - 1);
}
const daysPassed = Math.floor((today - lastPaymentDate) / (1000 * 60 * 60 * 24));
const nextPaymentDate = new Date(lastPaymentDate);
nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
const totalDays = Math.floor((nextPaymentDate - lastPaymentDate) / (1000 * 60 * 60 * 24));
const res = { daysPassed, totalDays, progress: daysPassed / totalDays };
if (!disableLog) {
debug(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`);
}
return res;
};
const createModal = ({ className, title, content }) => {
const modal = $('<div>').addClass(modalCls).addClass(className);
const modalContent = $('<div>').addClass(modalContentCls);
const closeButton = $('<span>').addClass(modalCloseCls).text('×');
const titleElement = $('<h1>')
.addClass('text-4xl gt-standard-mono font-medium')
.text(title);
modalContent.append(
closeButton,
titleElement,
$('<div>').addClass(hSpaceMdCls),
content
);
modal.append(modalContent);
closeButton.click(() => modal.hide());
$(window).click(event => {
if (event.target === modal[0]) {
modal.hide();
}
});
return modal;
};
const createSettingsModal = () => {
const subtitle = $('<p>').text('Enter the day of the month when you are billed (1-31):');
const input = $('<input>')
.addClass(inputCls)
.attr('type', 'number')
.attr('min', '1')
.attr('max', '31')
.val(GM_getValue('paymentDay') || '');
const tip = $('<p>')
.addClass('text-sm text-gray-500 mt-1')
.text('You can find your billing date via the "Manage Subscription" button on the left.');
const errorMessage = $('<p>').addClass(errorMessageCls).hide();
const saveAndReload = () => {
const newPaymentDay = parseInt(input.val(), 10);
if (newPaymentDay && newPaymentDay >= 1 && newPaymentDay <= 31) {
GM_setValue('paymentDay', newPaymentDay);
log(`Payment day has been set to: ${newPaymentDay}`);
$c(settingsModalCls).hide();
location.reload();
} else {
errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show();
}
};
const saveButton = $('<button>')
.addClass(buttonCls)
.text('Save & Reload')
.click(saveAndReload);
input.on('keypress', (e) => {
if (e.which === 13) saveAndReload();
});
const madeByText = $('<p>').html( // Updated authors
'Made with ❤️ by monnef, Sonnet 3.5 & Gemini'
);
const content = $('<div>').append(
subtitle,
$('<div>').addClass(hSpaceSmCls),
input,
tip,
errorMessage,
$('<div>').addClass(hSpaceLgCls),
$('<div>').addClass(flexBetweenCls).append(madeByText, saveButton)
);
return createModal({
className: settingsModalCls,
title: 'Usage Tracker Settings',
content: content
});
};
const addSettingsButton = (mainCaption) => {
const settingsButton = $('<button>')
.css({
position: 'absolute',
top: '-10px',
right: '0px',
height: '29.4px',
width: '29.4px',
padding: '0px',
filter: 'invert(1)',
})
.addClass(buttonWhiteCls)
.attr('title', 'Usage Tracker settings')
.append($(`<svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>`).css({ display: 'inline-block', verticalAlign: 'text-bottom' }));
settingsButton.click(() => {
log('Usage Tracker settings button clicked.');
$c(settingsModalCls).show();
$c(settingsModalCls).find(`.${inputCls}`).focus();
});
const buttonWrapper = $('<div>').css({ position: 'relative', height: '0px' });
buttonWrapper.append(settingsButton);
if (mainCaption.length > 0) {
mainCaption.prepend(buttonWrapper);
log('Settings button wrapper added to the page');
} else {
log('Main caption not found, settings button not added');
}
};
// --- Main Execution Logic ---
const state = {
addingUsageTrackerSucceeded: false,
addingUsageTrackerAttempts: 0,
};
const ATTEMPTS_LIMIT = 15;
const ATTEMPTS_INTERVAL = 500;
const ATTEMPTS_MAX_DELAY = 4000;
const main = () => {
state.addingUsageTrackerAttempts++;
log(`Attempt ${state.addingUsageTrackerAttempts}...`);
const scheduleNextAttempt = () => {
if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY);
log(`Attempt ${state.addingUsageTrackerAttempts} failed or incomplete. Retrying in ${delay}ms...`);
setTimeout(main, delay);
} else if (state.addingUsageTrackerSucceeded) {
log(`Attempt ${state.addingUsageTrackerAttempts} succeeded.`);
} else {
error(`All ${ATTEMPTS_LIMIT} attempts failed. Could not add Usage Tracker. Check selectors or page structure.`);
}
};
const decorationOkay = decorateUsageCard();
if (!decorationOkay) {
debug('Decoration failed. Will retry.');
scheduleNextAttempt();
return;
}
const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
if (usageEventsTable.length === 0) {
debug("'Recent Usage Events' table not found yet. Will retry.");
scheduleNextAttempt();
return;
}
debug(`Found ${usageEventsTable.length} usage events.`);
try {
state.addingUsageTrackerSucceeded = addUsageTracker();
} catch (e) {
error("Error during addUsageTracker:", e);
state.addingUsageTrackerSucceeded = false;
}
scheduleNextAttempt();
};
$(document).ready(() => {
log('Script started');
unsafeWindow.ut = { // For manual debugging
jq: $,
resetSettings: () => {
GM_setValue('paymentDay', undefined);
location.reload();
},
parseEvents: parseUsageEventsTable,
parseCosts: parseCurrentUsageCosts,
getBase: getBaseUsageData,
};
$('head').append($('<style>').text(styles));
$('body').append(createSettingsModal());
setTimeout(main, ATTEMPTS_INTERVAL); // Initial delay
});
})();