Claude - Plan usage limit

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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