Cursor.com Usage Tracker (Enhanced)

Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com.

目前為 2025-05-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();