Cursor.com Usage Tracker (Enhanced)

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.

Versión del día 27/05/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
    });

})();