@sriinnu/runic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swiftformat +50 -0
- package/.swiftlint.yml +160 -0
- package/CHANGELOG.md +307 -0
- package/CONTRIBUTING.md +456 -0
- package/Icon.icns +0 -0
- package/Icon.icon/Assets/runic.png +0 -0
- package/Icon.icon/icon.json +36 -0
- package/LICENSE +21 -0
- package/Package.resolved +51 -0
- package/Package.swift +136 -0
- package/Packages/Helix/LICENSE +21 -0
- package/Packages/Helix/Package.swift +29 -0
- package/Packages/Helix/README.md +976 -0
- package/Packages/Helix/Sources/Helix/Binding.swift +111 -0
- package/Packages/Helix/Sources/Helix/CommandParser.swift +160 -0
- package/Packages/Helix/Sources/Helix/CommandSignature.swift +117 -0
- package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +72 -0
- package/Packages/Helix/Sources/Helix/HelpGenerator.swift +175 -0
- package/Packages/Helix/Sources/Helix/NameSpecification.swift +98 -0
- package/Packages/Helix/Sources/Helix/ParsableCommand.swift +204 -0
- package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +44 -0
- package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +34 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +91 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +94 -0
- package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +167 -0
- package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +99 -0
- package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +146 -0
- package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +97 -0
- package/Packages/Helix/Sources/Helix/Program.swift +204 -0
- package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +367 -0
- package/Packages/Silo/LICENSE +21 -0
- package/Packages/Silo/Package.swift +34 -0
- package/Packages/Silo/README.md +466 -0
- package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +169 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +365 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +594 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +326 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +154 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +119 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +121 -0
- package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +391 -0
- package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +240 -0
- package/Packages/Silo/Sources/Silo/CryptoSupport.swift +86 -0
- package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +172 -0
- package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +172 -0
- package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +105 -0
- package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +191 -0
- package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +41 -0
- package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +110 -0
- package/README.md +109 -0
- package/RuniciOS/App/RuniciOSApp.swift +105 -0
- package/RuniciOS/Core/iOSUsageStore.swift +192 -0
- package/RuniciOS/Views/ProviderDetailView.swift +312 -0
- package/RuniciOS/Views/ProviderListView.swift +228 -0
- package/RuniciOS/Widgets/LargeWidget.swift +367 -0
- package/RuniciOS/Widgets/LiveActivity.swift +342 -0
- package/RuniciOS/Widgets/LockScreenWidget.swift +273 -0
- package/RuniciOS/Widgets/MediumWidget.swift +370 -0
- package/RuniciOS/Widgets/RunicWidget.swift +274 -0
- package/RuniciOS/Widgets/SmallWidget.swift +281 -0
- package/RuniciOS/Widgets/WidgetModels.swift +285 -0
- package/RuniciOS/Widgets/WidgetTimeline.swift +270 -0
- package/Scripts/CREATION-SUMMARY.md +367 -0
- package/Scripts/DIRECTORY-STRUCTURE.md +358 -0
- package/Scripts/GETTING-STARTED-CHECKLIST.md +364 -0
- package/Scripts/INDEX.md +365 -0
- package/Scripts/QUICK-REFERENCE.md +164 -0
- package/Scripts/README-SCRIPTS.md +926 -0
- package/Scripts/build-android.sh +526 -0
- package/Scripts/build-ios.sh +529 -0
- package/Scripts/build-macos.sh +603 -0
- package/Scripts/build-windows.sh +504 -0
- package/Scripts/build_icon.sh +49 -0
- package/Scripts/changelog-to-html.sh +89 -0
- package/Scripts/check-release-assets.sh +10 -0
- package/Scripts/ci-config.yml +474 -0
- package/Scripts/compile_and_run.sh +219 -0
- package/Scripts/compile_and_run_adhoc.sh +4 -0
- package/Scripts/docs-list.mjs +119 -0
- package/Scripts/make_appcast.sh +58 -0
- package/Scripts/package_app.sh +409 -0
- package/Scripts/release-lib.sh +214 -0
- package/Scripts/release.sh +76 -0
- package/Scripts/setup-all.sh +387 -0
- package/Scripts/setup-ios.sh +404 -0
- package/Scripts/setup-react-native.sh +523 -0
- package/Scripts/sign-and-notarize.sh +122 -0
- package/Scripts/test_live_update.sh +38 -0
- package/Scripts/validate_changelog.sh +23 -0
- package/Scripts/verify_appcast.sh +92 -0
- package/Sources/Runic/About.swift +93 -0
- package/Sources/Runic/AppNotifications.swift +115 -0
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +657 -0
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +518 -0
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +810 -0
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +87 -0
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +1379 -0
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +337 -0
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +395 -0
- package/Sources/Runic/Controllers/StatusItemController.swift +360 -0
- package/Sources/Runic/Core/BudgetNotificationManager.swift +80 -0
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +103 -0
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +154 -0
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +206 -0
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +95 -0
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +1216 -0
- package/Sources/Runic/Core/Rendering/IconView.swift +120 -0
- package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +71 -0
- package/Sources/Runic/Core/RunicAnimation.swift +53 -0
- package/Sources/Runic/Core/RunicColors.swift +73 -0
- package/Sources/Runic/Core/RunicCornerRadius.swift +15 -0
- package/Sources/Runic/Core/RunicSpacing.swift +68 -0
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +158 -0
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +30 -0
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/SettingsStore.swift +1702 -0
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +20 -0
- package/Sources/Runic/Core/Stores/UsageStore+Status.swift +189 -0
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +33 -0
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +67 -0
- package/Sources/Runic/Core/Stores/UsageStore.swift +3564 -0
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +143 -0
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +143 -0
- package/Sources/Runic/Core/UsageExporter.swift +193 -0
- package/Sources/Runic/Date+RelativeDescription.swift +21 -0
- package/Sources/Runic/HiddenWindowView.swift +32 -0
- package/Sources/Runic/InstallOrigin.swift +9 -0
- package/Sources/Runic/KeyboardShortcuts+Names.swift +6 -0
- package/Sources/Runic/MouseLocationReader.swift +65 -0
- package/Sources/Runic/Notifications+Runic.swift +6 -0
- package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +503 -0
- package/Sources/Runic/ProviderRegistry.swift +100 -0
- package/Sources/Runic/ProviderSwitcherButtons.swift +267 -0
- package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +11 -0
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +14 -0
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +52 -0
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +42 -0
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +26 -0
- package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +14 -0
- package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +18 -0
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +193 -0
- package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +110 -0
- package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +28 -0
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +12 -0
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +20 -0
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +15 -0
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +32 -0
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +15 -0
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +48 -0
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +66 -0
- package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +12 -0
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +99 -0
- package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +18 -0
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +24 -0
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +24 -0
- package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +2 -0
- package/Sources/Runic/Resources/ProviderIcon-auggie.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-azure.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +7 -0
- package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-claude.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-codex.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-cohere.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-copilot.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-cursor.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-factory.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-gemini.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-groq.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-kimi.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-minimax.svg +1 -0
- package/Sources/Runic/Resources/ProviderIcon-mistral.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +7 -0
- package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-qwen.svg +9 -0
- package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +4 -0
- package/Sources/Runic/Resources/ProviderIcon-together.svg +5 -0
- package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +17 -0
- package/Sources/Runic/Resources/ProviderIcon-xai.svg +3 -0
- package/Sources/Runic/Resources/ProviderIcon-zai.svg +5 -0
- package/Sources/Runic/Resources/RunicMenubarIcon.svg +7 -0
- package/Sources/Runic/RunicApp.swift +357 -0
- package/Sources/Runic/SessionQuotaNotifications.swift +58 -0
- package/Sources/Runic/UsagePaceText.swift +52 -0
- package/Sources/Runic/UsageProgressBar.swift +224 -0
- package/Sources/Runic/Utilities/DisplayLink.swift +96 -0
- package/Sources/Runic/Utilities/LoadingPattern.swift +60 -0
- package/Sources/Runic/Utilities/PerformanceConstants.swift +85 -0
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +256 -0
- package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +22 -0
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +275 -0
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +377 -0
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +312 -0
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +312 -0
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +228 -0
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +228 -0
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +162 -0
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +64 -0
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +268 -0
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +104 -0
- package/Sources/Runic/Views/Menu/MenuCardView.swift +1900 -0
- package/Sources/Runic/Views/Menu/MenuContent.swift +150 -0
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +369 -0
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +42 -0
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +235 -0
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +141 -0
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +209 -0
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +220 -0
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +88 -0
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +201 -0
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +398 -0
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +232 -0
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +417 -0
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +190 -0
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +132 -0
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +618 -0
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +305 -0
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +271 -0
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +268 -0
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +422 -0
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +641 -0
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +344 -0
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +230 -0
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +322 -0
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +469 -0
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +231 -0
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +50 -0
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +497 -0
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +288 -0
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +3435 -0
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +25 -0
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +59 -0
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +206 -0
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +238 -0
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +371 -0
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +175 -0
- package/Sources/Runic/Views/Team/TeamManagementView.swift +761 -0
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +181 -0
- package/Sources/RunicCLI/CLIEntry.swift +942 -0
- package/Sources/RunicCLI/CLIRenderer.swift +87 -0
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +588 -0
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +362 -0
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +439 -0
- package/Sources/RunicCLI/Commands/ResetCommand.swift +429 -0
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +168 -0
- package/Sources/RunicClaudeWatchdog/main.swift +122 -0
- package/Sources/RunicClaudeWebProbe/main.swift +62 -0
- package/Sources/RunicCore/AlertRuleStore.swift +187 -0
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +25 -0
- package/Sources/RunicCore/CopilotUsageModels.swift +84 -0
- package/Sources/RunicCore/CostUsageFetcher.swift +119 -0
- package/Sources/RunicCore/CostUsageModels.swift +479 -0
- package/Sources/RunicCore/CreditsModels.swift +50 -0
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +269 -0
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +622 -0
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +500 -0
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +679 -0
- package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +135 -0
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +173 -0
- package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +79 -0
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +86 -0
- package/Sources/RunicCore/Logging/RunicLog.swift +123 -0
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +371 -0
- package/Sources/RunicCore/OpenAIDashboardModels.swift +210 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +676 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +492 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +51 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +483 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +558 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +204 -0
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +78 -0
- package/Sources/RunicCore/PathEnvironment.swift +482 -0
- package/Sources/RunicCore/Performance/PerformanceModels.swift +371 -0
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +409 -0
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +178 -0
- package/Sources/RunicCore/ProjectBudgetStore.swift +99 -0
- package/Sources/RunicCore/ProviderCostSnapshot.swift +29 -0
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +66 -0
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +685 -0
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +142 -0
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +141 -0
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +186 -0
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +126 -0
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +168 -0
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +332 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +180 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +137 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +207 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +654 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +28 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +480 -0
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +780 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +146 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +269 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +100 -0
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +243 -0
- package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +125 -0
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +24 -0
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +181 -0
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +85 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +123 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +114 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +100 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +291 -0
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +386 -0
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +594 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +85 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +108 -0
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +398 -0
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1275 -0
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +65 -0
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +835 -0
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +71 -0
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +77 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +92 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +54 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +171 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +911 -0
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +87 -0
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +207 -0
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/ProviderBranding.swift +25 -0
- package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +17 -0
- package/Sources/RunicCore/Providers/ProviderDescriptor.swift +129 -0
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +263 -0
- package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +98 -0
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +501 -0
- package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +68 -0
- package/Sources/RunicCore/Providers/Providers.swift +184 -0
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +30 -0
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +317 -0
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +84 -0
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +123 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +48 -0
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +144 -0
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +83 -0
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +75 -0
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +72 -0
- package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +41 -0
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +724 -0
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +385 -0
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +310 -0
- package/Sources/RunicCore/Sync/SyncProtocol.swift +288 -0
- package/Sources/RunicCore/Sync/SyncRecord.swift +405 -0
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +294 -0
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +775 -0
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +333 -0
- package/Sources/RunicCore/Team/TeamModels.swift +583 -0
- package/Sources/RunicCore/Team/TeamShowbackExporter.swift +495 -0
- package/Sources/RunicCore/Team/TeamStore.swift +792 -0
- package/Sources/RunicCore/TextParsing.swift +55 -0
- package/Sources/RunicCore/UsageFetcher.swift +694 -0
- package/Sources/RunicCore/UsageFormatter.swift +410 -0
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +329 -0
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +448 -0
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +550 -0
- package/Sources/RunicCore/UsageLedger/UsageLedger.swift +29 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +660 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +209 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +417 -0
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +245 -0
- package/Sources/RunicCore/UsagePace.swift +81 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +73 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +74 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +208 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +455 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +132 -0
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +589 -0
- package/Sources/RunicCore/WidgetSnapshot.swift +183 -0
- package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +14 -0
- package/Sources/RunicMacros/ProviderRegistrationMacros.swift +216 -0
- package/Sources/RunicWidget/RunicWidgetBundle.swift +79 -0
- package/Sources/RunicWidget/RunicWidgetProvider.swift +283 -0
- package/Sources/RunicWidget/RunicWidgetViews.swift +680 -0
- package/Tests/RunicTests/AntigravityStatusProbeTests.swift +50 -0
- package/Tests/RunicTests/AppDelegateTests.swift +43 -0
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +22 -0
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +70 -0
- package/Tests/RunicTests/CLICostTests.swift +65 -0
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +22 -0
- package/Tests/RunicTests/CLISnapshotTests.swift +72 -0
- package/Tests/RunicTests/CLIWebFallbackTests.swift +58 -0
- package/Tests/RunicTests/ClaudeOAuthTests.swift +164 -0
- package/Tests/RunicTests/ClaudeResilienceTests.swift +30 -0
- package/Tests/RunicTests/ClaudeUsageTests.swift +370 -0
- package/Tests/RunicTests/CodexOAuthTests.swift +122 -0
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +82 -0
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +75 -0
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +75 -0
- package/Tests/RunicTests/CostUsageDecodingTests.swift +394 -0
- package/Tests/RunicTests/CostUsagePricingTests.swift +50 -0
- package/Tests/RunicTests/CostUsageScannerTests.swift +700 -0
- package/Tests/RunicTests/CursorStatusProbeTests.swift +323 -0
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +69 -0
- package/Tests/RunicTests/GeminiAPITestHelpers.swift +94 -0
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +28 -0
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +283 -0
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +279 -0
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +233 -0
- package/Tests/RunicTests/GeminiTestEnvironment.swift +87 -0
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +73 -0
- package/Tests/RunicTests/InstallOriginTests.swift +18 -0
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +9 -0
- package/Tests/RunicTests/LiveAccountTests.swift +24 -0
- package/Tests/RunicTests/LoadingPatternTests.swift +35 -0
- package/Tests/RunicTests/MenuCardModelTests.swift +545 -0
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/MiniMaxParsingTests.swift +68 -0
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +8 -0
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +112 -0
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +17 -0
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +75 -0
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +27 -0
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +134 -0
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +53 -0
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +92 -0
- package/Tests/RunicTests/PathBuilderTests.swift +237 -0
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +160 -0
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +38 -0
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +16 -0
- package/Tests/RunicTests/ProviderRegistryTests.swift +29 -0
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +202 -0
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +51 -0
- package/Tests/RunicTests/RunicTests.swift +120 -0
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +36 -0
- package/Tests/RunicTests/SettingsStoreTests.swift +161 -0
- package/Tests/RunicTests/StatusItemAnimationTests.swift +49 -0
- package/Tests/RunicTests/StatusMenuTests.swift +434 -0
- package/Tests/RunicTests/StatusProbeTests.swift +377 -0
- package/Tests/RunicTests/SubprocessRunnerTests.swift +20 -0
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +65 -0
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +148 -0
- package/Tests/RunicTests/TTYIntegrationTests.swift +41 -0
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +159 -0
- package/Tests/RunicTests/TextParsingTests.swift +12 -0
- package/Tests/RunicTests/UsageFormatterTests.swift +112 -0
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +172 -0
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +213 -0
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +83 -0
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +170 -0
- package/Tests/RunicTests/UsagePaceTests.swift +77 -0
- package/Tests/RunicTests/UsagePaceTextTests.swift +109 -0
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +91 -0
- package/Tests/RunicTests/WidgetSnapshotTests.swift +45 -0
- package/Tests/RunicTests/ZaiProviderTests.swift +61 -0
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +6 -0
- package/TestsLinux/PlatformGatingTests.swift +46 -0
- package/appcast.xml +105 -0
- package/assets/logo-concept-stack.svg +24 -0
- package/assets/logo-minimal-bars.svg +15 -0
- package/assets/logo.svg +20 -0
- package/assets/logo2.svg +43 -0
- package/bin/docs-list +5 -0
- package/bin/install-runic-cli.sh +32 -0
- package/build-and-run.sh +369 -0
- package/docs/.nojekyll +1 -0
- package/docs/CNAME +1 -0
- package/docs/antigravity.md +75 -0
- package/docs/architecture.md +32 -0
- package/docs/claude.md +93 -0
- package/docs/cli-commands/integration_guide.md +371 -0
- package/docs/cli-commands/quick_reference.md +362 -0
- package/docs/cli-commands/readme.md +361 -0
- package/docs/cli.md +156 -0
- package/docs/cloudkit_sync_guide.md +495 -0
- package/docs/codebase_review.md +288 -0
- package/docs/codex-oauth.md +635 -0
- package/docs/codex.md +104 -0
- package/docs/codexbar.png +0 -0
- package/docs/copilot.md +44 -0
- package/docs/cursor.md +46 -0
- package/docs/design/designcomparisonchart.md +510 -0
- package/docs/design/implementationguide.md +792 -0
- package/docs/design/menubaricondesignconcepts.md +796 -0
- package/docs/design/mockups/hybridiconconcept.svg +62 -0
- package/docs/design/mockups/infinityiconconcept.svg +41 -0
- package/docs/design/mockups/infinityiconstates.svg +129 -0
- package/docs/design/mockups/wavelogorefined.svg +48 -0
- package/docs/design/quick_summary.md +426 -0
- package/docs/design/readme.md +478 -0
- package/docs/design/sizespecifications.md +575 -0
- package/docs/design-system.md +836 -0
- package/docs/factory.md +103 -0
- package/docs/gemini.md +73 -0
- package/docs/icon.md +37 -0
- package/docs/icon.png +0 -0
- package/docs/index.html +175 -0
- package/docs/keychain_fix.md +274 -0
- package/docs/minimax.md +71 -0
- package/docs/packaging.md +25 -0
- package/docs/progress-bar-improvements.md +240 -0
- package/docs/progress-bar-quick-reference.md +283 -0
- package/docs/progress-bar-technical-details.md +432 -0
- package/docs/progress-bar-visual-test.swift +375 -0
- package/docs/provider.md +202 -0
- package/docs/providers.md +86 -0
- package/docs/refactor/macros.md +35 -0
- package/docs/refresh-loop.md +22 -0
- package/docs/releasing-homebrew.md +35 -0
- package/docs/releasing.md +121 -0
- package/docs/research/concrete_todo_2026-02-22.md +47 -0
- package/docs/research/core-feature-research.md +248 -0
- package/docs/research/feature_uiux_roadmap_2026-02-22.md +202 -0
- package/docs/research/parity_matrix.md +49 -0
- package/docs/research/provider_expansion_candidates_2026-02-22.md +80 -0
- package/docs/research/provider_model_token_project_matrix_2026-02-23.md +46 -0
- package/docs/research/release_gate_checklist.md +57 -0
- package/docs/site.css +505 -0
- package/docs/sparkle.md +28 -0
- package/docs/status.md +28 -0
- package/docs/sync/implementation.md +393 -0
- package/docs/sync/readme.md +317 -0
- package/docs/sync_files_summary.md +391 -0
- package/docs/sync_quick_reference.md +225 -0
- package/docs/todo.md +11 -0
- package/docs/ui.md +29 -0
- package/docs/web-integration.md +9 -0
- package/docs/widgets.md +85 -0
- package/docs/workflows/error-handling-improvements.md +434 -0
- package/docs/workflows/folder_structure.md +299 -0
- package/docs/workflows/loading_states.md +446 -0
- package/docs/workflows/loading_states_examples.md +565 -0
- package/docs/workflows/loading_states_flow.md +430 -0
- package/docs/workflows/performance_optimizations.md +457 -0
- package/docs/workflows/phase1-cli-testing-report.md +313 -0
- package/docs/workflows/quick_reference.md +228 -0
- package/docs/workflows/readme.md +59 -0
- package/docs/workflows/recent_improvements.md +275 -0
- package/docs/workflows/sync_enablement_summary.md +400 -0
- package/docs/zai.md +43 -0
- package/package.json +36 -0
- package/runic.png +0 -0
- package/version.env +2 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Codex OAuth resolver: tokens, refresh, usage endpoint, and fetch strategy wiring."
|
|
3
|
+
read_when:
|
|
4
|
+
- Adding or modifying Codex OAuth usage fetching
|
|
5
|
+
- Debugging auth.json parsing or token refresh behavior
|
|
6
|
+
- Adjusting Codex provider source selection
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Codex OAuth Resolver Implementation Plan
|
|
10
|
+
|
|
11
|
+
> Replicate Codex's direct OAuth token usage in Runic instead of calling the CLI.
|
|
12
|
+
|
|
13
|
+
## Background
|
|
14
|
+
|
|
15
|
+
Currently, Runic fetches Codex usage by:
|
|
16
|
+
1. Running `codex` CLI in PTY mode
|
|
17
|
+
2. Sending `/status` command
|
|
18
|
+
3. Parsing the text output
|
|
19
|
+
|
|
20
|
+
This is slow and unreliable. The goal is to directly read Codex's OAuth tokens and call the same API endpoints that Codex uses internally.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Codex OAuth Architecture (from source analysis)
|
|
25
|
+
|
|
26
|
+
### Token Storage
|
|
27
|
+
|
|
28
|
+
**Location:** `~/.codex/auth.json`
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"OPENAI_API_KEY": null,
|
|
33
|
+
"tokens": {
|
|
34
|
+
"id_token": "eyJ...",
|
|
35
|
+
"access_token": "eyJ...",
|
|
36
|
+
"refresh_token": "...",
|
|
37
|
+
"account_id": "account-..."
|
|
38
|
+
},
|
|
39
|
+
"last_refresh": "2025-12-28T12:34:56Z"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Source:** `codex-rs/core/src/auth/storage.rs`
|
|
44
|
+
|
|
45
|
+
### Token Refresh
|
|
46
|
+
|
|
47
|
+
**Endpoint:** `POST https://auth.openai.com/oauth/token`
|
|
48
|
+
|
|
49
|
+
**Request:**
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
53
|
+
"grant_type": "refresh_token",
|
|
54
|
+
"refresh_token": "<refresh_token>",
|
|
55
|
+
"scope": "openid profile email"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Response:**
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"id_token": "eyJ...",
|
|
63
|
+
"access_token": "eyJ...",
|
|
64
|
+
"refresh_token": "..."
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Refresh Interval:** 8 days (from `TOKEN_REFRESH_INTERVAL` constant)
|
|
69
|
+
|
|
70
|
+
**Source:** `codex-rs/core/src/auth.rs:504-545`
|
|
71
|
+
|
|
72
|
+
### Usage API
|
|
73
|
+
|
|
74
|
+
**Endpoint:** `GET {chatgpt_base_url}/wham/usage` (default: `https://chatgpt.com/backend-api/wham/usage`)
|
|
75
|
+
|
|
76
|
+
If `chatgpt_base_url` does not include `/backend-api`, Codex falls back to
|
|
77
|
+
`{base_url}/api/codex/usage` (see `PathStyle` in `backend-client/src/client.rs`).
|
|
78
|
+
|
|
79
|
+
**Headers:**
|
|
80
|
+
```
|
|
81
|
+
Authorization: Bearer <access_token>
|
|
82
|
+
ChatGPT-Account-Id: <account_id>
|
|
83
|
+
User-Agent: codex-cli
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Quick checks**
|
|
87
|
+
- Command: `cat ~/.codex/auth.json`
|
|
88
|
+
- Command: `curl -H "Authorization: Bearer <access_token>" -H "ChatGPT-Account-Id: <account_id>" -H "User-Agent: codex-cli" https://chatgpt.com/backend-api/wham/usage`
|
|
89
|
+
- Command: `RunicCLI usage --provider codex --source oauth --json --pretty`
|
|
90
|
+
|
|
91
|
+
**Response:**
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"plan_type": "pro",
|
|
95
|
+
"rate_limit": {
|
|
96
|
+
"primary_window": {
|
|
97
|
+
"used_percent": 15,
|
|
98
|
+
"reset_at": 1735401600,
|
|
99
|
+
"limit_window_seconds": 18000
|
|
100
|
+
},
|
|
101
|
+
"secondary_window": {
|
|
102
|
+
"used_percent": 5,
|
|
103
|
+
"reset_at": 1735920000,
|
|
104
|
+
"limit_window_seconds": 604800
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"credits": {
|
|
108
|
+
"has_credits": true,
|
|
109
|
+
"unlimited": false,
|
|
110
|
+
"balance": 150.0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Source:** `codex-rs/backend-client/src/client.rs:161-170`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Implementation
|
|
120
|
+
|
|
121
|
+
### Files to Create
|
|
122
|
+
|
|
123
|
+
| File | Location | Purpose |
|
|
124
|
+
|------|----------|---------|
|
|
125
|
+
| `CodexOAuthCredentials.swift` | `Sources/RunicCore/Providers/Codex/CodexOAuth/` | Token storage model + loader |
|
|
126
|
+
| `CodexOAuthUsageFetcher.swift` | `Sources/RunicCore/Providers/Codex/CodexOAuth/` | API client for usage endpoint |
|
|
127
|
+
| `CodexTokenRefresher.swift` | `Sources/RunicCore/Providers/Codex/CodexOAuth/` | Token refresh logic |
|
|
128
|
+
|
|
129
|
+
### Files to Modify
|
|
130
|
+
|
|
131
|
+
| File | Changes |
|
|
132
|
+
|------|---------|
|
|
133
|
+
| `CodexProviderDescriptor.swift` | Add `CodexOAuthFetchStrategy`, update `resolveStrategies()` |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### Step 1: CodexOAuthCredentials.swift
|
|
138
|
+
|
|
139
|
+
```swift
|
|
140
|
+
import Foundation
|
|
141
|
+
|
|
142
|
+
public struct CodexOAuthCredentials: Sendable {
|
|
143
|
+
public let accessToken: String
|
|
144
|
+
public let refreshToken: String
|
|
145
|
+
public let idToken: String?
|
|
146
|
+
public let accountId: String?
|
|
147
|
+
public let lastRefresh: Date?
|
|
148
|
+
|
|
149
|
+
public var needsRefresh: Bool {
|
|
150
|
+
guard let last = lastRefresh else { return true }
|
|
151
|
+
let eightDays: TimeInterval = 8 * 24 * 3600
|
|
152
|
+
return Date().timeIntervalSince(last) > eightDays
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public enum CodexOAuthCredentialsError: LocalizedError {
|
|
157
|
+
case notFound
|
|
158
|
+
case decodeFailed(String)
|
|
159
|
+
case missingTokens
|
|
160
|
+
|
|
161
|
+
public var errorDescription: String? {
|
|
162
|
+
switch self {
|
|
163
|
+
case .notFound:
|
|
164
|
+
"Codex auth.json not found. Run `codex` to log in."
|
|
165
|
+
case .decodeFailed(let msg):
|
|
166
|
+
"Failed to decode Codex credentials: \(msg)"
|
|
167
|
+
case .missingTokens:
|
|
168
|
+
"Codex auth.json exists but contains no tokens."
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public enum CodexOAuthCredentialsStore {
|
|
174
|
+
private static var authFilePath: URL {
|
|
175
|
+
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
176
|
+
// Respect CODEX_HOME if set
|
|
177
|
+
if let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"],
|
|
178
|
+
!codexHome.isEmpty {
|
|
179
|
+
return URL(fileURLWithPath: codexHome).appendingPathComponent("auth.json")
|
|
180
|
+
}
|
|
181
|
+
return home.appendingPathComponent(".codex/auth.json")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public static func load() throws -> CodexOAuthCredentials {
|
|
185
|
+
let url = authFilePath
|
|
186
|
+
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
187
|
+
throw CodexOAuthCredentialsError.notFound
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let data = try Data(contentsOf: url)
|
|
191
|
+
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
192
|
+
throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for API key auth (no tokens needed for refresh)
|
|
196
|
+
if let apiKey = json["OPENAI_API_KEY"] as? String, !apiKey.isEmpty {
|
|
197
|
+
return CodexOAuthCredentials(
|
|
198
|
+
accessToken: apiKey,
|
|
199
|
+
refreshToken: "",
|
|
200
|
+
idToken: nil,
|
|
201
|
+
accountId: nil,
|
|
202
|
+
lastRefresh: nil)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
guard let tokens = json["tokens"] as? [String: Any],
|
|
206
|
+
let accessToken = tokens["access_token"] as? String,
|
|
207
|
+
let refreshToken = tokens["refresh_token"] as? String else {
|
|
208
|
+
throw CodexOAuthCredentialsError.missingTokens
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let idToken = tokens["id_token"] as? String
|
|
212
|
+
let accountId = tokens["account_id"] as? String
|
|
213
|
+
|
|
214
|
+
let lastRefresh: Date? = {
|
|
215
|
+
guard let str = json["last_refresh"] as? String else { return nil }
|
|
216
|
+
let formatter = ISO8601DateFormatter()
|
|
217
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
218
|
+
return formatter.date(from: str) ?? ISO8601DateFormatter().date(from: str)
|
|
219
|
+
}()
|
|
220
|
+
|
|
221
|
+
return CodexOAuthCredentials(
|
|
222
|
+
accessToken: accessToken,
|
|
223
|
+
refreshToken: refreshToken,
|
|
224
|
+
idToken: idToken,
|
|
225
|
+
accountId: accountId,
|
|
226
|
+
lastRefresh: lastRefresh)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public static func save(_ credentials: CodexOAuthCredentials) throws {
|
|
230
|
+
let url = authFilePath
|
|
231
|
+
|
|
232
|
+
// Read existing file to preserve structure
|
|
233
|
+
var json: [String: Any] = [:]
|
|
234
|
+
if let data = try? Data(contentsOf: url),
|
|
235
|
+
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
236
|
+
json = existing
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
var tokens: [String: Any] = [
|
|
240
|
+
"access_token": credentials.accessToken,
|
|
241
|
+
"refresh_token": credentials.refreshToken
|
|
242
|
+
]
|
|
243
|
+
if let idToken = credentials.idToken {
|
|
244
|
+
tokens["id_token"] = idToken
|
|
245
|
+
}
|
|
246
|
+
if let accountId = credentials.accountId {
|
|
247
|
+
tokens["account_id"] = accountId
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
json["tokens"] = tokens
|
|
251
|
+
json["last_refresh"] = ISO8601DateFormatter().string(from: Date())
|
|
252
|
+
|
|
253
|
+
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
|
|
254
|
+
try data.write(to: url, options: .atomic)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Step 2: CodexOAuthUsageFetcher.swift
|
|
262
|
+
|
|
263
|
+
```swift
|
|
264
|
+
import Foundation
|
|
265
|
+
|
|
266
|
+
public struct CodexUsageResponse: Decodable, Sendable {
|
|
267
|
+
public let planType: PlanType
|
|
268
|
+
public let rateLimit: RateLimitDetails?
|
|
269
|
+
public let credits: CreditDetails?
|
|
270
|
+
|
|
271
|
+
enum CodingKeys: String, CodingKey {
|
|
272
|
+
case planType = "plan_type"
|
|
273
|
+
case rateLimit = "rate_limit"
|
|
274
|
+
case credits
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public enum PlanType: String, Decodable, Sendable {
|
|
278
|
+
case guest, free, go, plus, pro
|
|
279
|
+
case freeWorkspace = "free_workspace"
|
|
280
|
+
case team, business, education, quorum, k12, enterprise, edu
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public struct RateLimitDetails: Decodable, Sendable {
|
|
284
|
+
public let primaryWindow: WindowSnapshot?
|
|
285
|
+
public let secondaryWindow: WindowSnapshot?
|
|
286
|
+
|
|
287
|
+
enum CodingKeys: String, CodingKey {
|
|
288
|
+
case primaryWindow = "primary_window"
|
|
289
|
+
case secondaryWindow = "secondary_window"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public struct WindowSnapshot: Decodable, Sendable {
|
|
294
|
+
public let usedPercent: Int
|
|
295
|
+
public let resetAt: Int
|
|
296
|
+
public let limitWindowSeconds: Int
|
|
297
|
+
|
|
298
|
+
enum CodingKeys: String, CodingKey {
|
|
299
|
+
case usedPercent = "used_percent"
|
|
300
|
+
case resetAt = "reset_at"
|
|
301
|
+
case limitWindowSeconds = "limit_window_seconds"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public struct CreditDetails: Decodable, Sendable {
|
|
306
|
+
public let hasCredits: Bool
|
|
307
|
+
public let unlimited: Bool
|
|
308
|
+
public let balance: Double?
|
|
309
|
+
|
|
310
|
+
enum CodingKeys: String, CodingKey {
|
|
311
|
+
case hasCredits = "has_credits"
|
|
312
|
+
case unlimited
|
|
313
|
+
case balance
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public enum CodexOAuthFetchError: LocalizedError, Sendable {
|
|
319
|
+
case unauthorized
|
|
320
|
+
case invalidResponse
|
|
321
|
+
case serverError(Int, String?)
|
|
322
|
+
case networkError(Error)
|
|
323
|
+
|
|
324
|
+
public var errorDescription: String? {
|
|
325
|
+
switch self {
|
|
326
|
+
case .unauthorized:
|
|
327
|
+
"Codex OAuth token expired or invalid. Run `codex` to re-authenticate."
|
|
328
|
+
case .invalidResponse:
|
|
329
|
+
"Invalid response from Codex usage API."
|
|
330
|
+
case .serverError(let code, let msg):
|
|
331
|
+
"Codex API error \(code): \(msg ?? "unknown")"
|
|
332
|
+
case .networkError(let error):
|
|
333
|
+
"Network error: \(error.localizedDescription)"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public enum CodexOAuthUsageFetcher {
|
|
339
|
+
private static let defaultChatGPTBaseURL = "https://chatgpt.com/backend-api/"
|
|
340
|
+
private static let chatGPTUsagePath = "/wham/usage"
|
|
341
|
+
private static let codexUsagePath = "/api/codex/usage"
|
|
342
|
+
|
|
343
|
+
public static func fetchUsage(
|
|
344
|
+
accessToken: String,
|
|
345
|
+
accountId: String?
|
|
346
|
+
) async throws -> CodexUsageResponse {
|
|
347
|
+
var request = URLRequest(url: resolveUsageURL())
|
|
348
|
+
request.httpMethod = "GET"
|
|
349
|
+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
350
|
+
request.setValue("Runic", forHTTPHeaderField: "User-Agent")
|
|
351
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
352
|
+
|
|
353
|
+
if let accountId {
|
|
354
|
+
request.setValue(accountId, forHTTPHeaderField: "ChatGPT-Account-Id")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let (data, response): (Data, URLResponse)
|
|
358
|
+
do {
|
|
359
|
+
(data, response) = try await URLSession.shared.data(for: request)
|
|
360
|
+
} catch {
|
|
361
|
+
throw CodexOAuthFetchError.networkError(error)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
guard let http = response as? HTTPURLResponse else {
|
|
365
|
+
throw CodexOAuthFetchError.invalidResponse
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
switch http.statusCode {
|
|
369
|
+
case 200...299:
|
|
370
|
+
do {
|
|
371
|
+
return try JSONDecoder().decode(CodexUsageResponse.self, from: data)
|
|
372
|
+
} catch {
|
|
373
|
+
throw CodexOAuthFetchError.invalidResponse
|
|
374
|
+
}
|
|
375
|
+
case 401, 403:
|
|
376
|
+
throw CodexOAuthFetchError.unauthorized
|
|
377
|
+
default:
|
|
378
|
+
let body = String(data: data, encoding: .utf8)
|
|
379
|
+
throw CodexOAuthFetchError.serverError(http.statusCode, body)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### Step 3: CodexTokenRefresher.swift
|
|
388
|
+
|
|
389
|
+
```swift
|
|
390
|
+
import Foundation
|
|
391
|
+
|
|
392
|
+
public enum CodexTokenRefresher {
|
|
393
|
+
private static let refreshEndpoint = URL(string: "https://auth.openai.com/oauth/token")!
|
|
394
|
+
private static let clientID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
395
|
+
|
|
396
|
+
public enum RefreshError: LocalizedError {
|
|
397
|
+
case expired
|
|
398
|
+
case revoked
|
|
399
|
+
case reused
|
|
400
|
+
case networkError(Error)
|
|
401
|
+
case invalidResponse(String)
|
|
402
|
+
|
|
403
|
+
public var errorDescription: String? {
|
|
404
|
+
switch self {
|
|
405
|
+
case .expired:
|
|
406
|
+
"Refresh token expired. Please run `codex` to log in again."
|
|
407
|
+
case .revoked:
|
|
408
|
+
"Refresh token was revoked. Please run `codex` to log in again."
|
|
409
|
+
case .reused:
|
|
410
|
+
"Refresh token was already used. Please run `codex` to log in again."
|
|
411
|
+
case .networkError(let error):
|
|
412
|
+
"Network error during token refresh: \(error.localizedDescription)"
|
|
413
|
+
case .invalidResponse(let msg):
|
|
414
|
+
"Invalid refresh response: \(msg)"
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
public static func refresh(_ credentials: CodexOAuthCredentials) async throws -> CodexOAuthCredentials {
|
|
420
|
+
guard !credentials.refreshToken.isEmpty else {
|
|
421
|
+
// API key auth - no refresh needed
|
|
422
|
+
return credentials
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
var request = URLRequest(url: refreshEndpoint)
|
|
426
|
+
request.httpMethod = "POST"
|
|
427
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
428
|
+
|
|
429
|
+
let body: [String: String] = [
|
|
430
|
+
"client_id": clientID,
|
|
431
|
+
"grant_type": "refresh_token",
|
|
432
|
+
"refresh_token": credentials.refreshToken,
|
|
433
|
+
"scope": "openid profile email"
|
|
434
|
+
]
|
|
435
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
436
|
+
|
|
437
|
+
let (data, response): (Data, URLResponse)
|
|
438
|
+
do {
|
|
439
|
+
(data, response) = try await URLSession.shared.data(for: request)
|
|
440
|
+
} catch {
|
|
441
|
+
throw RefreshError.networkError(error)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
guard let http = response as? HTTPURLResponse else {
|
|
445
|
+
throw RefreshError.invalidResponse("No HTTP response")
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if http.statusCode == 401 {
|
|
449
|
+
// Parse error code to classify failure
|
|
450
|
+
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
451
|
+
let errorCode = (json["error"] as? [String: Any])?["code"] as? String
|
|
452
|
+
?? json["error"] as? String
|
|
453
|
+
?? json["code"] as? String {
|
|
454
|
+
switch errorCode.lowercased() {
|
|
455
|
+
case "refresh_token_expired": throw RefreshError.expired
|
|
456
|
+
case "refresh_token_reused": throw RefreshError.reused
|
|
457
|
+
case "refresh_token_invalidated": throw RefreshError.revoked
|
|
458
|
+
default: throw RefreshError.expired
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
throw RefreshError.expired
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
guard http.statusCode == 200 else {
|
|
465
|
+
throw RefreshError.invalidResponse("Status \(http.statusCode)")
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
469
|
+
throw RefreshError.invalidResponse("Invalid JSON")
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let newAccessToken = json["access_token"] as? String ?? credentials.accessToken
|
|
473
|
+
let newRefreshToken = json["refresh_token"] as? String ?? credentials.refreshToken
|
|
474
|
+
let newIdToken = json["id_token"] as? String ?? credentials.idToken
|
|
475
|
+
|
|
476
|
+
return CodexOAuthCredentials(
|
|
477
|
+
accessToken: newAccessToken,
|
|
478
|
+
refreshToken: newRefreshToken,
|
|
479
|
+
idToken: newIdToken,
|
|
480
|
+
accountId: credentials.accountId,
|
|
481
|
+
lastRefresh: Date())
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
### Step 4: Update CodexProviderDescriptor.swift
|
|
489
|
+
|
|
490
|
+
Add OAuth to `sourceModes` and create new strategy:
|
|
491
|
+
|
|
492
|
+
```swift
|
|
493
|
+
// In makeDescriptor(), update fetchPlan:
|
|
494
|
+
fetchPlan: ProviderFetchPlan(
|
|
495
|
+
sourceModes: [.auto, .oauth, .web, .cli], // Add .oauth
|
|
496
|
+
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
|
|
497
|
+
|
|
498
|
+
// Update resolveStrategies:
|
|
499
|
+
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
|
|
500
|
+
let oauth = CodexOAuthFetchStrategy()
|
|
501
|
+
let cli = CodexCLIUsageStrategy()
|
|
502
|
+
let web = CodexWebDashboardStrategy()
|
|
503
|
+
|
|
504
|
+
switch context.sourceMode {
|
|
505
|
+
case .oauth:
|
|
506
|
+
return [oauth]
|
|
507
|
+
case .web:
|
|
508
|
+
return [web]
|
|
509
|
+
case .cli:
|
|
510
|
+
return [cli]
|
|
511
|
+
case .auto:
|
|
512
|
+
// OAuth first (fast), CLI fallback
|
|
513
|
+
if context.runtime == .cli {
|
|
514
|
+
return [web, cli]
|
|
515
|
+
}
|
|
516
|
+
return [oauth, cli]
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Add new strategy:
|
|
521
|
+
struct CodexOAuthFetchStrategy: ProviderFetchStrategy {
|
|
522
|
+
let id: String = "codex.oauth"
|
|
523
|
+
let kind: ProviderFetchKind = .oauth
|
|
524
|
+
|
|
525
|
+
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
|
|
526
|
+
(try? CodexOAuthCredentialsStore.load()) != nil
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
|
|
530
|
+
var creds = try CodexOAuthCredentialsStore.load()
|
|
531
|
+
|
|
532
|
+
// Refresh if needed (8+ days old)
|
|
533
|
+
if creds.needsRefresh && !creds.refreshToken.isEmpty {
|
|
534
|
+
creds = try await CodexTokenRefresher.refresh(creds)
|
|
535
|
+
try CodexOAuthCredentialsStore.save(creds)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let usage = try await CodexOAuthUsageFetcher.fetchUsage(
|
|
539
|
+
accessToken: creds.accessToken,
|
|
540
|
+
accountId: creds.accountId)
|
|
541
|
+
|
|
542
|
+
return makeResult(
|
|
543
|
+
usage: Self.mapUsage(usage),
|
|
544
|
+
credits: Self.mapCredits(usage.credits),
|
|
545
|
+
sourceLabel: "oauth")
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
|
|
549
|
+
// Fallback to CLI on auth errors
|
|
550
|
+
if let fetchError = error as? CodexOAuthFetchError {
|
|
551
|
+
switch fetchError {
|
|
552
|
+
case .unauthorized: return true
|
|
553
|
+
default: return false
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if error is CodexOAuthCredentialsError { return true }
|
|
557
|
+
if error is CodexTokenRefresher.RefreshError { return true }
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private static func mapUsage(_ response: CodexUsageResponse) -> UsageSnapshot {
|
|
562
|
+
let primary: RateWindow? = response.rateLimit?.primaryWindow.map { window in
|
|
563
|
+
RateWindow(
|
|
564
|
+
usedPercent: Double(window.usedPercent),
|
|
565
|
+
windowMinutes: window.limitWindowSeconds / 60,
|
|
566
|
+
resetsAt: Date(timeIntervalSince1970: TimeInterval(window.resetAt)),
|
|
567
|
+
resetDescription: nil)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let secondary: RateWindow? = response.rateLimit?.secondaryWindow.map { window in
|
|
571
|
+
RateWindow(
|
|
572
|
+
usedPercent: Double(window.usedPercent),
|
|
573
|
+
windowMinutes: window.limitWindowSeconds / 60,
|
|
574
|
+
resetsAt: Date(timeIntervalSince1970: TimeInterval(window.resetAt)),
|
|
575
|
+
resetDescription: nil)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let identity = ProviderIdentitySnapshot(
|
|
579
|
+
providerID: .codex,
|
|
580
|
+
accountEmail: nil,
|
|
581
|
+
accountOrganization: nil,
|
|
582
|
+
loginMethod: response.planType.rawValue)
|
|
583
|
+
|
|
584
|
+
return UsageSnapshot(
|
|
585
|
+
primary: primary ?? RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
586
|
+
secondary: secondary,
|
|
587
|
+
tertiary: nil,
|
|
588
|
+
providerCost: nil,
|
|
589
|
+
updatedAt: Date(),
|
|
590
|
+
identity: identity)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private static func mapCredits(_ credits: CodexUsageResponse.CreditDetails?) -> CreditsSnapshot? {
|
|
594
|
+
guard let credits else { return nil }
|
|
595
|
+
return CreditsSnapshot(
|
|
596
|
+
hasCredits: credits.hasCredits,
|
|
597
|
+
unlimited: credits.unlimited,
|
|
598
|
+
balance: credits.balance)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Constants Reference
|
|
606
|
+
|
|
607
|
+
| Constant | Value | Source |
|
|
608
|
+
|----------|-------|--------|
|
|
609
|
+
| Client ID | `app_EMoamEEZ73f0CkXaXp7hrann` | `auth.rs:618` |
|
|
610
|
+
| Refresh URL | `https://auth.openai.com/oauth/token` | `auth.rs:66` |
|
|
611
|
+
| Usage URL | `https://chatgpt.com/backend-api/wham/usage` (default) | `client.rs:163` |
|
|
612
|
+
| Token refresh interval | 8 days | `auth.rs:59` |
|
|
613
|
+
| Auth file | `~/.codex/auth.json` | `storage.rs` |
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Testing
|
|
618
|
+
|
|
619
|
+
1. Ensure `~/.codex/auth.json` exists (run `codex` to log in first)
|
|
620
|
+
2. Run Runic with debug logging enabled
|
|
621
|
+
3. Verify OAuth strategy is selected and API calls succeed
|
|
622
|
+
4. Test token refresh by manually setting `last_refresh` to old date
|
|
623
|
+
5. Test fallback by temporarily renaming auth.json
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Error Handling
|
|
628
|
+
|
|
629
|
+
| Error | Behavior |
|
|
630
|
+
|-------|----------|
|
|
631
|
+
| No auth.json | Fall back to CLI strategy |
|
|
632
|
+
| Token expired | Attempt refresh, fall back to CLI on failure |
|
|
633
|
+
| Refresh failed | Log error, fall back to CLI |
|
|
634
|
+
| API error | Fall back to CLI |
|
|
635
|
+
| Network error | Retry with backoff, then fall back |
|