Claude - Plan usage limit

Monitors the Claude usage page and notifies on every 5% increase in plan consumption.

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.

Tendrás que 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.

Tendrás que instalar una extensión como Tampermonkey antes de poder 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)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

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

// ==UserScript==
// @name         Claude - Plan usage limit
// @namespace    https://claude.ai/
// @version      1.6.3
// @description  Monitors the Claude usage page and notifies on every 5% increase in plan consumption.
// @license      MIT
// @licence      MIT
// @author       [email protected]
// @match        https://claude.ai/*
// @run-at       document-idle
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

/* =============================================================================
 * Greasy Fork description (paste the block below into the script page on
 * https://greasyfork.org/fr/scripts/576225-claude-plan-usage-limit):
 * -----------------------------------------------------------------------------
 *
 * Monitors the Claude usage page (claude.ai/settings/usage) and sends a
 * notification each time plan consumption crosses a 5 % threshold
 * (5, 10, 15, 20 ... %).
 *
 * ## Features
 *
 * - Auto-detects French and English UI on the Claude page; notification text
 *   follows `navigator.language`.
 * - Uses `GM_notification` with a unique `tag` so a new threshold replaces the
 *   previous notification instead of stacking up.
 * - Stays armed even when the browser window is minimized: polling runs inside
 *   a Web Worker to bypass background-tab timer throttling.
 * - Detects SPA navigation (`pushState` / `replaceState` / `popstate`), so the
 *   notification fires as soon as you land on `/settings/usage`, even from
 *   another route on claude.ai.
 * - Optional sound cue (880 Hz, ~400 ms, generated via the Web Audio API),
 *   toggled from the Violentmonkey / Tampermonkey extension menu - useful on
 *   Firefox where OS notifications are short-lived.
 * - Persistent settings via `GM_setValue` (no `localStorage` pollution).
 *
 * ## Permissions
 *
 * - `GM_notification` - desktop notifications
 * - `GM_registerMenuCommand` - sound toggle in the extension menu
 * - `GM_setValue` / `GM_getValue` - persistent settings
 *
 * ## Privacy
 *
 * The script reads the percentage and reset countdown directly from the DOM of
 * the official Claude usage page. No network request, no third-party service,
 * no telemetry.
 *
 * ## Compatibility
 *
 * Tested on Firefox + Violentmonkey and Chrome + Tampermonkey.
 *
 * - Chrome + Tampermonkey: fully persistent OS notifications (kept until
 *   clicked).
 * - Chrome + Violentmonkey: persistent thanks to the `onclick` parameter.
 * - Firefox + Violentmonkey: notifications fire but are short-lived (Firefox
 *   limitation, Bugzilla #1187270). Enable the sound option for a reliable
 *   signal when the window is minimized.
 *
 * ============================================================================= */

