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