@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,694 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if canImport(Darwin)
|
|
3
|
+
import Darwin
|
|
4
|
+
#else
|
|
5
|
+
import Glibc
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
public struct RateWindow: Codable, Equatable, Sendable {
|
|
9
|
+
public let usedPercent: Double
|
|
10
|
+
public let windowMinutes: Int?
|
|
11
|
+
public let resetsAt: Date?
|
|
12
|
+
/// Optional textual reset description (used by Claude CLI UI scrape).
|
|
13
|
+
public let resetDescription: String?
|
|
14
|
+
/// Optional label for this quota window (for example, a model name).
|
|
15
|
+
public let label: String?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
usedPercent: Double,
|
|
19
|
+
windowMinutes: Int?,
|
|
20
|
+
resetsAt: Date?,
|
|
21
|
+
resetDescription: String?,
|
|
22
|
+
label: String? = nil)
|
|
23
|
+
{
|
|
24
|
+
self.usedPercent = usedPercent
|
|
25
|
+
self.windowMinutes = windowMinutes
|
|
26
|
+
self.resetsAt = resetsAt
|
|
27
|
+
self.resetDescription = resetDescription
|
|
28
|
+
self.label = label
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public var remainingPercent: Double {
|
|
32
|
+
max(0, 100 - self.usedPercent)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public struct ProviderIdentitySnapshot: Codable, Sendable {
|
|
37
|
+
public let providerID: UsageProvider?
|
|
38
|
+
public let accountEmail: String?
|
|
39
|
+
public let accountOrganization: String?
|
|
40
|
+
public let loginMethod: String?
|
|
41
|
+
|
|
42
|
+
public init(
|
|
43
|
+
providerID: UsageProvider?,
|
|
44
|
+
accountEmail: String?,
|
|
45
|
+
accountOrganization: String?,
|
|
46
|
+
loginMethod: String?)
|
|
47
|
+
{
|
|
48
|
+
self.providerID = providerID
|
|
49
|
+
self.accountEmail = accountEmail
|
|
50
|
+
self.accountOrganization = accountOrganization
|
|
51
|
+
self.loginMethod = loginMethod
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public func scoped(to provider: UsageProvider) -> ProviderIdentitySnapshot {
|
|
55
|
+
if self.providerID == provider { return self }
|
|
56
|
+
return ProviderIdentitySnapshot(
|
|
57
|
+
providerID: provider,
|
|
58
|
+
accountEmail: self.accountEmail,
|
|
59
|
+
accountOrganization: self.accountOrganization,
|
|
60
|
+
loginMethod: self.loginMethod)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public struct UsageSnapshot: Codable, Sendable {
|
|
65
|
+
public let primary: RateWindow
|
|
66
|
+
public let secondary: RateWindow?
|
|
67
|
+
public let tertiary: RateWindow?
|
|
68
|
+
public let providerCost: ProviderCostSnapshot?
|
|
69
|
+
public let zaiUsage: ZaiUsageSnapshot?
|
|
70
|
+
public let updatedAt: Date
|
|
71
|
+
public let identity: ProviderIdentitySnapshot?
|
|
72
|
+
|
|
73
|
+
private enum CodingKeys: String, CodingKey {
|
|
74
|
+
case primary
|
|
75
|
+
case secondary
|
|
76
|
+
case tertiary
|
|
77
|
+
case providerCost
|
|
78
|
+
case updatedAt
|
|
79
|
+
case identity
|
|
80
|
+
case accountEmail
|
|
81
|
+
case accountOrganization
|
|
82
|
+
case loginMethod
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public init(
|
|
86
|
+
primary: RateWindow,
|
|
87
|
+
secondary: RateWindow?,
|
|
88
|
+
tertiary: RateWindow? = nil,
|
|
89
|
+
providerCost: ProviderCostSnapshot? = nil,
|
|
90
|
+
zaiUsage: ZaiUsageSnapshot? = nil,
|
|
91
|
+
updatedAt: Date,
|
|
92
|
+
identity: ProviderIdentitySnapshot? = nil)
|
|
93
|
+
{
|
|
94
|
+
self.primary = primary
|
|
95
|
+
self.secondary = secondary
|
|
96
|
+
self.tertiary = tertiary
|
|
97
|
+
self.providerCost = providerCost
|
|
98
|
+
self.zaiUsage = zaiUsage
|
|
99
|
+
self.updatedAt = updatedAt
|
|
100
|
+
self.identity = identity
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public init(from decoder: Decoder) throws {
|
|
104
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
105
|
+
self.primary = try container.decode(RateWindow.self, forKey: .primary)
|
|
106
|
+
self.secondary = try container.decodeIfPresent(RateWindow.self, forKey: .secondary)
|
|
107
|
+
self.tertiary = try container.decodeIfPresent(RateWindow.self, forKey: .tertiary)
|
|
108
|
+
self.providerCost = try container.decodeIfPresent(ProviderCostSnapshot.self, forKey: .providerCost)
|
|
109
|
+
self.zaiUsage = nil // Not persisted, fetched fresh each time
|
|
110
|
+
self.updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
|
111
|
+
if let identity = try container.decodeIfPresent(ProviderIdentitySnapshot.self, forKey: .identity) {
|
|
112
|
+
self.identity = identity
|
|
113
|
+
} else {
|
|
114
|
+
let email = try container.decodeIfPresent(String.self, forKey: .accountEmail)
|
|
115
|
+
let organization = try container.decodeIfPresent(String.self, forKey: .accountOrganization)
|
|
116
|
+
let loginMethod = try container.decodeIfPresent(String.self, forKey: .loginMethod)
|
|
117
|
+
if email != nil || organization != nil || loginMethod != nil {
|
|
118
|
+
self.identity = ProviderIdentitySnapshot(
|
|
119
|
+
providerID: nil,
|
|
120
|
+
accountEmail: email,
|
|
121
|
+
accountOrganization: organization,
|
|
122
|
+
loginMethod: loginMethod)
|
|
123
|
+
} else {
|
|
124
|
+
self.identity = nil
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public func encode(to encoder: Encoder) throws {
|
|
130
|
+
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
131
|
+
try container.encode(self.primary, forKey: .primary)
|
|
132
|
+
if let secondary = self.secondary {
|
|
133
|
+
try container.encode(secondary, forKey: .secondary)
|
|
134
|
+
} else {
|
|
135
|
+
try container.encodeNil(forKey: .secondary)
|
|
136
|
+
}
|
|
137
|
+
try container.encodeIfPresent(self.tertiary, forKey: .tertiary)
|
|
138
|
+
try container.encodeIfPresent(self.providerCost, forKey: .providerCost)
|
|
139
|
+
try container.encode(self.updatedAt, forKey: .updatedAt)
|
|
140
|
+
try container.encodeIfPresent(self.identity, forKey: .identity)
|
|
141
|
+
try container.encodeIfPresent(self.identity?.accountEmail, forKey: .accountEmail)
|
|
142
|
+
try container.encodeIfPresent(self.identity?.accountOrganization, forKey: .accountOrganization)
|
|
143
|
+
try container.encodeIfPresent(self.identity?.loginMethod, forKey: .loginMethod)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public func identity(for provider: UsageProvider) -> ProviderIdentitySnapshot? {
|
|
147
|
+
guard let identity, identity.providerID == provider else { return nil }
|
|
148
|
+
return identity
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public func accountEmail(for provider: UsageProvider) -> String? {
|
|
152
|
+
self.identity(for: provider)?.accountEmail
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public func accountOrganization(for provider: UsageProvider) -> String? {
|
|
156
|
+
self.identity(for: provider)?.accountOrganization
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public func loginMethod(for provider: UsageProvider) -> String? {
|
|
160
|
+
self.identity(for: provider)?.loginMethod
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public func scoped(to provider: UsageProvider) -> UsageSnapshot {
|
|
164
|
+
guard let identity else { return self }
|
|
165
|
+
let scopedIdentity = identity.scoped(to: provider)
|
|
166
|
+
if scopedIdentity.providerID == identity.providerID { return self }
|
|
167
|
+
return UsageSnapshot(
|
|
168
|
+
primary: self.primary,
|
|
169
|
+
secondary: self.secondary,
|
|
170
|
+
tertiary: self.tertiary,
|
|
171
|
+
providerCost: self.providerCost,
|
|
172
|
+
zaiUsage: self.zaiUsage,
|
|
173
|
+
updatedAt: self.updatedAt,
|
|
174
|
+
identity: scopedIdentity)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public struct AccountInfo: Equatable, Sendable {
|
|
179
|
+
public let email: String?
|
|
180
|
+
public let plan: String?
|
|
181
|
+
|
|
182
|
+
public init(email: String?, plan: String?) {
|
|
183
|
+
self.email = email
|
|
184
|
+
self.plan = plan
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public enum UsageError: LocalizedError, Sendable {
|
|
189
|
+
case noSessions
|
|
190
|
+
case noRateLimitsFound
|
|
191
|
+
case decodeFailed
|
|
192
|
+
|
|
193
|
+
public var errorDescription: String? {
|
|
194
|
+
switch self {
|
|
195
|
+
case .noSessions:
|
|
196
|
+
"No Codex sessions found yet. Run at least one Codex prompt first."
|
|
197
|
+
case .noRateLimitsFound:
|
|
198
|
+
"Found sessions, but no rate limit events yet."
|
|
199
|
+
case .decodeFailed:
|
|
200
|
+
"Could not parse Codex session log."
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Codex RPC client (local process)
|
|
206
|
+
|
|
207
|
+
private struct RPCAccountResponse: Decodable {
|
|
208
|
+
let account: RPCAccountDetails?
|
|
209
|
+
let requiresOpenaiAuth: Bool?
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private enum RPCAccountDetails: Decodable {
|
|
213
|
+
case apiKey
|
|
214
|
+
case chatgpt(email: String, planType: String)
|
|
215
|
+
|
|
216
|
+
enum CodingKeys: String, CodingKey {
|
|
217
|
+
case type
|
|
218
|
+
case email
|
|
219
|
+
case planType
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
init(from decoder: Decoder) throws {
|
|
223
|
+
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
224
|
+
let type = try container.decode(String.self, forKey: .type)
|
|
225
|
+
switch type.lowercased() {
|
|
226
|
+
case "apikey":
|
|
227
|
+
self = .apiKey
|
|
228
|
+
case "chatgpt":
|
|
229
|
+
let email = try container.decodeIfPresent(String.self, forKey: .email) ?? "unknown"
|
|
230
|
+
let plan = try container.decodeIfPresent(String.self, forKey: .planType) ?? "unknown"
|
|
231
|
+
self = .chatgpt(email: email, planType: plan)
|
|
232
|
+
default:
|
|
233
|
+
throw DecodingError.dataCorruptedError(
|
|
234
|
+
forKey: .type,
|
|
235
|
+
in: container,
|
|
236
|
+
debugDescription: "Unknown account type \(type)")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private struct RPCRateLimitsResponse: Decodable, Encodable {
|
|
242
|
+
let rateLimits: RPCRateLimitSnapshot
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private struct RPCRateLimitSnapshot: Decodable, Encodable {
|
|
246
|
+
let primary: RPCRateLimitWindow?
|
|
247
|
+
let secondary: RPCRateLimitWindow?
|
|
248
|
+
let credits: RPCCreditsSnapshot?
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private struct RPCRateLimitWindow: Decodable, Encodable {
|
|
252
|
+
let usedPercent: Double
|
|
253
|
+
let windowDurationMins: Int?
|
|
254
|
+
let resetsAt: Int?
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private struct RPCCreditsSnapshot: Decodable, Encodable {
|
|
258
|
+
let hasCredits: Bool
|
|
259
|
+
let unlimited: Bool
|
|
260
|
+
let balance: String?
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private enum RPCWireError: Error, CustomStringConvertible {
|
|
264
|
+
case startFailed(String)
|
|
265
|
+
case requestFailed(String)
|
|
266
|
+
case malformed(String)
|
|
267
|
+
|
|
268
|
+
var description: String {
|
|
269
|
+
switch self {
|
|
270
|
+
case let .startFailed(message):
|
|
271
|
+
"Failed to start codex app-server: \(message)"
|
|
272
|
+
case let .requestFailed(message):
|
|
273
|
+
"RPC request failed: \(message)"
|
|
274
|
+
case let .malformed(message):
|
|
275
|
+
"Malformed response: \(message)"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// RPC helper used on background tasks; safe because we confine it to the owning task.
|
|
281
|
+
private final class CodexRPCClient: @unchecked Sendable {
|
|
282
|
+
private let process = Process()
|
|
283
|
+
private let stdinPipe = Pipe()
|
|
284
|
+
private let stdoutPipe = Pipe()
|
|
285
|
+
private let stderrPipe = Pipe()
|
|
286
|
+
private let stdoutLineStream: AsyncStream<Data>
|
|
287
|
+
private let stdoutLineContinuation: AsyncStream<Data>.Continuation
|
|
288
|
+
private var nextID = 1
|
|
289
|
+
private var processGroup: pid_t?
|
|
290
|
+
|
|
291
|
+
private final class LineBuffer: @unchecked Sendable {
|
|
292
|
+
private let lock = NSLock()
|
|
293
|
+
private var buffer = Data()
|
|
294
|
+
|
|
295
|
+
func appendAndDrainLines(_ data: Data) -> [Data] {
|
|
296
|
+
self.lock.lock()
|
|
297
|
+
defer { self.lock.unlock() }
|
|
298
|
+
|
|
299
|
+
self.buffer.append(data)
|
|
300
|
+
var out: [Data] = []
|
|
301
|
+
while let newline = self.buffer.firstIndex(of: 0x0A) {
|
|
302
|
+
let lineData = Data(self.buffer[..<newline])
|
|
303
|
+
self.buffer.removeSubrange(...newline)
|
|
304
|
+
if !lineData.isEmpty {
|
|
305
|
+
out.append(lineData)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return out
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private static func debugWriteStderr(_ message: String) {
|
|
313
|
+
#if !os(Linux)
|
|
314
|
+
fputs(message, stderr)
|
|
315
|
+
#endif
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
init(
|
|
319
|
+
executable: String = "codex",
|
|
320
|
+
arguments: [String] = ["-s", "read-only", "-a", "untrusted", "app-server"]) throws
|
|
321
|
+
{
|
|
322
|
+
var stdoutContinuation: AsyncStream<Data>.Continuation!
|
|
323
|
+
self.stdoutLineStream = AsyncStream<Data> { continuation in
|
|
324
|
+
stdoutContinuation = continuation
|
|
325
|
+
}
|
|
326
|
+
self.stdoutLineContinuation = stdoutContinuation
|
|
327
|
+
|
|
328
|
+
let resolvedExec = BinaryLocator.resolveCodexBinary()
|
|
329
|
+
?? TTYCommandRunner.which(executable)
|
|
330
|
+
|
|
331
|
+
guard let resolvedExec else {
|
|
332
|
+
throw RPCWireError.startFailed(
|
|
333
|
+
"Codex CLI not found. Install with `npm i -g @openai/codex` (or bun) then relaunch Runic.")
|
|
334
|
+
}
|
|
335
|
+
var env = ProcessInfo.processInfo.environment
|
|
336
|
+
env["PATH"] = PathBuilder.effectivePATH(
|
|
337
|
+
purposes: [.rpc, .nodeTooling],
|
|
338
|
+
env: env)
|
|
339
|
+
|
|
340
|
+
self.process.environment = env
|
|
341
|
+
self.process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
|
|
342
|
+
self.process.arguments = [resolvedExec] + arguments
|
|
343
|
+
self.process.standardInput = self.stdinPipe
|
|
344
|
+
self.process.standardOutput = self.stdoutPipe
|
|
345
|
+
self.process.standardError = self.stderrPipe
|
|
346
|
+
|
|
347
|
+
do {
|
|
348
|
+
try self.process.run()
|
|
349
|
+
} catch {
|
|
350
|
+
throw RPCWireError.startFailed(error.localizedDescription)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let pid = self.process.processIdentifier
|
|
354
|
+
if setpgid(pid, pid) == 0 {
|
|
355
|
+
self.processGroup = pid
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let stdoutHandle = self.stdoutPipe.fileHandleForReading
|
|
359
|
+
let stdoutLineContinuation = self.stdoutLineContinuation
|
|
360
|
+
let stdoutBuffer = LineBuffer()
|
|
361
|
+
stdoutHandle.readabilityHandler = { handle in
|
|
362
|
+
let data = handle.availableData
|
|
363
|
+
if data.isEmpty {
|
|
364
|
+
handle.readabilityHandler = nil
|
|
365
|
+
stdoutLineContinuation.finish()
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let lines = stdoutBuffer.appendAndDrainLines(data)
|
|
370
|
+
|
|
371
|
+
for lineData in lines {
|
|
372
|
+
stdoutLineContinuation.yield(lineData)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let stderrHandle = self.stderrPipe.fileHandleForReading
|
|
377
|
+
stderrHandle.readabilityHandler = { handle in
|
|
378
|
+
let data = handle.availableData
|
|
379
|
+
// When the child closes stderr, availableData returns empty and will keep re-firing; clear the handler
|
|
380
|
+
// to avoid a busy read loop on the file-descriptor monitoring queue.
|
|
381
|
+
if data.isEmpty {
|
|
382
|
+
handle.readabilityHandler = nil
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
guard let text = String(data: data, encoding: .utf8), !text.isEmpty else { return }
|
|
386
|
+
for line in text.split(whereSeparator: \.isNewline) {
|
|
387
|
+
Self.debugWriteStderr("[codex stderr] \(line)\n")
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func initialize(clientName: String, clientVersion: String) async throws {
|
|
393
|
+
_ = try await self.request(
|
|
394
|
+
method: "initialize",
|
|
395
|
+
params: ["clientInfo": ["name": clientName, "version": clientVersion]])
|
|
396
|
+
try self.sendNotification(method: "initialized")
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func fetchAccount() async throws -> RPCAccountResponse {
|
|
400
|
+
let message = try await self.request(method: "account/read")
|
|
401
|
+
return try self.decodeResult(from: message)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
func fetchRateLimits() async throws -> RPCRateLimitsResponse {
|
|
405
|
+
let message = try await self.request(method: "account/rateLimits/read")
|
|
406
|
+
return try self.decodeResult(from: message)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
func shutdown() {
|
|
410
|
+
self.stdoutPipe.fileHandleForReading.readabilityHandler = nil
|
|
411
|
+
self.stderrPipe.fileHandleForReading.readabilityHandler = nil
|
|
412
|
+
if self.process.isRunning {
|
|
413
|
+
self.process.terminate()
|
|
414
|
+
}
|
|
415
|
+
if let pgid = self.processGroup {
|
|
416
|
+
kill(-pgid, SIGTERM)
|
|
417
|
+
}
|
|
418
|
+
let waitDeadline = Date().addingTimeInterval(2.0)
|
|
419
|
+
while self.process.isRunning, Date() < waitDeadline {
|
|
420
|
+
usleep(100_000)
|
|
421
|
+
}
|
|
422
|
+
if self.process.isRunning {
|
|
423
|
+
if let pgid = self.processGroup {
|
|
424
|
+
kill(-pgid, SIGKILL)
|
|
425
|
+
}
|
|
426
|
+
kill(self.process.processIdentifier, SIGKILL)
|
|
427
|
+
}
|
|
428
|
+
try? self.stdinPipe.fileHandleForWriting.close()
|
|
429
|
+
try? self.stdoutPipe.fileHandleForReading.close()
|
|
430
|
+
try? self.stderrPipe.fileHandleForReading.close()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// MARK: - JSON-RPC helpers
|
|
434
|
+
|
|
435
|
+
private func request(method: String, params: [String: Any]? = nil) async throws -> [String: Any] {
|
|
436
|
+
let id = self.nextID
|
|
437
|
+
self.nextID += 1
|
|
438
|
+
try self.sendRequest(id: id, method: method, params: params)
|
|
439
|
+
|
|
440
|
+
while true {
|
|
441
|
+
let message = try await self.readNextMessage()
|
|
442
|
+
|
|
443
|
+
if message["id"] == nil, let methodName = message["method"] as? String {
|
|
444
|
+
Self.debugWriteStderr("[codex notify] \(methodName)\n")
|
|
445
|
+
continue
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
guard let messageID = self.jsonID(message["id"]), messageID == id else { continue }
|
|
449
|
+
|
|
450
|
+
if let error = message["error"] as? [String: Any], let messageText = error["message"] as? String {
|
|
451
|
+
throw RPCWireError.requestFailed(messageText)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return message
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private func sendNotification(method: String, params: [String: Any]? = nil) throws {
|
|
459
|
+
let paramsValue: Any = params ?? [:]
|
|
460
|
+
try self.sendPayload(["method": method, "params": paramsValue])
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private func sendRequest(id: Int, method: String, params: [String: Any]?) throws {
|
|
464
|
+
let paramsValue: Any = params ?? [:]
|
|
465
|
+
let payload: [String: Any] = ["id": id, "method": method, "params": paramsValue]
|
|
466
|
+
try self.sendPayload(payload)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private func sendPayload(_ payload: [String: Any]) throws {
|
|
470
|
+
let data = try JSONSerialization.data(withJSONObject: payload)
|
|
471
|
+
self.stdinPipe.fileHandleForWriting.write(data)
|
|
472
|
+
self.stdinPipe.fileHandleForWriting.write(Data([0x0A]))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func readNextMessage() async throws -> [String: Any] {
|
|
476
|
+
for await lineData in self.stdoutLineStream {
|
|
477
|
+
if lineData.isEmpty { continue }
|
|
478
|
+
if let json = try? JSONSerialization.jsonObject(with: lineData) as? [String: Any] {
|
|
479
|
+
return json
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
throw RPCWireError.malformed("codex app-server closed stdout")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private func decodeResult<T: Decodable>(from message: [String: Any]) throws -> T {
|
|
486
|
+
guard let result = message["result"] else {
|
|
487
|
+
throw RPCWireError.malformed("missing result field")
|
|
488
|
+
}
|
|
489
|
+
let data = try JSONSerialization.data(withJSONObject: result)
|
|
490
|
+
let decoder = JSONDecoder()
|
|
491
|
+
return try decoder.decode(T.self, from: data)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private func jsonID(_ value: Any?) -> Int? {
|
|
495
|
+
switch value {
|
|
496
|
+
case let int as Int:
|
|
497
|
+
int
|
|
498
|
+
case let number as NSNumber:
|
|
499
|
+
number.intValue
|
|
500
|
+
default:
|
|
501
|
+
nil
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// MARK: - Public fetcher used by the app
|
|
507
|
+
|
|
508
|
+
public struct UsageFetcher: Sendable {
|
|
509
|
+
private let environment: [String: String]
|
|
510
|
+
|
|
511
|
+
public init(environment: [String: String] = ProcessInfo.processInfo.environment) {
|
|
512
|
+
self.environment = environment
|
|
513
|
+
LoginShellPathCache.shared.captureOnce()
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public func loadLatestUsage() async throws -> UsageSnapshot {
|
|
517
|
+
try await self.withFallback(primary: self.loadRPCUsage, secondary: self.loadTTYUsage)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private func loadRPCUsage() async throws -> UsageSnapshot {
|
|
521
|
+
let rpc = try CodexRPCClient()
|
|
522
|
+
defer { rpc.shutdown() }
|
|
523
|
+
|
|
524
|
+
try await rpc.initialize(clientName: "runic", clientVersion: "0.5.4")
|
|
525
|
+
// The app-server answers on a single stdout stream, so keep requests
|
|
526
|
+
// serialized to avoid starving one reader when multiple awaiters race
|
|
527
|
+
// for the same pipe.
|
|
528
|
+
let limits = try await rpc.fetchRateLimits().rateLimits
|
|
529
|
+
let account = try? await rpc.fetchAccount()
|
|
530
|
+
|
|
531
|
+
guard let primary = Self.makeWindow(from: limits.primary),
|
|
532
|
+
let secondary = Self.makeWindow(from: limits.secondary)
|
|
533
|
+
else {
|
|
534
|
+
throw UsageError.noRateLimitsFound
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let identity = ProviderIdentitySnapshot(
|
|
538
|
+
providerID: .codex,
|
|
539
|
+
accountEmail: account?.account.flatMap { details in
|
|
540
|
+
if case let .chatgpt(email, _) = details { email } else { nil }
|
|
541
|
+
},
|
|
542
|
+
accountOrganization: nil,
|
|
543
|
+
loginMethod: account?.account.flatMap { details in
|
|
544
|
+
if case let .chatgpt(_, plan) = details { plan } else { nil }
|
|
545
|
+
})
|
|
546
|
+
return UsageSnapshot(
|
|
547
|
+
primary: primary,
|
|
548
|
+
secondary: secondary,
|
|
549
|
+
tertiary: nil,
|
|
550
|
+
updatedAt: Date(),
|
|
551
|
+
identity: identity)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private func loadTTYUsage() async throws -> UsageSnapshot {
|
|
555
|
+
let status = try await CodexStatusProbe().fetch()
|
|
556
|
+
guard let fiveLeft = status.fiveHourPercentLeft, let weekLeft = status.weeklyPercentLeft else {
|
|
557
|
+
throw UsageError.noRateLimitsFound
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let primary = RateWindow(
|
|
561
|
+
usedPercent: max(0, 100 - Double(fiveLeft)),
|
|
562
|
+
windowMinutes: 300,
|
|
563
|
+
resetsAt: nil,
|
|
564
|
+
resetDescription: status.fiveHourResetDescription)
|
|
565
|
+
let secondary = RateWindow(
|
|
566
|
+
usedPercent: max(0, 100 - Double(weekLeft)),
|
|
567
|
+
windowMinutes: 10080,
|
|
568
|
+
resetsAt: nil,
|
|
569
|
+
resetDescription: status.weeklyResetDescription)
|
|
570
|
+
|
|
571
|
+
return UsageSnapshot(
|
|
572
|
+
primary: primary,
|
|
573
|
+
secondary: secondary,
|
|
574
|
+
tertiary: nil,
|
|
575
|
+
updatedAt: Date(),
|
|
576
|
+
identity: nil)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
public func loadLatestCredits() async throws -> CreditsSnapshot {
|
|
580
|
+
try await self.withFallback(primary: self.loadRPCCredits, secondary: self.loadTTYCredits)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private func loadRPCCredits() async throws -> CreditsSnapshot {
|
|
584
|
+
let rpc = try CodexRPCClient()
|
|
585
|
+
defer { rpc.shutdown() }
|
|
586
|
+
try await rpc.initialize(clientName: "runic", clientVersion: "0.5.4")
|
|
587
|
+
let limits = try await rpc.fetchRateLimits().rateLimits
|
|
588
|
+
guard let credits = limits.credits else { throw UsageError.noRateLimitsFound }
|
|
589
|
+
let remaining = Self.parseCredits(credits.balance)
|
|
590
|
+
return CreditsSnapshot(remaining: remaining, events: [], updatedAt: Date())
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private func loadTTYCredits() async throws -> CreditsSnapshot {
|
|
594
|
+
let status = try await CodexStatusProbe().fetch()
|
|
595
|
+
guard let credits = status.credits else { throw UsageError.noRateLimitsFound }
|
|
596
|
+
return CreditsSnapshot(remaining: credits, events: [], updatedAt: Date())
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private func withFallback<T>(
|
|
600
|
+
primary: @escaping () async throws -> T,
|
|
601
|
+
secondary: @escaping () async throws -> T) async throws -> T
|
|
602
|
+
{
|
|
603
|
+
do {
|
|
604
|
+
return try await primary()
|
|
605
|
+
} catch let primaryError {
|
|
606
|
+
do {
|
|
607
|
+
return try await secondary()
|
|
608
|
+
} catch {
|
|
609
|
+
// Preserve the original failure so callers see the primary path error.
|
|
610
|
+
throw primaryError
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
public func debugRawRateLimits() async -> String {
|
|
616
|
+
do {
|
|
617
|
+
let rpc = try CodexRPCClient()
|
|
618
|
+
defer { rpc.shutdown() }
|
|
619
|
+
try await rpc.initialize(clientName: "runic", clientVersion: "0.5.4")
|
|
620
|
+
let limits = try await rpc.fetchRateLimits()
|
|
621
|
+
let data = try JSONEncoder().encode(limits)
|
|
622
|
+
return String(data: data, encoding: .utf8) ?? "<unprintable>"
|
|
623
|
+
} catch {
|
|
624
|
+
return "Codex RPC probe failed: \(error)"
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
public func loadAccountInfo() -> AccountInfo {
|
|
629
|
+
// Keep using auth.json for quick startup (non-blocking, no RPC spin-up required).
|
|
630
|
+
let authURL = URL(fileURLWithPath: self.environment["CODEX_HOME"] ?? "\(NSHomeDirectory())/.codex")
|
|
631
|
+
.appendingPathComponent("auth.json")
|
|
632
|
+
guard let data = try? Data(contentsOf: authURL),
|
|
633
|
+
let auth = try? JSONDecoder().decode(AuthFile.self, from: data),
|
|
634
|
+
let idToken = auth.tokens?.idToken
|
|
635
|
+
else {
|
|
636
|
+
return AccountInfo(email: nil, plan: nil)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
guard let payload = UsageFetcher.parseJWT(idToken) else {
|
|
640
|
+
return AccountInfo(email: nil, plan: nil)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let authDict = payload["https://api.openai.com/auth"] as? [String: Any]
|
|
644
|
+
let profileDict = payload["https://api.openai.com/profile"] as? [String: Any]
|
|
645
|
+
|
|
646
|
+
let plan = (authDict?["chatgpt_plan_type"] as? String)
|
|
647
|
+
?? (payload["chatgpt_plan_type"] as? String)
|
|
648
|
+
|
|
649
|
+
let email = (payload["email"] as? String)
|
|
650
|
+
?? (profileDict?["email"] as? String)
|
|
651
|
+
|
|
652
|
+
return AccountInfo(email: email, plan: plan)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// MARK: - Helpers
|
|
656
|
+
|
|
657
|
+
private static func makeWindow(from rpc: RPCRateLimitWindow?) -> RateWindow? {
|
|
658
|
+
guard let rpc else { return nil }
|
|
659
|
+
let resetsAtDate = rpc.resetsAt.map { Date(timeIntervalSince1970: TimeInterval($0)) }
|
|
660
|
+
let resetDescription = resetsAtDate.map { UsageFormatter.resetDescription(from: $0) }
|
|
661
|
+
return RateWindow(
|
|
662
|
+
usedPercent: rpc.usedPercent,
|
|
663
|
+
windowMinutes: rpc.windowDurationMins,
|
|
664
|
+
resetsAt: resetsAtDate,
|
|
665
|
+
resetDescription: resetDescription)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private static func parseCredits(_ balance: String?) -> Double {
|
|
669
|
+
guard let balance, let val = Double(balance) else { return 0 }
|
|
670
|
+
return val
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
public static func parseJWT(_ token: String) -> [String: Any]? {
|
|
674
|
+
let parts = token.split(separator: ".")
|
|
675
|
+
guard parts.count >= 2 else { return nil }
|
|
676
|
+
let payloadPart = parts[1]
|
|
677
|
+
|
|
678
|
+
var padded = String(payloadPart)
|
|
679
|
+
.replacingOccurrences(of: "-", with: "+")
|
|
680
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
681
|
+
while padded.count % 4 != 0 {
|
|
682
|
+
padded.append("=")
|
|
683
|
+
}
|
|
684
|
+
guard let data = Data(base64Encoded: padded) else { return nil }
|
|
685
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
|
|
686
|
+
return json
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Minimal auth.json struct preserved from previous implementation
|
|
691
|
+
private struct AuthFile: Decodable {
|
|
692
|
+
struct Tokens: Decodable { let idToken: String? }
|
|
693
|
+
let tokens: Tokens?
|
|
694
|
+
}
|