(function () {
    'use strict';

    const USAGE_PATH_REGEX = /^\/settings\/usage(?:\/|$)/;
    const LABELS = ['Plan usage limits', "Limites d'utilisation du forfait"];
    const PERCENT_REGEX = /(\d+(?:[.,]\d+)?)\s*%/;
    const RESET_REGEX = /(?:Resets(?:\s+in)?|Réinitialisation(?:\s+dans)?)\s+(.+)/;
    const TEST_MODE = false;
    const THRESHOLD = 5;
    const POLL_INTERVAL_MS = TEST_MODE ? 15000 : 30000;
    const INITIAL_TIMEOUT_MS = 15000;
    const INITIAL_POLL_MS = 3000;
    const STORAGE_KEY = 'claude-usage-last-notified-percent';
    const BEEP_STORAGE_KEY = 'claude-usage-beep-enabled';

    const LANG = (navigator.language || 'en').toLowerCase().startsWith('fr') ? 'fr' : 'en';
    const I18N = {
        en: {
            timeLocale: 'en-US',
            usedTitle: pct => `Claude - ${pct}% used`,
            usedTitleTest: pct => `Claude - ${pct}% used [TEST]`,
            resetsIn: time => `Resets in ${time}`,
            planLimit: 'Plan usage limit',
            initialState: 'Initial state',
            thresholdReached: pct => `Threshold reached: ${pct}%`,
            resetDetected: 'Reset detected',
            belowNext: 'Below next threshold',
            testModePrefix: reason => `TEST_MODE (${reason})`,
            notFoundWarn: (labels, sec) => `[Claude Usage] Could not find ${labels} on the page after ${sec}s.`,
            notifyFired: (title, body) => `Notification fired: "${title}" - ${body}`,
            beepLabel: 'Sound on notification'
        },
        fr: {
            timeLocale: 'fr-FR',
            usedTitle: pct => `Claude - ${pct} % utilisé`,
            usedTitleTest: pct => `Claude - ${pct} % utilisé [TEST]`,
            resetsIn: time => `Réinitialisation dans ${time}`,
            planLimit: "Limite d'utilisation du forfait",
            initialState: 'État initial',
            thresholdReached: pct => `Seuil atteint : ${pct} %`,
            resetDetected: 'Réinitialisation détectée',
            belowNext: 'Sous le prochain seuil',
            testModePrefix: reason => `MODE TEST (${reason})`,
            notFoundWarn: (labels, sec) => `[Claude Usage] Impossible de trouver ${labels} sur la page après ${sec} s.`,
            notifyFired: (title, body) => `Notification déclenchée : "${title}" - ${body}`,
            beepLabel: 'Son à la notification'
        }
    };
    const t = I18N[LANG];

    const LOGO_DATA_URI = 'data:image/png;base64,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';

    function findSection() {
        const span = [...document.querySelectorAll('span')]
            .find(s => LABELS.includes(s.textContent.trim()));
        return span ? span.closest('section') : null;
    }

    function findResetText(section) {
        const walker = document.createTreeWalker(section, NodeFilter.SHOW_TEXT);
        let node;
        while ((node = walker.nextNode())) {
            const txt = (node.nodeValue || '').trim();
            const match = txt.match(RESET_REGEX);
            if (match) return match[1].trim();
        }
        return null;
    }

    function extract() {
        const section = findSection();
        if (!section) return null;

        const percentMatch = section.textContent.match(PERCENT_REGEX);
        if (!percentMatch) return null;

        return {
            percent: parseFloat(percentMatch[1].replace(',', '.')),
            reset: findResetText(section)
        };
    }

    function readLastNotified() {
        const value = GM_getValue(STORAGE_KEY, null);
        return Number.isFinite(value) ? value : null;
    }

    function writeLastNotified(value) {
        GM_setValue(STORAGE_KEY, value);
    }

    function logResult({ percent, reset }, prefix) {
        const stamp = new Date().toLocaleTimeString(t.timeLocale);
        const resetPart = reset ? ` - ${t.resetsIn(reset)}` : '';
        console.log(
            `%c[Claude Usage] ${stamp} - ${prefix}: ${percent}%${resetPart}`,
            'color:#c96442;font-weight:bold;'
        );
    }

    function isBeepEnabled() {
        return GM_getValue(BEEP_STORAGE_KEY, false);
    }

    function setBeepEnabled(enabled) {
        GM_setValue(BEEP_STORAGE_KEY, !!enabled);
    }

    const BEEP_MENU_ID = 'claude-usage-beep-toggle';
    function refreshBeepMenu() {
        const caption = `${t.beepLabel} : ${isBeepEnabled() ? '✓' : '✗'}`;
        GM_registerMenuCommand(caption, () => {
            setBeepEnabled(!isBeepEnabled());
            refreshBeepMenu();
        }, { id: BEEP_MENU_ID });
    }

    function playBeep() {
        if (!isBeepEnabled()) return;
        try {
            const Ctx = window.AudioContext || window.webkitAudioContext;
            if (!Ctx) return;
            const ctx = new Ctx();
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sine';
            osc.frequency.value = 880;
            osc.connect(gain).connect(ctx.destination);
            const t0 = ctx.currentTime;
            gain.gain.setValueAtTime(0.001, t0);
            gain.gain.exponentialRampToValueAtTime(0.25, t0 + 0.02);
            gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.4);
            osc.start(t0);
            osc.stop(t0 + 0.45);
            osc.onended = () => ctx.close();
        } catch (e) {
            console.warn('[Claude Usage] Beep failed:', e);
        }
    }

    function notify({ percent, reset }, title) {
        const body = reset ? t.resetsIn(reset) : t.planLimit;
        console.log(
            `%c[Claude Usage] ${t.notifyFired(title, body)}`,
            'color:#c96442;font-weight:bold;'
        );
        playBeep();

        if (typeof GM_notification === 'function') {
            GM_notification({
                title,
                text: body,
                image: LOGO_DATA_URI,
                tag: 'claude-usage',
                silent: false,
                zombieTimeout: 60000,
                onclick: () => { try { window.focus(); } catch (e) { /* noop */ } }
            });
            return;
        }

        if (typeof Notification === 'undefined') return;

        const show = () => new Notification(title, { body, icon: LOGO_DATA_URI });
        if (Notification.permission === 'granted') {
            show();
        } else if (Notification.permission !== 'denied') {
            Notification.requestPermission().then(p => { if (p === 'granted') show(); });
        }
    }

    function bucket(percent) {
        return Math.floor(percent / THRESHOLD) * THRESHOLD;
    }

    function check(reason) {
        const result = extract();
        if (!result) return false;

        const currentBucket = bucket(result.percent);
        const last = readLastNotified();

        if (TEST_MODE) {
            logResult(result, t.testModePrefix(reason));
            notify(result, t.usedTitleTest(result.percent));
            writeLastNotified(currentBucket);
            return true;
        }

        if (reason === 'initial') {
            logResult(result, t.initialState);
            notify(result, t.usedTitle(result.percent));
            writeLastNotified(currentBucket);
            return true;
        }

        if (last === null || currentBucket > last) {
            logResult(result, t.thresholdReached(currentBucket));
            notify(result, t.usedTitle(result.percent));
            writeLastNotified(currentBucket);
            return true;
        }

        if (currentBucket < last) {
            logResult(result, t.resetDetected);
            notify(result, t.usedTitle(result.percent));
            writeLastNotified(currentBucket);
            return true;
        }

        if (reason === 'manual') {
            logResult(result, t.belowNext);
        }
        return true;
    }

    let pollWorker = null;
    let bootstrapTimer = null;

    function stopTimers() {
        if (pollWorker) { pollWorker.terminate(); pollWorker = null; }
        if (bootstrapTimer) { clearInterval(bootstrapTimer); bootstrapTimer = null; }
    }

    function startMonitoring() {
        const workerCode = 'setInterval(() => self.postMessage("tick"), ' + POLL_INTERVAL_MS + ');';
        const blobUrl = URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }));
        pollWorker = new Worker(blobUrl);
        URL.revokeObjectURL(blobUrl);
        pollWorker.onmessage = () => {
            if (!isOnUsagePage()) { stopTimers(); return; }
            check('poll');
        };
    }

    function isOnUsagePage() {
        return USAGE_PATH_REGEX.test(location.pathname);
    }

    function bootstrap() {
        stopTimers();
        if (check('initial')) {
            startMonitoring();
            return;
        }
        const start = Date.now();
        bootstrapTimer = setInterval(() => {
            if (!isOnUsagePage()) { stopTimers(); return; }
            if (check('initial')) {
                clearInterval(bootstrapTimer);
                bootstrapTimer = null;
                startMonitoring();
            } else if (Date.now() - start > INITIAL_TIMEOUT_MS) {
                clearInterval(bootstrapTimer);
                bootstrapTimer = null;
                console.warn(t.notFoundWarn(LABELS.map(l => `"${l}"`).join(' / '), INITIAL_TIMEOUT_MS / 1000));
            }
        }, INITIAL_POLL_MS);
    }

    function onUrlChange() {
        if (isOnUsagePage()) {
            bootstrap();
        } else {
            stopTimers();
        }
    }

    ['pushState', 'replaceState'].forEach(method => {
        const original = history[method];
        history[method] = function (...args) {
            const result = original.apply(this, args);
            window.dispatchEvent(new Event('claude-usage-locationchange'));
            return result;
        };
    });
    window.addEventListener('popstate', () => window.dispatchEvent(new Event('claude-usage-locationchange')));
    window.addEventListener('claude-usage-locationchange', onUrlChange);

    refreshBeepMenu();
    onUrlChange();
})();