Maximus upgradus
This commit is contained in:
+24
-15
@@ -1,24 +1,33 @@
|
||||
import type { ClaudeOrg, ClaudeUsageResponse } from "@/types/claude";
|
||||
import type { ClaudeAuth, ClaudeOrg, ClaudeUsageResponse } from "@/types/claude";
|
||||
|
||||
const BASE_URL = "https://claude.ai/api";
|
||||
|
||||
function cookieHeader(sessionKey: string): Record<string, string> {
|
||||
const BROWSER_UA =
|
||||
"Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36";
|
||||
|
||||
function buildHeaders(auth: ClaudeAuth): Record<string, string> {
|
||||
const cookies = [`sessionKey=${auth.sessionKey}`];
|
||||
if (auth.lastActiveOrg) cookies.push(`lastActiveOrg=${auth.lastActiveOrg}`);
|
||||
return {
|
||||
Cookie: `sessionKey=${sessionKey}`,
|
||||
"Accept": "application/json",
|
||||
"Origin": "https://claude.ai",
|
||||
Cookie: cookies.join("; "),
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"User-Agent": BROWSER_UA,
|
||||
Referer: "https://claude.ai/",
|
||||
Origin: "https://claude.ai",
|
||||
"anthropic-client-platform": "web_claude_to_cc_migration",
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchClaudeOrgs(sessionKey: string): Promise<ClaudeOrg[]> {
|
||||
export async function fetchClaudeOrgs(auth: ClaudeAuth): Promise<ClaudeOrg[]> {
|
||||
const response = await fetch(`${BASE_URL}/organizations`, {
|
||||
headers: cookieHeader(sessionKey),
|
||||
headers: buildHeaders(auth),
|
||||
credentials: "omit",
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error("TOKEN_EXPIRED");
|
||||
}
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => null);
|
||||
if (response.status === 401 || response.status === 403) throw new Error("TOKEN_EXPIRED");
|
||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||
}
|
||||
|
||||
@@ -26,17 +35,17 @@ export async function fetchClaudeOrgs(sessionKey: string): Promise<ClaudeOrg[]>
|
||||
}
|
||||
|
||||
export async function fetchClaudeUsage(
|
||||
sessionKey: string,
|
||||
auth: ClaudeAuth,
|
||||
orgUuid: string
|
||||
): Promise<ClaudeUsageResponse> {
|
||||
const response = await fetch(`${BASE_URL}/organizations/${orgUuid}/usage`, {
|
||||
headers: cookieHeader(sessionKey),
|
||||
headers: buildHeaders(auth),
|
||||
credentials: "omit",
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error("TOKEN_EXPIRED");
|
||||
}
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => null);
|
||||
if (response.status === 401 || response.status === 403) throw new Error("TOKEN_EXPIRED");
|
||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function fetchCodexUsage(auth: CodexAuth): Promise<CodexUsageRespon
|
||||
throw new Error("TOKEN_EXPIRED");
|
||||
}
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => null);
|
||||
throw new Error(`HTTP_ERROR_${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
+6
-8
@@ -1,5 +1,4 @@
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { File } from "expo-file-system/next";
|
||||
import type { CodexAuth } from "@/types/codex";
|
||||
|
||||
export async function pickAndReadCodexAuth(): Promise<CodexAuth> {
|
||||
@@ -8,13 +7,11 @@ export async function pickAndReadCodexAuth(): Promise<CodexAuth> {
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
throw new Error("PICKER_CANCELLED");
|
||||
}
|
||||
if (result.canceled) throw new Error("PICKER_CANCELLED");
|
||||
|
||||
const { uri } = result.assets[0];
|
||||
const file = new File(uri);
|
||||
const text = file.text();
|
||||
const response = await fetch(uri);
|
||||
const text = await response.text();
|
||||
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
@@ -23,8 +20,9 @@ export async function pickAndReadCodexAuth(): Promise<CodexAuth> {
|
||||
throw new Error("INVALID_JSON");
|
||||
}
|
||||
|
||||
const accessToken = parsed["accessToken"] as string | undefined;
|
||||
const accountId = parsed["accountId"] as string | undefined;
|
||||
const tokens = parsed["tokens"] as Record<string, unknown> | undefined;
|
||||
const accessToken = (tokens?.["access_token"] ?? parsed["accessToken"]) as string | undefined;
|
||||
const accountId = (tokens?.["account_id"] ?? parsed["accountId"]) as string | undefined;
|
||||
|
||||
if (!accessToken || typeof accessToken !== "string") {
|
||||
throw new Error("MISSING_ACCESS_TOKEN");
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import * as Notifications from "expo-notifications";
|
||||
import {
|
||||
loadNotifSettings,
|
||||
loadNotifThresholdLastFired,
|
||||
saveNotifThresholdLastFired,
|
||||
} from "@/lib/storage";
|
||||
import type { ClaudeUsageResponse } from "@/types/claude";
|
||||
import type { CodexUsageResponse } from "@/types/codex";
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export async function requestPermissions(): Promise<boolean> {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
return status === "granted";
|
||||
}
|
||||
|
||||
export async function getPermissionStatus(): Promise<string> {
|
||||
const { status } = await Notifications.getPermissionsAsync();
|
||||
return status;
|
||||
}
|
||||
|
||||
const DAILY_ID = "codexbar-daily-digest";
|
||||
|
||||
export async function scheduleDailyDigest(
|
||||
hour: number,
|
||||
minute: number,
|
||||
claudeUsage: ClaudeUsageResponse | null,
|
||||
codexUsage: CodexUsageResponse | null
|
||||
): Promise<void> {
|
||||
try {
|
||||
await Notifications.cancelScheduledNotificationAsync(DAILY_ID);
|
||||
} catch {}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (claudeUsage) {
|
||||
parts.push(`Claude 7d: ${Math.round(claudeUsage.seven_day.utilization)}% used`);
|
||||
}
|
||||
if (codexUsage) {
|
||||
parts.push(
|
||||
`Codex: ${Math.round(codexUsage.rate_limit.secondary_window.used_percent)}% used`
|
||||
);
|
||||
}
|
||||
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
identifier: DAILY_ID,
|
||||
content: {
|
||||
title: "Usage Digest",
|
||||
body:
|
||||
parts.length > 0
|
||||
? parts.join(" · ")
|
||||
: "Open to check your usage limits",
|
||||
},
|
||||
trigger: {
|
||||
type: Notifications.SchedulableTriggerInputTypes.CALENDAR,
|
||||
hour,
|
||||
minute,
|
||||
repeats: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelDailyDigest(): Promise<void> {
|
||||
try {
|
||||
await Notifications.cancelScheduledNotificationAsync(DAILY_ID);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
interface ThresholdAlert {
|
||||
service: string;
|
||||
window: string;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
function buildThresholdAlerts(
|
||||
thresholdPct: number,
|
||||
claudeUsage: ClaudeUsageResponse | null,
|
||||
codexUsage: CodexUsageResponse | null
|
||||
): ThresholdAlert[] {
|
||||
const alerts: ThresholdAlert[] = [];
|
||||
|
||||
if (claudeUsage) {
|
||||
const remaining = 100 - claudeUsage.seven_day.utilization;
|
||||
if (remaining <= thresholdPct) {
|
||||
alerts.push({ service: "Claude", window: "7-day", remaining });
|
||||
}
|
||||
}
|
||||
|
||||
if (codexUsage) {
|
||||
const remaining = 100 - codexUsage.rate_limit.secondary_window.used_percent;
|
||||
if (remaining <= thresholdPct) {
|
||||
alerts.push({ service: "Codex", window: "weekly", remaining });
|
||||
}
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
/** Call this after every successful usage fetch. Reschedules daily digest and fires threshold alerts. */
|
||||
export async function onUsageDataLoaded(
|
||||
claudeUsage: ClaudeUsageResponse | null,
|
||||
codexUsage: CodexUsageResponse | null
|
||||
): Promise<void> {
|
||||
const settings = await loadNotifSettings();
|
||||
|
||||
if (settings.dailyEnabled) {
|
||||
await scheduleDailyDigest(
|
||||
settings.dailyHour,
|
||||
settings.dailyMinute,
|
||||
claudeUsage,
|
||||
codexUsage
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.thresholdEnabled) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const lastFired = await loadNotifThresholdLastFired();
|
||||
if (lastFired !== today) {
|
||||
const alerts = buildThresholdAlerts(
|
||||
settings.thresholdPct,
|
||||
claudeUsage,
|
||||
codexUsage
|
||||
);
|
||||
if (alerts.length > 0) {
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: "Low quota warning",
|
||||
body: alerts
|
||||
.map(
|
||||
(a) =>
|
||||
`${a.service} ${a.window}: ${Math.round(a.remaining)}% remaining`
|
||||
)
|
||||
.join("\n"),
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
await saveNotifThresholdLastFired(today);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,59 @@ import * as SecureStore from "expo-secure-store";
|
||||
const KEYS = {
|
||||
CODEX_AUTH: "codexbar_codex_auth",
|
||||
CLAUDE_SESSION_KEY: "codexbar_claude_session_key",
|
||||
CLAUDE_LAST_ACTIVE_ORG: "codexbar_claude_last_active_org",
|
||||
NOTIF_DAILY_ENABLED: "notifDailyEnabled",
|
||||
NOTIF_DAILY_HOUR: "notifDailyHour",
|
||||
NOTIF_DAILY_MINUTE: "notifDailyMinute",
|
||||
NOTIF_THRESHOLD_ENABLED: "notifThresholdEnabled",
|
||||
NOTIF_THRESHOLD_PCT: "notifThresholdPct",
|
||||
NOTIF_THRESHOLD_LAST_FIRED: "notifThresholdLastFired",
|
||||
} as const;
|
||||
|
||||
export interface NotifSettings {
|
||||
dailyEnabled: boolean;
|
||||
dailyHour: number;
|
||||
dailyMinute: number;
|
||||
thresholdEnabled: boolean;
|
||||
/** Alert when remaining quota falls below this % (e.g. 20 = fire when < 20% left) */
|
||||
thresholdPct: number;
|
||||
}
|
||||
|
||||
export async function loadNotifSettings(): Promise<NotifSettings> {
|
||||
const [de, dh, dm, te, tp] = await Promise.all([
|
||||
SecureStore.getItemAsync(KEYS.NOTIF_DAILY_ENABLED),
|
||||
SecureStore.getItemAsync(KEYS.NOTIF_DAILY_HOUR),
|
||||
SecureStore.getItemAsync(KEYS.NOTIF_DAILY_MINUTE),
|
||||
SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_ENABLED),
|
||||
SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_PCT),
|
||||
]);
|
||||
return {
|
||||
dailyEnabled: de === "true",
|
||||
dailyHour: dh !== null ? parseInt(dh, 10) : 9,
|
||||
dailyMinute: dm !== null ? parseInt(dm, 10) : 0,
|
||||
thresholdEnabled: te === "true",
|
||||
thresholdPct: tp !== null ? parseInt(tp, 10) : 20,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveNotifSettings(s: NotifSettings): Promise<void> {
|
||||
await Promise.all([
|
||||
SecureStore.setItemAsync(KEYS.NOTIF_DAILY_ENABLED, String(s.dailyEnabled)),
|
||||
SecureStore.setItemAsync(KEYS.NOTIF_DAILY_HOUR, String(s.dailyHour)),
|
||||
SecureStore.setItemAsync(KEYS.NOTIF_DAILY_MINUTE, String(s.dailyMinute)),
|
||||
SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_ENABLED, String(s.thresholdEnabled)),
|
||||
SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_PCT, String(s.thresholdPct)),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function loadNotifThresholdLastFired(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(KEYS.NOTIF_THRESHOLD_LAST_FIRED);
|
||||
}
|
||||
|
||||
export async function saveNotifThresholdLastFired(date: string): Promise<void> {
|
||||
await SecureStore.setItemAsync(KEYS.NOTIF_THRESHOLD_LAST_FIRED, date);
|
||||
}
|
||||
|
||||
export async function saveCodexAuth(auth: {
|
||||
accessToken: string;
|
||||
accountId?: string;
|
||||
@@ -29,10 +80,23 @@ export async function loadClaudeSessionKey(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(KEYS.CLAUDE_SESSION_KEY);
|
||||
}
|
||||
|
||||
export async function saveClaudeLastActiveOrg(orgId: string): Promise<void> {
|
||||
await SecureStore.setItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG, orgId);
|
||||
}
|
||||
|
||||
export async function loadClaudeLastActiveOrg(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG);
|
||||
}
|
||||
|
||||
export async function clearClaudeLastActiveOrg(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG);
|
||||
}
|
||||
|
||||
export async function clearCodexAuth(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(KEYS.CODEX_AUTH);
|
||||
}
|
||||
|
||||
export async function clearClaudeSessionKey(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(KEYS.CLAUDE_SESSION_KEY);
|
||||
await SecureStore.deleteItemAsync(KEYS.CLAUDE_LAST_ACTIVE_ORG);
|
||||
}
|
||||
|
||||
+16
-6
@@ -5,11 +5,17 @@ export function formatResetCountdown(resetAtSeconds: number): string {
|
||||
if (diffSeconds <= 0) return "resetting now";
|
||||
if (diffSeconds < 60) return "resets in <1m";
|
||||
|
||||
const hours = Math.floor(diffSeconds / 3600);
|
||||
const totalHours = Math.floor(diffSeconds / 3600);
|
||||
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
||||
|
||||
if (hours > 0 && minutes > 0) return `resets in ${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `resets in ${hours}h`;
|
||||
if (totalHours >= 24) {
|
||||
const days = Math.floor(totalHours / 24);
|
||||
const hours = totalHours % 24;
|
||||
if (hours > 0) return `resets in ${days}d ${hours}h`;
|
||||
return `resets in ${days}d`;
|
||||
}
|
||||
if (totalHours > 0 && minutes > 0) return `resets in ${totalHours}h ${minutes}m`;
|
||||
if (totalHours > 0) return `resets in ${totalHours}h`;
|
||||
return `resets in ${minutes}m`;
|
||||
}
|
||||
|
||||
@@ -19,7 +25,11 @@ export function formatResetCountdownISO(isoString: string): string {
|
||||
}
|
||||
|
||||
export function windowLabel(limitWindowSeconds: number): string {
|
||||
if (limitWindowSeconds <= 3600) return "1h window";
|
||||
if (limitWindowSeconds <= 86400) return "24h window";
|
||||
return `${Math.round(limitWindowSeconds / 3600)}h window`;
|
||||
const hours = limitWindowSeconds / 3600;
|
||||
if (hours < 24) return `${Math.round(hours)}h`;
|
||||
const days = hours / 24;
|
||||
const wholeDays = Math.floor(days);
|
||||
const remainderHours = Math.round(hours - wholeDays * 24);
|
||||
if (remainderHours === 0) return `${wholeDays}d`;
|
||||
return `${wholeDays}d ${remainderHours}h`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user