// ==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
// @namespace http://monnef.eu
// @version 0.5.3
// @description Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com.
// @match https://www.cursor.com/settings
// @grant none
// @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);
$.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 mainCaptionCls = genCssId('main-caption');
const hrCls = genCssId('hr');
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 enhancedTrackerContainerCls = genCssId('enhanced-tracker'); // Specific class for the container
const colors = {
cursor: {
lightGray: '#e5e7eb',
gray: '#a7a9ac',
grayDark: '#333333',
},
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 = `
.${hrCls} { border: 0; height: 1px; background-color: #333333; margin: 15px 0; }
.${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; 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;
bottom: 150%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
border: 1px solid ${colors.cursor.gray};
font-size: 12px;
pointer-events: none;
}
.${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);
// --- Data Parsing Functions ---
const parseUsageEventsTable = () => {
const modelUsage = {};
let totalPaidRequests = 0;
let totalRequests = 0;
let erroredRequests = 0;
const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
// debug(`Parsing Usage Events Table: Found ${table.length} rows.`);
if (table.length === 0) {
error("Recent Usage Events table: Not found or empty.");
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) {
totalRequests += requests;
if (!modelUsage[model]) {
modelUsage[model] = { count: 0, cost: 0 };
}
modelUsage[model].count += requests;
if (status === 'Usage-based') {
totalPaidRequests += requests;
}
} else if (status === 'Errored, Not Charged') {
erroredRequests += 1;
}
});
// debug('Finished Parsing Usage Events:', { modelUsage, totalPaidRequests, totalRequests });
return { modelUsage, totalPaidRequests, totalRequests, erroredRequests };
};
const parseCurrentUsageCosts = (modelUsage) => {
let totalCost = 0;
const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr');
// debug(`Parsing Current Usage Costs Table: Found ${costTable.length} rows.`);
if (costTable.length === 0) {
error("Current Usage (Cost) table: Not found or empty.");
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;
let foundModel = false;
for (const modelKey in modelUsage) { // Use modelKey to avoid conflict with 'model' variable
if (description.includes(modelKey.toLowerCase())) {
modelUsage[modelKey].cost += cost;
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; // Count this as found for "Other Costs" logic
}
if (!foundModel && cost > 0 && !description.includes('mid-month usage paid')) { // Avoid double-adding "Other Costs" for positive values if it was already an "Extra/Other Premium"
if (!modelUsage['Other Costs']) {
modelUsage['Other Costs'] = { count: 0, cost: 0 };
}
modelUsage['Other Costs'].cost += cost;
}
});
for (const modelKey in modelUsage) { // Use modelKey
modelUsage[modelKey].cost = modelUsage[modelKey].cost || 0;
}
// debug('Finished Parsing Costs & Updated Model Usage:', { modelUsage, totalCost });
return { totalCost };
};
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 ---
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;
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;
};
const displayEnhancedTrackerData = () => {
debug('displayEnhancedTrackerData: Function START');
const tracker = $c(mainCaptionCls);
if (tracker.length === 0) {
error('displayEnhancedTrackerData: Main caption element NOT FOUND. Cannot display stats.');
return false;
}
debug(`displayEnhancedTrackerData: Found tracker caption element (length: ${tracker.length}). Class: ${mainCaptionCls}`);
// Clear previous tracker data
const existingTrackerData = tracker.siblings(`.${enhancedTrackerContainerCls}`);
debug(`displayEnhancedTrackerData: Found ${existingTrackerData.length} existing tracker data containers to remove.`);
existingTrackerData.remove();
debug('displayEnhancedTrackerData: Calling parsing functions...');
const { modelUsage, totalPaidRequests, totalRequests, erroredRequests } = parseUsageEventsTable();
parseCurrentUsageCosts(modelUsage);
const baseUsage = getBaseUsageData();
debug('displayEnhancedTrackerData: Parsing functions COMPLETE.');
const container = $('<div>').addClass(enhancedTrackerContainerCls);
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('Weighted Usage-Based Requests', totalPaidRequests.toFixed(1));
addStat('Total Requests', (totalRequests+baseUsage.used).toFixed(1));
addStat('Errored Requests', (erroredRequests).toFixed(1));
container.append(
statsContainer,
$('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '10px' }).text('Model Usage Breakdown (Weighted, Hover for details):'),
createMultiSegmentProgressBar(modelUsage)
);
debug('displayEnhancedTrackerData: Stats container PREPARED. Appending to DOM...');
tracker.after(container);
debug('displayEnhancedTrackerData: Enhanced tracker data supposedly displayed.');
return true;
};
// --- Core Script Functions ---
const decorateUsageCard = () => {
debug("decorateUsageCard: Function START");
if ($c(mainCaptionCls).length > 0) {
debug("decorateUsageCard: Card already decorated.");
return true;
}
const usageHeading = $('h2:contains("Usage")');
if (usageHeading.length > 0) {
debug(`decorateUsageCard: Found 'h2:contains("Usage")' (count: ${usageHeading.length}). Decorating...`);
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);
debug(`decorateUsageCard: Added tracker caption. Check DOM for class '${mainCaptionCls}'. Current count in DOM: ${$c(mainCaptionCls).length}`);
return true;
}
debug("decorateUsageCard: 'h2:contains(\"Usage\")' NOT FOUND.");
return false;
};
const addUsageTracker = () => {
debug("addUsageTracker: Function START");
const success = displayEnhancedTrackerData();
debug(`addUsageTracker: displayEnhancedTrackerData returned ${success}`);
return success;
};
// --- Main Execution Logic ---
const state = {
addingUsageTrackerSucceeded: false,
addingUsageTrackerAttempts: 0,
};
const ATTEMPTS_LIMIT = 15;
const ATTEMPTS_INTERVAL = 750;
const ATTEMPTS_MAX_DELAY = 5000;
const main = () => {
state.addingUsageTrackerAttempts++;
log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts}...`);
const scheduleNextAttempt = () => {
if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) {
const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY);
log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} incomplete/failed. Retrying in ${delay}ms...`);
setTimeout(main, delay);
} else if (state.addingUsageTrackerSucceeded) {
log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} SUCCEEDED.`);
} else {
error(`Main Execution: All ${ATTEMPTS_LIMIT} attempts FAILED. Could not add Usage Tracker.`);
}
};
debug("Main Execution: Calling decorateUsageCard...");
const decorationOkay = decorateUsageCard();
debug(`Main Execution: decorateUsageCard returned ${decorationOkay}`);
if (!decorationOkay) {
scheduleNextAttempt();
return;
}
debug("Main Execution: Checking for Recent Usage Events table...");
const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr');
if (usageEventsTable.length === 0) {
debug("Main Execution: 'Recent Usage Events' table NOT FOUND YET.");
scheduleNextAttempt();
return;
}
debug(`Main Execution: 'Recent Usage Events' table FOUND (${usageEventsTable.length} rows).`);
debug("Main Execution: Attempting to add/update tracker UI via addUsageTracker...");
try {
state.addingUsageTrackerSucceeded = addUsageTracker();
} catch (e) {
error("Main Execution: CRITICAL ERROR during addUsageTracker call:", e);
state.addingUsageTrackerSucceeded = false;
}
debug(`Main Execution: addUsageTracker process finished. Success: ${state.addingUsageTrackerSucceeded}`);
scheduleNextAttempt();
};
$(document).ready(() => {
log('Document ready. Script starting...');
// For manual debugging: assign to window instead of unsafeWindow for @grant none
window.ut = {
jq: $,
parseEvents: parseUsageEventsTable,
parseCosts: parseCurrentUsageCosts,
getBase: getBaseUsageData
};
$('head').append($('<style>').text(styles));
setTimeout(main, ATTEMPTS_INTERVAL);
});
})();