// ==UserScript==
// @name Cursor.com Usage Tracker
// @author monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small
// @namespace http://monnef.eu
// @version 0.2
// @description Tracks and displays the usage statistics and payment cycles for Premium models on Cursor.com, helping users monitor their subscriptions and usage limits.
// @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
// ==/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 genCssId = name => `ut-${name}`;
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 donationModalCls = genCssId('donation-modal');
const hrCls = genCssId('hr');
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 colors = {
cursor: {
blue: '#3864f6',
blueDarker: '#2e53cc',
lightGray: '#e5e7eb',
gray: '#a7a9ac',
grayDark: '#333333',
}
};
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;
cursor: pointer;
}
.${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} p {
}
.${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: ${colors.cursor.grayDark};
margin: 10px 0;
}
`;
const genHr = () => $('<hr>').addClass(hrCls);
const getUsageCard = () => $('.col-span-2 > div:has(h2:contains("Usage"))');
const decorateUsageCard = () => {
if ($c(mainCaptionCls).length > 0) return true;
const usageCard = getUsageCard();
if (!usageCard.length) {
log('Usage card not found. Not decorating.');
return false;
}
log('Usage card found. Decorating.', usageCard);
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');
const sig = $('<span>')
.addClass(sigCls)
.text('by monnef')
.attr('title', 'Enjoying this script? Consider a small donation.')
;
caption.append(sig);
usageCard.append(genHr(), caption);
addSettingsButton(caption);
sig.click(() => {
log('Signature clicked, showing donation modal');
$c(donationModalCls).show();
});
if ($c(donationModalCls).length === 0) {
$('body').append(createDonationModal());
}
// Add message for unset billing date
const paymentDay = GM_getValue('paymentDay');
if (!paymentDay) {
const message = $('<div>')
.addClass(getSettingsTextClassName())
.html('Billing date not set. Please set it in the <strong>Usage Tracker settings</strong> to see usage statistics (top right of this section).');
usageCard.append(
// $('<div>').addClass(hSpaceMdCls),
message
);
}
return true;
};
const addUsageTracker = () => {
const paymentDay = GM_getValue('paymentDay');
log('Checking payment day.');
if (!paymentDay) {
log('Payment day not set. Not adding tracker.');
return false;
}
log(`Payment day is set to: ${paymentDay}`);
const usageData = getUsageData();
if (usageData.used !== undefined && usageData.total !== undefined) {
log(`Retrieved usage data: ${JSON.stringify(usageData)}`);
displayTrackerData(usageData, paymentDay);
return true;
} else {
log('Failed to retrieve usage data.');
return false;
}
};
const getPremiumModelsLabel = () => $('span:contains("Premium models")');
/**
* @returns {{ used: number, total: number }}
*/
const getUsageData = () => {
log('Attempting to find usage data...');
const usageElement = getPremiumModelsLabel().siblings('span');
if (usageElement.length === 0) {
log('Usage element not found.');
return {};
}
const usageText = usageElement.text();
log(`Extracted 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);
log(`Parsed values - Used: ${used}, Total: ${total}`);
return { used, total };
} else {
log('Regex did not match the extracted text.');
return {};
}
};
/**
* @param {{ used: number, total: number }} usageData
* @param {number} paymentDay
* @returns {void}
*/
const displayTrackerData = (usageData, paymentDay) => {
const daysInfo = calculateDaysPassed({ today: new Date(), paymentDay });
const hueShift = 170;
if (daysInfo) {
const { premiumRequestsPerDay, remainingUsesPerDay } = calculateMetrics(usageData, daysInfo);
const progressBar = createProgressBar({
value: daysInfo.daysPassed,
max: daysInfo.totalDays,
label: 'Days',
rightLabel: `${daysInfo.daysPassed} / ${daysInfo.totalDays}`,
textMiddle: ' days passed out of ',
textAfter: ' days in the current billing cycle.',
hueShift,
});
const usageCard = getUsageCard();
usageCard.append(progressBar);
log('Progress bar added to the page');
// Create new progress bars for the new metrics
const premiumRequestsBar = createProgressBar({
value: premiumRequestsPerDay.toFixed(2),
max: (usageData.total / daysInfo.totalDays).toFixed(2),
label: 'Premium Requests/Day',
rightLabel: `${premiumRequestsPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`,
textBefore: 'So far you have used ',
textMiddle: ' requests per day on average.',
hideMax: true,
hueShift,
});
const remainingUsesBar = createProgressBar({
value: remainingUsesPerDay.toFixed(2),
max: (usageData.total / daysInfo.totalDays).toFixed(2),
label: 'Remaining Uses/Day',
rightLabel: `${remainingUsesPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`,
textBefore: 'You have ',
textMiddle: ' uses per day available for the remaining ',
textAfter: ` days.`,
overrideMax: `${daysInfo.totalDays - daysInfo.daysPassed}`,
hueShift,
});
// Append new progress bars to the usage card
usageCard.append(premiumRequestsBar, remainingUsesBar);
}
};
/**
* @param {{ used: number, total: number }} usageData
* @param {{ daysPassed: number, totalDays: number }} daysInfo
* @returns {{ premiumRequestsPerDay: number, remainingUsesPerDay: number }}
*/
const calculateMetrics = (usageData, daysInfo) => {
const premiumRequestsPerDay = daysInfo.daysPassed > 0
? (usageData.used / daysInfo.daysPassed)
: 0; // Avoid division by zero
const remainingUsesPerDay = (usageData.total - usageData.used) / (daysInfo.totalDays - daysInfo.daysPassed || 1); // Prevent division by zero
return { premiumRequestsPerDay, remainingUsesPerDay };
};
/**
* @param {{ today: Date, paymentDay: number }} params
* @returns {{ daysPassed: number, totalDays: number }}
*/
const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => {
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) {
log(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`);
}
return res;
};
const TEST_calculateDaysPassed = () => {
const testCases = [
{ today: new Date(2023, 9, 15), paymentDay: 15, expected: { daysPassed: 0, totalDays: 31 } },
{ today: new Date(2023, 9, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 31 } },
{ today: new Date(2023, 10, 1), paymentDay: 15, expected: { daysPassed: 17, totalDays: 31 } },
{ today: new Date(2023, 10, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 30 } },
{ today: new Date(2023, 9, 1), paymentDay: 15, expected: { daysPassed: 16, totalDays: 30 } },
{ today: new Date(2023, 12, 29), paymentDay: 2, expected: { daysPassed: 27, totalDays: 31 } },
];
return testCases.every(({ today, paymentDay, expected }) => {
const result = calculateDaysPassed({ today, paymentDay, disableLog: true });
const passed = result.daysPassed === expected.daysPassed && result.totalDays === expected.totalDays;
if (!passed) {
log(`Test failed for today: ${today.toDateString()}, paymentDay: ${paymentDay}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`);
}
return passed;
});
};
const TEST_calculateMetrics = () => {
const testCases = [
{ usageData: { used: 10, total: 100 }, daysInfo: { daysPassed: 5, totalDays: 30 }, expected: { premiumRequestsPerDay: 2, remainingUsesPerDay: 3.6 } },
{ usageData: { used: 0, total: 100 }, daysInfo: { daysPassed: 0, totalDays: 30 }, expected: { premiumRequestsPerDay: 0, remainingUsesPerDay: 3.3333333333333335 } },
{ usageData: { used: 50, total: 100 }, daysInfo: { daysPassed: 10, totalDays: 30 }, expected: { premiumRequestsPerDay: 5, remainingUsesPerDay: 2.5 } },
];
return testCases.every(({ usageData, daysInfo, expected }) => {
const result = calculateMetrics(usageData, daysInfo);
const passed = result.premiumRequestsPerDay === expected.premiumRequestsPerDay && result.remainingUsesPerDay === expected.remainingUsesPerDay;
if (!passed) {
log(`Test failed for usageData: ${JSON.stringify(usageData)}, daysInfo: ${JSON.stringify(daysInfo)}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`);
}
return passed;
});
};
const TEST = () => {
const tests = {
calculateDaysPassed: TEST_calculateDaysPassed(),
calculateMetrics: TEST_calculateMetrics(),
};
const allPassed = Object.values(tests).every(testRes => testRes);
if (allPassed) {
log('TEST: All tests passed successfully.', tests);
} else {
log('TEST: Some tests failed. Check the logs above for details.', tests);
}
};
/**
* @returns {string|null}
*/
const getClassNameByPrefix = prefix => {
const classes = $('[class]')
.map((_, element) => $(element).attr('class').match(new RegExp(`\\b${prefix}\\S+`, 'g')) || [])
.get();
if (classes.length === 0) {
log('No classes found');
return null;
}
const firstClass = classes[0];
if (classes.some(cls => cls !== firstClass)) {
error(`Classes are not the same: ${classes.join(', ')}`);
}
return firstClass;
};
/**
* @returns {string|null}
*/
const getSettingsTextClassName = () => '[&_b]:md:font-semibold font-mono text-sm/[0.875rem] tracking-4 md:text-sm/[1.25rem]';
/**
* @returns {string}
*/
const getInfoNumberClassName = () => 'font-bold inline';
/**
* @param {{ value: number, max: number, label: string, rightLabel: string, textBefore?: string, textMiddle?: string, textAfter?: string, hideMax?: boolean, overrideMax?: string, hueShift?: number }} params
* @returns {string}
*/
const createProgressBar = ({ value, max, label, rightLabel, textBefore = '', textMiddle = '', textAfter = '', hideMax = false, overrideMax, hueShift }) => {
const percentage = (value / max) * 100;
return `<div class='flex flex-col gap-1.5 flex-1'>
<div class='flex items-center justify-between gap-2'>
<div class='gt-standard-mono text-sm font-medium flex items-center gap-1'>
<span class='truncate'>${label}</span>
</div>
<div class='font-mono text-sm font-semibold truncate'>${rightLabel}</div>
</div>
<div class='w-full rounded-full bg-brand-borders overflow-hidden' style='height: 6px;'>
<div class='h-full' style='background-color: rgb(99, 161, 26); width: ${percentage.toFixed(2)}%; filter: hue-rotate(${hueShift ?? 0}deg);' title='${percentage.toFixed(0)}%'></div>
</div>
<div class='flex items-center gap-1'>
<div class='${getSettingsTextClassName()}'>
${textBefore}<span class='${getInfoNumberClassName()}'>${value}</span>${textMiddle}<span class='${getInfoNumberClassName()}' ${hideMax ? 'style="display: none;"' : ''}>${overrideMax ? overrideMax : max}</span>${textAfter}
</div>
</div>
</div>`;
};
/**
* @param {{ className: string, title: string, content: JQuery<HTMLElement> }} params
* @returns {JQuery<HTMLElement>}
*/
const createModal = ({ className, title, titleIconAfter, 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);
if (titleIconAfter) {
titleElement.append(
createLucideIcon({ iconName: titleIconAfter, size: '32px', invert: true })
.css({ marginLeft: '10px' })
);
}
;
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(); // Refresh to update tracker
} else {
errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show();
}
};
const saveButton = $('<button>')
.addClass(buttonCls)
.text('Save & Reload')
.click(saveAndReload);
// Add keypress event listener to the input
input.on('keypress', (e) => {
if (e.which === 13) { // 13 is the Enter key code
saveAndReload();
}
});
const madeByText = $('<p>').append(
'Made with ',
createLucideIcon({ iconName: 'heart', size: '14px', invert: true }),
' by monnef & Sonnet 3.5'
);
const content = $('<div>').append(
subtitle,
$('<div>').addClass(hSpaceSmCls),
input,
tip,
errorMessage,
$('<div>').addClass(hSpaceLgCls),
$('<div>').addClass(flexBetweenCls).append(madeByText, saveButton)
);
const modal = createModal({
className: settingsModalCls,
title: 'Usage Tracker Settings',
content: content
});
return modal;
};
const createDonationModal = () => {
const subtitle = $('<p>').text('Thank you for considering a donation! Your support is appreciated.');
const createCopyButton = (text, successMessage) => {
const button = $('<button>')
.addClass(buttonDarkCls)
.addClass(copyButtonCls)
.text('Copy');
button.click(async () => {
try {
await navigator.clipboard.writeText(text);
button.text(successMessage);
setTimeout(() => button.text('Copy'), 2000);
} catch (err) {
error('Clipboard write failed:', err);
button.text('Failed');
setTimeout(() => button.text('Copy'), 2000);
}
});
return button;
};
const addressText = $('<p>').text('Bitcoin Address:');
const addressInput = $('<input>')
.addClass(inputCls)
.addClass(inputWithButtonCls)
.attr('type', 'text')
.val(bitcoinAddress)
.prop('readonly', true);
const copyAddressButton = createCopyButton(bitcoinAddress, 'Copied!');
const paymentLinkText = $('<p>').text('Payment link:');
const paymentLinkInput = $('<input>')
.addClass(inputCls)
.addClass(inputWithButtonCls)
.attr('type', 'text')
.val(bitcoinPaymentLink)
.prop('readonly', true);
const copyPaymentLinkButton = createCopyButton(bitcoinPaymentLink, 'Copied!');
const content = $('<div>').append(
subtitle,
genHr(),
addressText,
addressInput,
copyAddressButton,
genHr(),
paymentLinkText,
paymentLinkInput,
copyPaymentLinkButton,
$('<div>').addClass(hSpaceMdCls)
);
return createModal({
className: donationModalCls,
title: 'Donate',
titleIconAfter: 'heart-handshake',
content: content
});
};
const createLucideIcon = ({ iconName, size = '16px', invert = false }) => {
return $('<img>')
.attr('src', `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`)
.css({
width: size,
height: size,
display: 'inline-block',
verticalAlign: 'text-bottom',
filter: invert ? 'invert(1)' : 'none'
});
};
const addSettingsButton = (mainCaption) => {
const settingsButton = $('<button>')
.css({
position: 'absolute',
top: '-10px',
right: '0px',
height: '29.4px',
width: '29.4px',
padding: '0px',
})
.addClass(buttonWhiteCls)
.attr('title', 'Usage Tracker settings')
.append(createLucideIcon({ iconName: 'settings' }));
settingsButton.click(() => {
log('Usage Tracker settings button clicked.');
$c(settingsModalCls).show();
const inputField = $c(settingsModalCls).find(`.${inputCls}`);
inputField.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');
}
};
const state = {
addingUsageTrackerSucceeded: false,
addingUsageTrackerAttempts: 0,
};
const ATTEMPTS_LIMIT = 10;
const ATTEMPTS_INTERVAL = 250;
const ATTEMPTS_MAX_DELAY = 3000;
/**
* @returns {void}
*/
const main = () => {
const scheduleNextAttempt = () => {
if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
const delay = Math.min(ATTEMPTS_INTERVAL * (2 ** (state.addingUsageTrackerAttempts - 1)), ATTEMPTS_MAX_DELAY);
log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. Retrying in ${delay}ms...`);
setTimeout(main, delay);
} else if (state.addingUsageTrackerSucceeded) {
log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} succeeded.`);
} else {
log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. No more attempts.`);
}
};
const decorationOkay = decorateUsageCard();
const paymentDay = GM_getValue('paymentDay');
if (decorationOkay && !paymentDay) {
log('Payment day not set. Not adding tracker.');
return;
}
if (!decorationOkay) {
log('Decoration failed. Try again later.');
scheduleNextAttempt();
state.addingUsageTrackerAttempts++;
return;
}
state.addingUsageTrackerSucceeded = addUsageTracker();
state.addingUsageTrackerAttempts++;
if (state.addingUsageTrackerAttempts === 1) {
const paymentDay = GM_getValue('paymentDay');
if (!paymentDay) {
log('Payment day not set. Not adding tracker.');
return;
}
}
scheduleNextAttempt();
};
const bitcoinAddress = 'bc1qr7crhydmp68qpa0gumuf2h6jcvdtta4wju49r7';
const bitcoinPaymentLink = `bitcoin:${bitcoinAddress}`;
$(document).ready(() => {
log('Script started');
unsafeWindow.ut = {
jq: $,
resetSettings: () => {
GM_setValue('paymentDay', undefined);
location.reload();
}
};
$('head').append($('<style>').text(styles));
$('body').append(createSettingsModal());
$('body').append(createDonationModal());
setTimeout(() => {
TEST();
main();
}, ATTEMPTS_INTERVAL);
});
})();