- // ==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.9
- // @description Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com, with consistent model colors and a legend sorted by usage.
- // @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');
- const legendCls = genCssId('legend');
- const legendItemCls = genCssId('legend-item');
- const legendColorBoxCls = genCssId('legend-color-box');
-
- const colors = {
- cursor: {
- lightGray: '#e5e7eb',
- gray: '#a7a9ac',
- grayDark: '#333333',
- },
- modelColorPalette: [
- '#FF6F61', '#4CAF50', '#2196F3', '#FFEB3B', '#9C27B0',
- '#FF9800', '#00BCD4', '#E91E63', '#8BC34A', '#3F51B5',
- '#CDDC39', '#673AB7', '#FFC107', '#009688', '#FF5722',
- '#795548', '#607D8B', '#9E9E9E', '#F44336', '#4DD0E1',
- '#FFB74D', '#BA68C8', '#AED581', '#7986CB', '#A1887F'
- ]
- };
-
- 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: 8px;
- background-color: ${colors.cursor.grayDark};
- border-radius: 9999px;
- margin: 10px 0;
- }
- .${barSegmentCls} {
- height: 100%;
- position: relative;
- transition: filter 0.2s ease-in-out;
- }
- .${barSegmentCls}:first-child { border-top-left-radius: 9999px; border-bottom-left-radius: 9999px; }
- .${barSegmentCls}:last-child { border-top-right-radius: 9999px; border-bottom-right-radius: 9999px; }
-
- .${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;
- }
- .${legendCls} {
- margin-top: 15px;
- padding: 10px;
- background-color: #1e1e1e;
- border-radius: 6px;
- display: flex;
- flex-wrap: wrap;
- gap: 8px 15px;
- }
- .${legendItemCls} {
- display: flex;
- align-items: center;
- font-size: 12px;
- color: ${colors.cursor.lightGray};
- }
- .${legendColorBoxCls} {
- width: 12px;
- height: 12px;
- margin-right: 6px;
- border: 1px solid #444;
- flex-shrink: 0;
- }
- `;
-
- 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');
-
- if (table.length === 0) {
- error("Recent Usage Events table: Not found or empty.");
- return { modelUsage, totalPaidRequests, totalRequests, erroredRequests };
- }
-
- 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;
- }
- });
- return { modelUsage, totalPaidRequests, totalRequests, erroredRequests };
- };
-
- const parseCurrentUsageCosts = (modelUsage) => {
- let overallTotalCost = 0;
- const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr');
-
- if (costTable.length === 0) {
- error("Current Usage (Cost) table: Not found or empty.");
- for (const modelKey in modelUsage) {
- if (!modelUsage[modelKey].hasOwnProperty('cost')) {
- modelUsage[modelKey].cost = 0;
- }
- }
- return { overallTotalCost };
- }
-
- for (const modelKey in modelUsage) {
- modelUsage[modelKey].cost = 0;
- }
- if (!modelUsage['Extra/Other Premium']) {
- modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 };
- }
- if (!modelUsage['Other Costs']) {
- modelUsage['Other Costs'] = { count: 0, cost: 0 };
- }
-
- 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;
- overallTotalCost += cost;
- if (cost <= 0 && description.includes('paid for')) {
- return;
- }
-
- let foundModel = false;
- for (const modelKey in modelUsage) {
- if (modelKey === 'Extra/Other Premium' || modelKey === 'Other Costs') continue;
- if (description.includes(modelKey.toLowerCase())) {
- modelUsage[modelKey].cost += cost;
- foundModel = true;
- }
- }
-
- if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) {
- modelUsage['Extra/Other Premium'].cost += cost;
- foundModel = true;
- }
- if (!foundModel && cost > 0) {
- modelUsage['Other Costs'].cost += cost;
- }
- });
-
- if (modelUsage['Extra/Other Premium'] && modelUsage['Extra/Other Premium'].cost === 0 && modelUsage['Extra/Other Premium'].count === 0) {
- delete modelUsage['Extra/Other Premium'];
- }
- if (modelUsage['Other Costs'] && modelUsage['Other Costs'].cost === 0 && modelUsage['Other Costs'].count === 0) {
- delete modelUsage['Other Costs'];
- }
-
- return { overallTotalCost };
- };
-
- const getBaseUsageData = () => {
- const premiumLabel = $('span:contains("Premium models")').first();
- if (premiumLabel.length === 0) {
- return {};
- }
- const usageSpan = premiumLabel.siblings('span').last();
- const usageText = usageSpan.text();
- const regex = /(\d+) \/ (\d+)/;
- const matches = usageText.match(regex);
- if (matches && matches.length === 3) {
- return { used: parseInt(matches[1], 10), total: parseInt(matches[2], 10) };
- }
- return {};
- };
-
- // --- Display Functions ---
-
- const createGenericProgressBar = (modelUsage, weightField, modelToColorMap) => {
- const barContainer = $('<div>').addClass(multiBarCls);
- const totalWeight = Object.values(modelUsage)
- .reduce((sum, model) => sum + (model[weightField] || 0), 0);
-
- if (totalWeight === 0) {
- return barContainer.text(`No data for ${weightField}-weighted bar.`).css({ height: 'auto', padding: '5px' });
- }
-
- const sortedModels = Object.entries(modelUsage)
- .filter(([_, data]) => (data[weightField] || 0) > 0)
- .sort(([, a], [, b]) => (b[weightField] || 0) - (a[weightField] || 0));
-
- sortedModels.forEach((entry) => {
- const [model, data] = entry;
- const percentage = (data[weightField] / totalWeight) * 100;
- const reqCount = (data.count || 0).toFixed(1);
- const costAmount = (data.cost || 0).toFixed(2);
- const color = modelToColorMap[model] || colors.cursor.gray;
-
- const tooltipText = `${model}: ${reqCount} reqs ($${costAmount})`;
- const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText);
- const segment = $('<div>')
- .addClass(barSegmentCls)
- .css({ width: `${percentage}%`, backgroundColor: color })
- .append(tooltip);
- barContainer.append(segment);
- });
- return barContainer;
- };
-
- const createLegend = (modelToColorMap, modelUsage) => {
- const legendContainer = $('<div>').addClass(legendCls);
-
- // Get models that are in the color map (meaning they have some usage/cost)
- const modelsInLegend = Object.keys(modelToColorMap);
-
- // Sort these models by their usage count (descending)
- const sortedModelsForLegend = modelsInLegend.sort((modelA, modelB) => {
- const countA = modelUsage[modelA]?.count || 0;
- const countB = modelUsage[modelB]?.count || 0;
- return countB - countA; // Sort descending by request count
- });
-
- for (const model of sortedModelsForLegend) {
- const color = modelToColorMap[model];
- const count = (modelUsage[model]?.count || 0).toFixed(1);
- const cost = (modelUsage[model]?.cost || 0).toFixed(2);
-
- const colorBox = $('<span>')
- .addClass(legendColorBoxCls)
- .css('background-color', color);
- const legendItem = $('<div>')
- .addClass(legendItemCls)
- .append(colorBox)
- .append(document.createTextNode(`${model}`));
- legendContainer.append(legendItem);
- }
- return legendContainer;
- };
-
- const displayEnhancedTrackerData = () => {
- debug('displayEnhancedTrackerData: Function START');
- const tracker = $c(mainCaptionCls);
- if (tracker.length === 0) {
- error('displayEnhancedTrackerData: Main caption element NOT FOUND.');
- return false;
- }
- debug(`displayEnhancedTrackerData: Found tracker caption element.`);
- tracker.siblings(`.${enhancedTrackerContainerCls}`).remove();
-
- const { modelUsage, totalPaidRequests, totalRequests, erroredRequests } = parseUsageEventsTable();
- const { overallTotalCost } = parseCurrentUsageCosts(modelUsage);
- const baseUsage = getBaseUsageData();
-
- const modelToColorMap = {};
- let colorPaletteIndex = 0;
- const uniqueModelsInUsage = Object.keys(modelUsage)
- .filter(modelName => (modelUsage[modelName].count || 0) > 0 || (modelUsage[modelName].cost || 0) > 0);
-
- for (const modelName of uniqueModelsInUsage) {
- modelToColorMap[modelName] = colors.modelColorPalette[colorPaletteIndex % colors.modelColorPalette.length];
- colorPaletteIndex++;
- }
- debug('displayEnhancedTrackerData: modelToColorMap built:', modelToColorMap);
-
- 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('Usage-Based Weighted Requests', totalPaidRequests.toFixed(1));
- addStat('Plan Premium Requests', baseUsage.used !== undefined ? `${baseUsage.used} / ${baseUsage.total || 'N/A'}` : 'N/A');
- addStat('Total Weighted Requests (Events)', totalRequests.toFixed(1));
- addStat('Errored Requests (Events)', erroredRequests);
- addStat('Total Billed Cost (This Cycle)', `$${overallTotalCost.toFixed(2)}`);
-
-
- container.append(statsContainer);
- container.append($('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '15px', marginBottom: '2px' })
- .text('Model Usage Breakdown (Weighted by Requests):')
- );
- container.append(createGenericProgressBar(modelUsage, 'count', modelToColorMap));
-
-
- if (Object.keys(modelToColorMap).length > 0) {
- // Pass modelUsage to createLegend to access counts for sorting and display
- container.append(createLegend(modelToColorMap, 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...');
- window.ut = {
- jq: $,
- parseEvents: parseUsageEventsTable,
- parseCosts: parseCurrentUsageCosts,
- getBase: getBaseUsageData
- };
- $('head').append($('<style>').text(styles));
- setTimeout(main, ATTEMPTS_INTERVAL);
- });
-
- })();