#if os(macOS) let openAIDashboardScrapeScript = """ (() => { const textOf = el => { const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : ''; return raw.trim(); }; const parseHexColor = (color) => { if (!color) return null; const c = String(color).trim().toLowerCase(); if (c.startsWith('#')) { if (c.length === 4) { return '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3]; } if (c.length === 7) return c; return c; } const m = c.match(/^rgba?\\(([^)]+)\\)$/); if (m) { const parts = m[1].split(',').map(x => parseFloat(x.trim())).filter(x => Number.isFinite(x)); if (parts.length >= 3) { const r = Math.max(0, Math.min(255, Math.round(parts[0]))); const g = Math.max(0, Math.min(255, Math.round(parts[1]))); const b = Math.max(0, Math.min(255, Math.round(parts[2]))); const toHex = n => n.toString(16).padStart(2, '0'); return '#' + toHex(r) + toHex(g) + toHex(b); } } return c; }; const reactPropsOf = (el) => { if (!el) return null; try { const keys = Object.keys(el); const propsKey = keys.find(k => k.startsWith('__reactProps$')); if (propsKey) return el[propsKey] || null; const fiberKey = keys.find(k => k.startsWith('__reactFiber$')); if (fiberKey) { const fiber = el[fiberKey]; return (fiber && (fiber.memoizedProps || fiber.pendingProps)) || null; } } catch {} return null; }; const reactFiberOf = (el) => { if (!el) return null; try { const keys = Object.keys(el); const fiberKey = keys.find(k => k.startsWith('__reactFiber$')); return fiberKey ? (el[fiberKey] || null) : null; } catch { return null; } }; const nestedBarMetaOf = (root) => { if (!root || typeof root !== 'object') return null; const queue = [root]; const seen = typeof WeakSet !== 'undefined' ? new WeakSet() : null; let steps = 0; while (queue.length && steps < 250) { const cur = queue.shift(); steps++; if (!cur || typeof cur !== 'object') continue; if (seen) { if (seen.has(cur)) continue; seen.add(cur); } if (cur.payload && (cur.dataKey || cur.name || cur.value !== undefined)) return cur; const values = Array.isArray(cur) ? cur : Object.values(cur); for (const v of values) { if (v && typeof v === 'object') queue.push(v); } } return null; }; const barMetaFromElement = (el) => { const direct = reactPropsOf(el); if (direct && direct.payload && (direct.dataKey || direct.name || direct.value !== undefined)) return direct; const fiber = reactFiberOf(el); if (fiber) { let cur = fiber; for (let i = 0; i < 10 && cur; i++) { const props = (cur.memoizedProps || cur.pendingProps) || null; if (props && props.payload && (props.dataKey || props.name || props.value !== undefined)) return props; const nested = props ? nestedBarMetaOf(props) : null; if (nested) return nested; cur = cur.return || null; } } if (direct) { const nested = nestedBarMetaOf(direct); if (nested) return nested; } return null; }; const normalizeHref = (raw) => { if (!raw) return null; const href = String(raw).trim(); if (!href) return null; if (href.startsWith('http://') || href.startsWith('https://')) return href; if (href.startsWith('//')) return window.location.protocol + href; if (href.startsWith('/')) return window.location.origin + href; return window.location.origin + '/' + href; }; const isLikelyCreditsURL = (raw) => { if (!raw) return false; try { const url = new URL(raw, window.location.origin); if (!url.host || !url.host.includes('chatgpt.com')) return false; const path = url.pathname.toLowerCase(); return ( path.includes('settings') || path.includes('usage') || path.includes('billing') || path.includes('credits') ); } catch { return false; } }; const purchaseTextMatches = (text) => { const lower = String(text || '').trim().toLowerCase(); if (!lower) return false; if (lower.includes('add more')) return true; if (!lower.includes('credit')) return false; return ( lower.includes('buy') || lower.includes('add') || lower.includes('purchase') || lower.includes('top up') || lower.includes('top-up') ); }; const elementLabel = (el) => { if (!el) return ''; return ( textOf(el) || el.getAttribute('aria-label') || el.getAttribute('title') || '' ); }; const urlFromProps = (props) => { if (!props || typeof props !== 'object') return null; const candidates = [ props.href, props.to, props.url, props.link, props.destination, props.navigateTo ]; for (const candidate of candidates) { if (typeof candidate === 'string' && candidate.trim()) { return normalizeHref(candidate); } } return null; }; const purchaseURLFromElement = (el) => { if (!el) return null; const isAnchor = el.tagName && el.tagName.toLowerCase() === 'a'; const anchor = isAnchor ? el : (el.closest ? el.closest('a') : null); const anchorHref = anchor ? anchor.getAttribute('href') : null; const dataHref = el.getAttribute ? (el.getAttribute('data-href') || el.getAttribute('data-url') || el.getAttribute('data-link') || el.getAttribute('data-destination')) : null; const propHref = urlFromProps(reactPropsOf(el)) || urlFromProps(reactPropsOf(anchor)); const normalized = normalizeHref(anchorHref || dataHref || propHref); return normalized && isLikelyCreditsURL(normalized) ? normalized : null; }; const cleanPlanName = (raw) => String(raw || '') .replace(/\\b(claude|codex|account|plan)\\b/gi, ' ') .replace(/_/g, ' ') .replace(/-/g, ' ') .replace(/\\s+/g, ' ') .trim(); const codexPlanDisplayName = (raw) => { const trimmed = String(raw || '').trim(); if (!trimmed) return null; const lower = trimmed.toLowerCase(); const exact = { pro: 'Pro 20x', prolite: 'Pro 5x', 'pro_lite': 'Pro 5x', 'pro-lite': 'Pro 5x', 'pro lite': 'Pro 5x' }; if (exact[lower]) return exact[lower]; const cleaned = cleanPlanName(trimmed); if (!cleaned) return trimmed; if (exact[cleaned.toLowerCase()]) return exact[cleaned.toLowerCase()]; return cleaned.split(' ') .filter(Boolean) .map(word => { const wordLower = word.toLowerCase(); if (wordLower === 'cbp' || wordLower === 'k12') return wordLower.toUpperCase(); if (word === word.toUpperCase() && /[a-z]/i.test(word)) return word; return word.charAt(0).toUpperCase() + word.slice(1); }) .join(' ') || cleaned; }; const normalizePlanValue = (value) => { const trimmed = String(value || '').trim(); if (!trimmed) return null; const lower = trimmed.toLowerCase(); const allowed = [ 'free', 'plus', 'pro', 'team', 'enterprise', 'business', 'edu', 'education', 'gov', 'premium', 'essential' ]; if (!allowed.some(token => lower.includes(token))) return null; return codexPlanDisplayName(trimmed) || cleanPlanName(trimmed); }; const planCandidate = (key, value) => { const lower = String(key || '').toLowerCase(); if (!lower.includes('plan') && !lower.includes('tier') && !lower.includes('subscription')) return null; if (typeof value === 'string') return normalizePlanValue(value); if (value && typeof value === 'object' && !Array.isArray(value)) { return normalizePlanValue(value.name) || normalizePlanValue(value.displayName) || normalizePlanValue(value.tier); } return null; }; const findPlan = (root) => { if (!root || typeof root !== 'object') return null; const queue = [root]; const seenObjects = typeof WeakSet !== 'undefined' ? new WeakSet() : null; let index = 0; let seen = 0; while (index < queue.length && seen < 6000) { const cur = queue[index++]; seen++; if (!cur || typeof cur !== 'object') continue; if (seenObjects) { if (seenObjects.has(cur)) continue; seenObjects.add(cur); } if (Array.isArray(cur)) { for (const v of cur) { if (v && typeof v === 'object') queue.push(v); } continue; } for (const [k, v] of Object.entries(cur)) { const plan = planCandidate(k, v); if (plan) return plan; if (v && typeof v === 'object') queue.push(v); } } return null; }; const parseJSONScript = (id) => { try { const node = document.getElementById(id); const raw = node && node.textContent ? String(node.textContent) : ''; return raw ? JSON.parse(raw) : null; } catch { return null; } }; const pickLikelyPurchaseButton = (buttons) => { if (!buttons || buttons.length === 0) return null; const labeled = buttons.find(btn => { const label = elementLabel(btn); if (purchaseTextMatches(label)) return true; const aria = String(btn.getAttribute('aria-label') || '').toLowerCase(); return aria.includes('credit') || aria.includes('buy') || aria.includes('add'); }); return labeled || buttons[0]; }; const findCreditsPurchaseButton = () => { const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p')); const labelMatch = nodes.find(node => { const lower = textOf(node).toLowerCase(); return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining')); }); if (!labelMatch) return null; let cur = labelMatch; for (let i = 0; i < 6 && cur; i++) { const buttons = Array.from(cur.querySelectorAll('button, a')); const picked = pickLikelyPurchaseButton(buttons); if (picked) return picked; cur = cur.parentElement; } return null; }; const dayKeyFromPayload = (payload) => { if (!payload || typeof payload !== 'object') return null; const localDayKeyForDate = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const keys = ['day', 'date', 'name', 'label', 'x', 'time', 'timestamp']; for (const k of keys) { const v = payload[k]; if (typeof v === 'string') { const s = v.trim(); if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return s; const iso = s.match(/^(\\d{4}-\\d{2}-\\d{2})/); if (iso) return iso[1]; } if (typeof v === 'number' && Number.isFinite(v) && (k === 'timestamp' || k === 'time' || k === 'x')) { try { const d = new Date(v); if (!isNaN(d.getTime())) return localDayKeyForDate(d); } catch {} } } return null; }; const isSkillUsageServiceKey = (raw) => { const key = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase(); return key.startsWith('skillusage:'); }; const displayNameForUsageServiceKey = (raw) => { const key = raw === null || raw === undefined ? '' : String(raw).trim(); if (!key) return key; if (isSkillUsageServiceKey(key)) return null; if (key.toUpperCase() === key && key.length <= 6) return key; const lower = key.toLowerCase(); if (lower === 'cli') return 'CLI'; if (lower.includes('github') && lower.includes('review')) return 'GitHub Code Review'; const words = lower.replace(/[_-]+/g, ' ').split(' ').filter(Boolean); return words.map(w => w.length <= 2 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(' '); }; const isLikelyCodexUsageService = (raw) => { const service = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase(); return ( service === 'cli' || service === 'desktop' || service === 'desktop app' || service === 'vscode' || service === 'vs code' || service === 'unknown' || (service.includes('github') && service.includes('review')) ); }; const usageChartRootForPath = (path) => { if (!path || !path.closest) return null; return ( path.closest('.recharts-wrapper') || path.closest('svg.recharts-surface') || path.closest('section') || path.parentElement || null ); }; const uniqueUsageChartRoots = (paths) => { const roots = []; for (const path of paths) { const root = usageChartRootForPath(path); if (root && !roots.includes(root)) roots.push(root); } return roots; }; const usageBreakdownTitleScore = (title) => { const lower = String(title || '').trim().toLowerCase().replace(/\\s+/g, ' '); if (!lower) return 0; if (lower === 'usage breakdown') return 1000000; if (lower.includes('usage breakdown')) return 900000; if (lower === 'personal usage') return 800000; if (lower.includes('threads') || lower.includes('turns') || lower.includes('client') || lower.includes('skill') || lower.includes('invocation')) return -1000000; return 0; }; const titleLikeElements = (scope) => { try { return Array.from(scope.querySelectorAll('h1,h2,h3,[role=\"heading\"],div,span,p')) .filter(el => { const title = textOf(el); const lower = title.toLowerCase(); const tag = el.tagName ? el.tagName.toLowerCase() : ''; const isHeading = tag === 'h1' || tag === 'h2' || tag === 'h3' || String(el.getAttribute('role') || '').toLowerCase() === 'heading'; return title.length > 0 && title.length <= 80 && ( isHeading || usageBreakdownTitleScore(title) !== 0 || lower.includes('usage breakdown') || lower.includes('threads') || lower.includes('turns') || lower.includes('client') || lower.includes('skill') || lower.includes('invocation') ); }); } catch { return []; } }; const titleNodePrecedesRoot = (titleNode, root) => { if (!titleNode || titleNode === root || root.contains(titleNode) || titleNode.contains(root)) return false; const relation = titleNode.compareDocumentPosition(root); return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING); }; const nearestScoredChartTitleInScope = (scope, root) => { let best = null; for (const titleNode of titleLikeElements(scope)) { if (!titleNodePrecedesRoot(titleNode, root)) continue; const title = textOf(titleNode); const score = usageBreakdownTitleScore(title); if (score === 0) continue; if (!best || score >= best.score) best = { title, score }; } return best ? best.title : ''; }; const chartTitleBoundaryForRoot = (root) => { if (!root) return null; try { return root.closest('section,[role=\"region\"],article') || root.parentElement || null; } catch { return root.parentElement || null; } }; const nearestTitleTextInScope = (scope, root) => { if (!scope) return ''; let nearest = null; for (const titleNode of titleLikeElements(scope)) { if (titleNodePrecedesRoot(titleNode, root)) nearest = titleNode; } return textOf(nearest); }; const nearestChartTitleTextForRoot = (root) => { if (!root) return ''; try { const boundary = chartTitleBoundaryForRoot(root) || root.parentElement || null; let ancestor = root.parentElement || null; for (let i = 0; i < 8 && ancestor; i++) { const scoredTitle = nearestScoredChartTitleInScope(ancestor, root); if (scoredTitle) return scoredTitle; if (ancestor === boundary) break; ancestor = ancestor.parentElement || null; } return nearestTitleTextInScope(boundary, root); } catch { return ''; } }; const legendMapForUsageChartRoot = (root) => { const legendMap = {}; const scopes = [ root, root && root.parentElement, root && root.closest ? root.closest('section') : null ].filter(Boolean); for (const scope of scopes) { try { const legendItems = Array.from(scope.querySelectorAll('div[title]')); for (const item of legendItems) { const title = item.getAttribute('title') ? String(item.getAttribute('title')).trim() : ''; const square = item.querySelector('div[style*=\"background-color\"]'); const color = (square && square.style && square.style.backgroundColor) ? square.style.backgroundColor : null; const hex = parseHexColor(color); if (title && hex) legendMap[hex] = title; } } catch {} if (Object.keys(legendMap).length > 0) break; } return legendMap; }; const parseUsageBreakdownFromChartPaths = (paths, legendMap) => { const totalsByDay = {}; // day -> service -> value const addValue = (day, service, value) => { if (!day || !service || isSkillUsageServiceKey(service)) return false; if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return false; if (!totalsByDay[day]) totalsByDay[day] = {}; totalsByDay[day][service] = (totalsByDay[day][service] || 0) + value; return true; }; let pointCount = 0; for (const path of paths) { const meta = barMetaFromElement(path) || barMetaFromElement(path.parentElement) || null; if (!meta) continue; const payload = meta.payload || null; const day = dayKeyFromPayload(payload); if (!day) continue; const valuesObj = (payload && payload.values && typeof payload.values === 'object') ? payload.values : null; if (valuesObj) { for (const [k, v] of Object.entries(valuesObj)) { const service = displayNameForUsageServiceKey(k); if (addValue(day, service, v)) pointCount++; } continue; } let value = null; if (typeof meta.value === 'number' && Number.isFinite(meta.value)) value = meta.value; if (value === null && typeof meta.value === 'string') { const v = parseFloat(meta.value.replace(/,/g, '')); if (Number.isFinite(v)) value = v; } if (value === null) continue; const fill = parseHexColor(meta.fill || path.getAttribute('fill')); const service = (fill && legendMap[fill]) || (typeof meta.name === 'string' && meta.name) || null; if (addValue(day, service, value)) pointCount++; } const dayKeys = Object.keys(totalsByDay) .filter(day => Object.keys(totalsByDay[day] || {}).length > 0) .sort((a, b) => b.localeCompare(a)) .slice(0, 30); const breakdown = dayKeys.map(day => { const servicesMap = totalsByDay[day] || {}; const services = Object.keys(servicesMap).map(service => ({ service, creditsUsed: servicesMap[service] })).sort((a, b) => { if (a.creditsUsed === b.creditsUsed) return a.service.localeCompare(b.service); return b.creditsUsed - a.creditsUsed; }); const totalCreditsUsed = services.reduce((sum, s) => sum + (Number(s.creditsUsed) || 0), 0); return { day, services, totalCreditsUsed }; }); const services = Array.from(new Set(breakdown.flatMap(day => day.services.map(service => service.service)))); const totalCreditsUsed = breakdown.reduce((sum, day) => sum + (Number(day.totalCreditsUsed) || 0), 0); const likelyCodexServiceCount = services.filter(isLikelyCodexUsageService).length; return { breakdown, pointCount, services, totalCreditsUsed, likelyCodexServiceCount, score: likelyCodexServiceCount * 1000 + services.length * 100 + pointCount + totalCreditsUsed / 1000 }; }; const usageBreakdownJSON = (() => { try { if (window.__codexbarUsageBreakdownJSON) return window.__codexbarUsageBreakdownJSON; const paths = Array.from(document.querySelectorAll('g.recharts-bar-rectangle path.recharts-rectangle')); let debug = { pathCount: paths.length, chartCount: 0, eligibleCandidateCount: 0, selectedCandidateTitle: null, candidateSummaries: [], sampleReactKeys: null, sampleMetaKeys: null, samplePayloadKeys: null, sampleValuesKeys: null, sampleDayKey: null }; try { const sample = paths[0] || null; if (sample) { const names = Object.getOwnPropertyNames(sample); debug.sampleReactKeys = names.filter(k => k.includes('react')).slice(0, 10); const metaSample = barMetaFromElement(sample) || barMetaFromElement(sample.parentElement) || null; if (metaSample) { debug.sampleMetaKeys = Object.keys(metaSample).slice(0, 12); const payload = metaSample.payload || null; if (payload && typeof payload === 'object') { debug.samplePayloadKeys = Object.keys(payload).slice(0, 12); debug.sampleDayKey = dayKeyFromPayload(payload); const values = payload.values || null; if (values && typeof values === 'object') { debug.sampleValuesKeys = Object.keys(values).slice(0, 12); } } } } } catch {} const roots = uniqueUsageChartRoots(paths); debug.chartCount = roots.length; const candidates = roots.map(root => { const chartPaths = paths.filter(path => usageChartRootForPath(path) === root); const title = nearestChartTitleTextForRoot(root); const titleScore = usageBreakdownTitleScore(title); const parsed = parseUsageBreakdownFromChartPaths(chartPaths, legendMapForUsageChartRoot(root)); return { root, title, titleScore, pathCount: chartPaths.length, ...parsed, score: titleScore + parsed.score }; }).filter(candidate => candidate.breakdown.length > 0); const rejectedTitleCandidates = candidates.filter(candidate => candidate.titleScore < 0); const titledCandidates = candidates.filter(candidate => candidate.titleScore > 0); const unknownTitleCandidates = candidates.filter(candidate => candidate.titleScore === 0); const eligibleCandidates = titledCandidates; eligibleCandidates.sort((a, b) => b.score - a.score); debug.eligibleCandidateCount = eligibleCandidates.length; debug.selectedCandidateTitle = eligibleCandidates[0] ? eligibleCandidates[0].title : null; if (eligibleCandidates.length === 0 && candidates.length > 0) { if (unknownTitleCandidates.length > 0) { debug.error = 'No English usage breakdown chart title found. Candidate titles: ' + candidates.map(candidate => candidate.title || 'Untitled chart').join(', '); } else if (rejectedTitleCandidates.length > 0) { debug.error = 'Only non-usage chart candidates found: ' + rejectedTitleCandidates.map(candidate => candidate.title || 'Untitled chart').join(', '); } } debug.candidateSummaries = candidates.slice(0, 6).map(candidate => ({ title: candidate.title, titleScore: candidate.titleScore, pathCount: candidate.pathCount, dayCount: candidate.breakdown.length, pointCount: candidate.pointCount, serviceCount: candidate.services.length, likelyCodexServiceCount: candidate.likelyCodexServiceCount, services: candidate.services.slice(0, 8) })); const breakdown = eligibleCandidates[0] ? eligibleCandidates[0].breakdown : []; const json = (breakdown.length > 0) ? JSON.stringify(breakdown) : null; window.__codexbarUsageBreakdownJSON = json; window.__codexbarUsageBreakdownDebug = json ? null : JSON.stringify(debug); return json; } catch { return null; } })(); const usageBreakdownDebug = (() => { try { return window.__codexbarUsageBreakdownDebug || null; } catch { return null; } })(); const usageBreakdownError = (() => { try { if (!usageBreakdownDebug) return null; const parsed = JSON.parse(usageBreakdownDebug); return parsed && parsed.error ? String(parsed.error) : null; } catch { return null; } })(); const bodyText = document.body ? String(document.body.innerText || '').trim() : ''; const href = window.location ? String(window.location.href || '') : ''; const workspacePicker = bodyText.includes('Select a workspace'); const title = document.title ? String(document.title || '') : ''; const cloudflareInterstitial = title.toLowerCase().includes('just a moment') || bodyText.toLowerCase().includes('checking your browser') || bodyText.toLowerCase().includes('cloudflare'); const authSelector = [ 'input[type="email"]', 'input[type="password"]', 'input[name="username"]' ].join(', '); const hasAuthInputs = !!document.querySelector(authSelector); const lower = bodyText.toLowerCase(); const loginCTA = lower.includes('sign in') || lower.includes('log in') || lower.includes('continue with google') || lower.includes('continue with apple') || lower.includes('continue with microsoft'); const loginRequired = href.includes('/auth/') || href.includes('/login') || (hasAuthInputs && loginCTA) || (!hasAuthInputs && loginCTA && href.includes('chatgpt.com')); const scrollY = (typeof window.scrollY === 'number') ? window.scrollY : 0; const scrollHeight = document.documentElement ? (document.documentElement.scrollHeight || 0) : 0; const viewportHeight = (typeof window.innerHeight === 'number') ? window.innerHeight : 0; let creditsHeaderPresent = false; let creditsHeaderInViewport = false; let didScrollToCredits = false; let rows = []; try { const looksLikeCreditsEventRow = (cells) => { if (!cells || cells.length < 3) return false; const first = String(cells[0] || ''); const amount = String(cells[2] || ''); return /\\d{4}|\\d{1,2}[\\/.\\-]\\d{1,2}/.test(first) && /\\d/.test(amount); }; const allTableRows = () => Array.from(document.querySelectorAll('tbody tr')).map(tr => { const cells = Array.from(tr.querySelectorAll('td')).map(td => textOf(td)); return cells; }).filter(looksLikeCreditsEventRow); const headings = Array.from(document.querySelectorAll('h1,h2,h3')); const header = headings.find(h => textOf(h).toLowerCase() === 'credits usage history'); if (header) { creditsHeaderPresent = true; const rect = header.getBoundingClientRect(); creditsHeaderInViewport = rect.top >= 0 && rect.top <= viewportHeight; // Only scrape rows from the *credits usage history* table. The page can contain other tables, // and treating any