@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,780 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Silo
|
|
3
|
+
#if canImport(FoundationNetworking)
|
|
4
|
+
import FoundationNetworking
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
/// Fetches Claude usage data directly from the claude.ai API using browser session cookies.
|
|
8
|
+
///
|
|
9
|
+
/// This approach mirrors what Claude Usage Tracker does, but automatically extracts the session key
|
|
10
|
+
/// from browser cookies instead of requiring manual setup.
|
|
11
|
+
///
|
|
12
|
+
/// API endpoints used:
|
|
13
|
+
/// - `GET https://claude.ai/api/organizations` → get org UUID
|
|
14
|
+
/// - `GET https://claude.ai/api/organizations/{org_id}/usage` → usage percentages + reset times
|
|
15
|
+
public enum ClaudeWebAPIFetcher {
|
|
16
|
+
private static let baseURL = "https://claude.ai/api"
|
|
17
|
+
private static let maxProbeBytes = 200_000
|
|
18
|
+
#if os(macOS)
|
|
19
|
+
private static let cookieClient = BrowserCookieClient()
|
|
20
|
+
private static let cookieImportOrder: BrowserCookieImportOrder =
|
|
21
|
+
ProviderDefaults.metadata[.claude]?.browserCookieOrder ?? Browser.defaultImportOrder
|
|
22
|
+
#else
|
|
23
|
+
private static let cookieImportOrder: BrowserCookieImportOrder = []
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
public struct OrganizationInfo: Sendable {
|
|
27
|
+
public let id: String
|
|
28
|
+
public let name: String?
|
|
29
|
+
|
|
30
|
+
public init(id: String, name: String?) {
|
|
31
|
+
self.id = id
|
|
32
|
+
self.name = name
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public struct SessionKeyInfo: Sendable {
|
|
37
|
+
public let key: String
|
|
38
|
+
public let sourceLabel: String
|
|
39
|
+
public let cookieCount: Int
|
|
40
|
+
|
|
41
|
+
public init(key: String, sourceLabel: String, cookieCount: Int) {
|
|
42
|
+
self.key = key
|
|
43
|
+
self.sourceLabel = sourceLabel
|
|
44
|
+
self.cookieCount = cookieCount
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public enum FetchError: LocalizedError, Sendable {
|
|
49
|
+
case noSessionKeyFound
|
|
50
|
+
case invalidSessionKey
|
|
51
|
+
case notSupportedOnThisPlatform
|
|
52
|
+
case networkError(Error)
|
|
53
|
+
case invalidResponse
|
|
54
|
+
case unauthorized
|
|
55
|
+
case serverError(statusCode: Int)
|
|
56
|
+
case noOrganization
|
|
57
|
+
|
|
58
|
+
public var errorDescription: String? {
|
|
59
|
+
switch self {
|
|
60
|
+
case .noSessionKeyFound:
|
|
61
|
+
"No Claude session key found in browser cookies."
|
|
62
|
+
case .invalidSessionKey:
|
|
63
|
+
"Invalid Claude session key format."
|
|
64
|
+
case .notSupportedOnThisPlatform:
|
|
65
|
+
"Claude web fetching is only supported on macOS."
|
|
66
|
+
case let .networkError(error):
|
|
67
|
+
"Network error: \(error.localizedDescription)"
|
|
68
|
+
case .invalidResponse:
|
|
69
|
+
"Invalid response from Claude API."
|
|
70
|
+
case .unauthorized:
|
|
71
|
+
"Unauthorized. Your Claude session may have expired."
|
|
72
|
+
case let .serverError(code):
|
|
73
|
+
"Claude API error: HTTP \(code)"
|
|
74
|
+
case .noOrganization:
|
|
75
|
+
"No Claude organization found for this account."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Claude usage data from the API
|
|
81
|
+
public struct WebUsageData: Sendable {
|
|
82
|
+
public let sessionPercentUsed: Double
|
|
83
|
+
public let sessionResetsAt: Date?
|
|
84
|
+
public let weeklyPercentUsed: Double?
|
|
85
|
+
public let weeklyResetsAt: Date?
|
|
86
|
+
public let opusPercentUsed: Double?
|
|
87
|
+
public let extraUsageCost: ProviderCostSnapshot?
|
|
88
|
+
public let accountOrganization: String?
|
|
89
|
+
public let accountEmail: String?
|
|
90
|
+
public let loginMethod: String?
|
|
91
|
+
|
|
92
|
+
public init(
|
|
93
|
+
sessionPercentUsed: Double,
|
|
94
|
+
sessionResetsAt: Date?,
|
|
95
|
+
weeklyPercentUsed: Double?,
|
|
96
|
+
weeklyResetsAt: Date?,
|
|
97
|
+
opusPercentUsed: Double?,
|
|
98
|
+
extraUsageCost: ProviderCostSnapshot?,
|
|
99
|
+
accountOrganization: String?,
|
|
100
|
+
accountEmail: String?,
|
|
101
|
+
loginMethod: String?)
|
|
102
|
+
{
|
|
103
|
+
self.sessionPercentUsed = sessionPercentUsed
|
|
104
|
+
self.sessionResetsAt = sessionResetsAt
|
|
105
|
+
self.weeklyPercentUsed = weeklyPercentUsed
|
|
106
|
+
self.weeklyResetsAt = weeklyResetsAt
|
|
107
|
+
self.opusPercentUsed = opusPercentUsed
|
|
108
|
+
self.extraUsageCost = extraUsageCost
|
|
109
|
+
self.accountOrganization = accountOrganization
|
|
110
|
+
self.accountEmail = accountEmail
|
|
111
|
+
self.loginMethod = loginMethod
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public struct ProbeResult: Sendable {
|
|
116
|
+
public let url: String
|
|
117
|
+
public let statusCode: Int?
|
|
118
|
+
public let contentType: String?
|
|
119
|
+
public let topLevelKeys: [String]
|
|
120
|
+
public let emails: [String]
|
|
121
|
+
public let planHints: [String]
|
|
122
|
+
public let notableFields: [String]
|
|
123
|
+
public let bodyPreview: String?
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Public API
|
|
127
|
+
|
|
128
|
+
#if os(macOS)
|
|
129
|
+
|
|
130
|
+
/// Attempts to fetch Claude usage data using cookies extracted from browsers.
|
|
131
|
+
/// Tries browser cookies using the standard import order.
|
|
132
|
+
public static func fetchUsage(logger: ((String) -> Void)? = nil) async throws -> WebUsageData {
|
|
133
|
+
let log: (String) -> Void = { msg in logger?("[claude-web] \(msg)") }
|
|
134
|
+
|
|
135
|
+
let sessionInfo = try extractSessionKeyInfo(logger: log)
|
|
136
|
+
log("Found session key: \(sessionInfo.key.prefix(20))...")
|
|
137
|
+
|
|
138
|
+
return try await self.fetchUsage(using: sessionInfo, logger: log)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public static func fetchUsage(
|
|
142
|
+
using sessionKeyInfo: SessionKeyInfo,
|
|
143
|
+
logger: ((String) -> Void)? = nil) async throws -> WebUsageData
|
|
144
|
+
{
|
|
145
|
+
let log: (String) -> Void = { msg in logger?(msg) }
|
|
146
|
+
let sessionKey = sessionKeyInfo.key
|
|
147
|
+
|
|
148
|
+
// Fetch organization info
|
|
149
|
+
let organization = try await fetchOrganizationInfo(sessionKey: sessionKey, logger: log)
|
|
150
|
+
log("Organization ID: \(organization.id)")
|
|
151
|
+
if let name = organization.name { log("Organization name: \(name)") }
|
|
152
|
+
|
|
153
|
+
var usage = try await fetchUsageData(orgId: organization.id, sessionKey: sessionKey, logger: log)
|
|
154
|
+
if usage.extraUsageCost == nil,
|
|
155
|
+
let extra = await fetchExtraUsageCost(orgId: organization.id, sessionKey: sessionKey, logger: log)
|
|
156
|
+
{
|
|
157
|
+
usage = WebUsageData(
|
|
158
|
+
sessionPercentUsed: usage.sessionPercentUsed,
|
|
159
|
+
sessionResetsAt: usage.sessionResetsAt,
|
|
160
|
+
weeklyPercentUsed: usage.weeklyPercentUsed,
|
|
161
|
+
weeklyResetsAt: usage.weeklyResetsAt,
|
|
162
|
+
opusPercentUsed: usage.opusPercentUsed,
|
|
163
|
+
extraUsageCost: extra,
|
|
164
|
+
accountOrganization: usage.accountOrganization,
|
|
165
|
+
accountEmail: usage.accountEmail,
|
|
166
|
+
loginMethod: usage.loginMethod)
|
|
167
|
+
}
|
|
168
|
+
if let account = await fetchAccountInfo(sessionKey: sessionKey, orgId: organization.id, logger: log) {
|
|
169
|
+
usage = WebUsageData(
|
|
170
|
+
sessionPercentUsed: usage.sessionPercentUsed,
|
|
171
|
+
sessionResetsAt: usage.sessionResetsAt,
|
|
172
|
+
weeklyPercentUsed: usage.weeklyPercentUsed,
|
|
173
|
+
weeklyResetsAt: usage.weeklyResetsAt,
|
|
174
|
+
opusPercentUsed: usage.opusPercentUsed,
|
|
175
|
+
extraUsageCost: usage.extraUsageCost,
|
|
176
|
+
accountOrganization: usage.accountOrganization,
|
|
177
|
+
accountEmail: account.email,
|
|
178
|
+
loginMethod: account.loginMethod)
|
|
179
|
+
}
|
|
180
|
+
if usage.accountOrganization == nil, let name = organization.name {
|
|
181
|
+
usage = WebUsageData(
|
|
182
|
+
sessionPercentUsed: usage.sessionPercentUsed,
|
|
183
|
+
sessionResetsAt: usage.sessionResetsAt,
|
|
184
|
+
weeklyPercentUsed: usage.weeklyPercentUsed,
|
|
185
|
+
weeklyResetsAt: usage.weeklyResetsAt,
|
|
186
|
+
opusPercentUsed: usage.opusPercentUsed,
|
|
187
|
+
extraUsageCost: usage.extraUsageCost,
|
|
188
|
+
accountOrganization: name,
|
|
189
|
+
accountEmail: usage.accountEmail,
|
|
190
|
+
loginMethod: usage.loginMethod)
|
|
191
|
+
}
|
|
192
|
+
return usage
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Probes a list of endpoints using the current claude.ai session cookies.
|
|
196
|
+
/// - Parameters:
|
|
197
|
+
/// - endpoints: Absolute URLs or "/api/..." paths. Supports "{orgId}" placeholder.
|
|
198
|
+
/// - includePreview: When true, includes a truncated response preview in results.
|
|
199
|
+
public static func probeEndpoints(
|
|
200
|
+
_ endpoints: [String],
|
|
201
|
+
includePreview: Bool = false,
|
|
202
|
+
logger: ((String) -> Void)? = nil) async throws -> [ProbeResult]
|
|
203
|
+
{
|
|
204
|
+
let log: (String) -> Void = { msg in logger?("[claude-probe] \(msg)") }
|
|
205
|
+
let sessionInfo = try extractSessionKeyInfo(logger: log)
|
|
206
|
+
let sessionKey = sessionInfo.key
|
|
207
|
+
let organization = try? await fetchOrganizationInfo(sessionKey: sessionKey, logger: log)
|
|
208
|
+
let expanded = endpoints.map { endpoint -> String in
|
|
209
|
+
var url = endpoint
|
|
210
|
+
if let orgId = organization?.id {
|
|
211
|
+
url = url.replacingOccurrences(of: "{orgId}", with: orgId)
|
|
212
|
+
}
|
|
213
|
+
if url.hasPrefix("/") {
|
|
214
|
+
url = "https://claude.ai\(url)"
|
|
215
|
+
}
|
|
216
|
+
return url
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var results: [ProbeResult] = []
|
|
220
|
+
results.reserveCapacity(expanded.count)
|
|
221
|
+
|
|
222
|
+
for endpoint in expanded {
|
|
223
|
+
guard let url = URL(string: endpoint) else { continue }
|
|
224
|
+
var request = URLRequest(url: url)
|
|
225
|
+
request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie")
|
|
226
|
+
request.setValue("application/json, text/html;q=0.9, */*;q=0.8", forHTTPHeaderField: "Accept")
|
|
227
|
+
request.httpMethod = "GET"
|
|
228
|
+
request.timeoutInterval = 20
|
|
229
|
+
|
|
230
|
+
do {
|
|
231
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
232
|
+
let http = response as? HTTPURLResponse
|
|
233
|
+
let contentType = http?.allHeaderFields["Content-Type"] as? String
|
|
234
|
+
let truncated = data.prefix(Self.maxProbeBytes)
|
|
235
|
+
let body = String(data: truncated, encoding: .utf8) ?? ""
|
|
236
|
+
|
|
237
|
+
let parsed = Self.parseProbeBody(data: data, fallbackText: body, contentType: contentType)
|
|
238
|
+
let preview = includePreview ? parsed.preview : nil
|
|
239
|
+
|
|
240
|
+
results.append(ProbeResult(
|
|
241
|
+
url: endpoint,
|
|
242
|
+
statusCode: http?.statusCode,
|
|
243
|
+
contentType: contentType,
|
|
244
|
+
topLevelKeys: parsed.keys,
|
|
245
|
+
emails: parsed.emails,
|
|
246
|
+
planHints: parsed.planHints,
|
|
247
|
+
notableFields: parsed.notableFields,
|
|
248
|
+
bodyPreview: preview))
|
|
249
|
+
} catch {
|
|
250
|
+
results.append(ProbeResult(
|
|
251
|
+
url: endpoint,
|
|
252
|
+
statusCode: nil,
|
|
253
|
+
contentType: nil,
|
|
254
|
+
topLevelKeys: [],
|
|
255
|
+
emails: [],
|
|
256
|
+
planHints: [],
|
|
257
|
+
notableFields: [],
|
|
258
|
+
bodyPreview: "Error: \(error.localizedDescription)"))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return results
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Checks if we can find a Claude session key in browser cookies without making API calls.
|
|
266
|
+
public static func hasSessionKey(logger: ((String) -> Void)? = nil) -> Bool {
|
|
267
|
+
do {
|
|
268
|
+
_ = try self.sessionKeyInfo(logger: logger)
|
|
269
|
+
return true
|
|
270
|
+
} catch {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public static func sessionKeyInfo(logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo {
|
|
276
|
+
try self.extractSessionKeyInfo(logger: logger)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: - Session Key Extraction
|
|
280
|
+
|
|
281
|
+
private static func extractSessionKeyInfo(logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo {
|
|
282
|
+
let log: (String) -> Void = { msg in logger?(msg) }
|
|
283
|
+
|
|
284
|
+
let cookieDomains = ["claude.ai"]
|
|
285
|
+
|
|
286
|
+
for browserSource in Self.cookieImportOrder {
|
|
287
|
+
do {
|
|
288
|
+
let query = BrowserCookieQuery(domains: cookieDomains)
|
|
289
|
+
let sources = try Self.cookieClient.records(
|
|
290
|
+
matching: query,
|
|
291
|
+
in: browserSource)
|
|
292
|
+
for source in sources {
|
|
293
|
+
if let sessionKey = findSessionKey(in: source.records.map { record in
|
|
294
|
+
(name: record.name, value: record.value)
|
|
295
|
+
}) {
|
|
296
|
+
log("Found sessionKey in \(source.label)")
|
|
297
|
+
return SessionKeyInfo(
|
|
298
|
+
key: sessionKey,
|
|
299
|
+
sourceLabel: source.label,
|
|
300
|
+
cookieCount: source.records.count)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
log("\(browserSource.displayName) cookie load failed: \(error.localizedDescription)")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw FetchError.noSessionKeyFound
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private static func findSessionKey(in cookies: [(name: String, value: String)]) -> String? {
|
|
312
|
+
for cookie in cookies where cookie.name == "sessionKey" {
|
|
313
|
+
let value = cookie.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
314
|
+
// Validate it looks like a Claude session key
|
|
315
|
+
if value.hasPrefix("sk-ant-") {
|
|
316
|
+
return value
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// MARK: - API Calls
|
|
323
|
+
|
|
324
|
+
private static func fetchOrganizationInfo(
|
|
325
|
+
sessionKey: String,
|
|
326
|
+
logger: ((String) -> Void)? = nil) async throws -> OrganizationInfo
|
|
327
|
+
{
|
|
328
|
+
let url = URL(string: "\(baseURL)/organizations")!
|
|
329
|
+
var request = URLRequest(url: url)
|
|
330
|
+
request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie")
|
|
331
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
332
|
+
request.httpMethod = "GET"
|
|
333
|
+
request.timeoutInterval = 15
|
|
334
|
+
|
|
335
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
336
|
+
|
|
337
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
338
|
+
throw FetchError.invalidResponse
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger?("Organizations API status: \(httpResponse.statusCode)")
|
|
342
|
+
|
|
343
|
+
switch httpResponse.statusCode {
|
|
344
|
+
case 200:
|
|
345
|
+
return try self.parseOrganizationResponse(data)
|
|
346
|
+
case 401, 403:
|
|
347
|
+
throw FetchError.unauthorized
|
|
348
|
+
default:
|
|
349
|
+
throw FetchError.serverError(statusCode: httpResponse.statusCode)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private static func fetchUsageData(
|
|
354
|
+
orgId: String,
|
|
355
|
+
sessionKey: String,
|
|
356
|
+
logger: ((String) -> Void)? = nil) async throws -> WebUsageData
|
|
357
|
+
{
|
|
358
|
+
let url = URL(string: "\(baseURL)/organizations/\(orgId)/usage")!
|
|
359
|
+
var request = URLRequest(url: url)
|
|
360
|
+
request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie")
|
|
361
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
362
|
+
request.httpMethod = "GET"
|
|
363
|
+
request.timeoutInterval = 15
|
|
364
|
+
|
|
365
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
366
|
+
|
|
367
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
368
|
+
throw FetchError.invalidResponse
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
logger?("Usage API status: \(httpResponse.statusCode)")
|
|
372
|
+
|
|
373
|
+
switch httpResponse.statusCode {
|
|
374
|
+
case 200:
|
|
375
|
+
return try self.parseUsageResponse(data)
|
|
376
|
+
case 401, 403:
|
|
377
|
+
throw FetchError.unauthorized
|
|
378
|
+
default:
|
|
379
|
+
throw FetchError.serverError(statusCode: httpResponse.statusCode)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private static func parseUsageResponse(_ data: Data) throws -> WebUsageData {
|
|
384
|
+
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
385
|
+
throw FetchError.invalidResponse
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Parse five_hour (session) usage
|
|
389
|
+
var sessionPercent: Double?
|
|
390
|
+
var sessionResets: Date?
|
|
391
|
+
if let fiveHour = json["five_hour"] as? [String: Any] {
|
|
392
|
+
if let utilization = fiveHour["utilization"] as? Int {
|
|
393
|
+
sessionPercent = Double(utilization)
|
|
394
|
+
}
|
|
395
|
+
if let resetsAt = fiveHour["resets_at"] as? String {
|
|
396
|
+
sessionResets = self.parseISO8601Date(resetsAt)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
guard let sessionPercent else {
|
|
400
|
+
// If we can't parse session utilization, treat this as a failure so callers can fall back to the CLI.
|
|
401
|
+
throw FetchError.invalidResponse
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Parse seven_day (weekly) usage
|
|
405
|
+
var weeklyPercent: Double?
|
|
406
|
+
var weeklyResets: Date?
|
|
407
|
+
if let sevenDay = json["seven_day"] as? [String: Any] {
|
|
408
|
+
if let utilization = sevenDay["utilization"] as? Int {
|
|
409
|
+
weeklyPercent = Double(utilization)
|
|
410
|
+
}
|
|
411
|
+
if let resetsAt = sevenDay["resets_at"] as? String {
|
|
412
|
+
weeklyResets = self.parseISO8601Date(resetsAt)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Parse seven_day_opus (Opus-specific weekly) usage
|
|
417
|
+
var opusPercent: Double?
|
|
418
|
+
if let sevenDayOpus = json["seven_day_opus"] as? [String: Any] {
|
|
419
|
+
if let utilization = sevenDayOpus["utilization"] as? Int {
|
|
420
|
+
opusPercent = Double(utilization)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return WebUsageData(
|
|
425
|
+
sessionPercentUsed: sessionPercent,
|
|
426
|
+
sessionResetsAt: sessionResets,
|
|
427
|
+
weeklyPercentUsed: weeklyPercent,
|
|
428
|
+
weeklyResetsAt: weeklyResets,
|
|
429
|
+
opusPercentUsed: opusPercent,
|
|
430
|
+
extraUsageCost: nil,
|
|
431
|
+
accountOrganization: nil,
|
|
432
|
+
accountEmail: nil,
|
|
433
|
+
loginMethod: nil)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// MARK: - Extra usage cost (Claude "Extra")
|
|
437
|
+
|
|
438
|
+
private struct OverageSpendLimitResponse: Decodable {
|
|
439
|
+
let monthlyCreditLimit: Double?
|
|
440
|
+
let currency: String?
|
|
441
|
+
let usedCredits: Double?
|
|
442
|
+
let isEnabled: Bool?
|
|
443
|
+
|
|
444
|
+
enum CodingKeys: String, CodingKey {
|
|
445
|
+
case monthlyCreditLimit = "monthly_credit_limit"
|
|
446
|
+
case currency
|
|
447
|
+
case usedCredits = "used_credits"
|
|
448
|
+
case isEnabled = "is_enabled"
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Best-effort fetch of Claude Extra spend/limit (does not fail the main usage fetch).
|
|
453
|
+
private static func fetchExtraUsageCost(
|
|
454
|
+
orgId: String,
|
|
455
|
+
sessionKey: String,
|
|
456
|
+
logger: ((String) -> Void)? = nil) async -> ProviderCostSnapshot?
|
|
457
|
+
{
|
|
458
|
+
let url = URL(string: "\(baseURL)/organizations/\(orgId)/overage_spend_limit")!
|
|
459
|
+
var request = URLRequest(url: url)
|
|
460
|
+
request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie")
|
|
461
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
462
|
+
request.httpMethod = "GET"
|
|
463
|
+
request.timeoutInterval = 15
|
|
464
|
+
|
|
465
|
+
do {
|
|
466
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
467
|
+
guard let httpResponse = response as? HTTPURLResponse else { return nil }
|
|
468
|
+
logger?("Overage API status: \(httpResponse.statusCode)")
|
|
469
|
+
guard httpResponse.statusCode == 200 else { return nil }
|
|
470
|
+
return Self.parseOverageSpendLimit(data)
|
|
471
|
+
} catch {
|
|
472
|
+
return nil
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private static func parseOverageSpendLimit(_ data: Data) -> ProviderCostSnapshot? {
|
|
477
|
+
guard let decoded = try? JSONDecoder().decode(OverageSpendLimitResponse.self, from: data) else { return nil }
|
|
478
|
+
guard decoded.isEnabled == true else { return nil }
|
|
479
|
+
guard let used = decoded.usedCredits,
|
|
480
|
+
let limit = decoded.monthlyCreditLimit,
|
|
481
|
+
let currency = decoded.currency,
|
|
482
|
+
!currency.isEmpty else { return nil }
|
|
483
|
+
|
|
484
|
+
let usedAmount = used / 100.0
|
|
485
|
+
let limitAmount = limit / 100.0
|
|
486
|
+
|
|
487
|
+
return ProviderCostSnapshot(
|
|
488
|
+
used: usedAmount,
|
|
489
|
+
limit: limitAmount,
|
|
490
|
+
currencyCode: currency,
|
|
491
|
+
period: "Monthly",
|
|
492
|
+
resetsAt: nil,
|
|
493
|
+
updatedAt: Date())
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#if DEBUG
|
|
497
|
+
|
|
498
|
+
// MARK: - Test hooks (DEBUG-only)
|
|
499
|
+
|
|
500
|
+
public static func _parseUsageResponseForTesting(_ data: Data) throws -> WebUsageData {
|
|
501
|
+
try self.parseUsageResponse(data)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
public static func _parseOrganizationsResponseForTesting(_ data: Data) throws -> OrganizationInfo {
|
|
505
|
+
try self.parseOrganizationResponse(data)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
public static func _parseOverageSpendLimitForTesting(_ data: Data) -> ProviderCostSnapshot? {
|
|
509
|
+
self.parseOverageSpendLimit(data)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
public static func _parseAccountInfoForTesting(_ data: Data, orgId: String?) -> WebAccountInfo? {
|
|
513
|
+
self.parseAccountInfo(data, orgId: orgId)
|
|
514
|
+
}
|
|
515
|
+
#endif
|
|
516
|
+
|
|
517
|
+
private static func parseISO8601Date(_ string: String) -> Date? {
|
|
518
|
+
let formatter = ISO8601DateFormatter()
|
|
519
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
520
|
+
if let date = formatter.date(from: string) {
|
|
521
|
+
return date
|
|
522
|
+
}
|
|
523
|
+
// Try without fractional seconds
|
|
524
|
+
formatter.formatOptions = [.withInternetDateTime]
|
|
525
|
+
return formatter.date(from: string)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private struct OrganizationResponse: Decodable {
|
|
529
|
+
let uuid: String
|
|
530
|
+
let name: String?
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private static func parseOrganizationResponse(_ data: Data) throws -> OrganizationInfo {
|
|
534
|
+
guard let organizations = try? JSONDecoder().decode([OrganizationResponse].self, from: data) else {
|
|
535
|
+
throw FetchError.invalidResponse
|
|
536
|
+
}
|
|
537
|
+
guard let first = organizations.first else { throw FetchError.noOrganization }
|
|
538
|
+
let name = first.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
539
|
+
let sanitized = (name?.isEmpty ?? true) ? nil : name
|
|
540
|
+
return OrganizationInfo(id: first.uuid, name: sanitized)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
public struct WebAccountInfo: Sendable {
|
|
544
|
+
public let email: String?
|
|
545
|
+
public let loginMethod: String?
|
|
546
|
+
|
|
547
|
+
public init(email: String?, loginMethod: String?) {
|
|
548
|
+
self.email = email
|
|
549
|
+
self.loginMethod = loginMethod
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private struct AccountResponse: Decodable {
|
|
554
|
+
let emailAddress: String?
|
|
555
|
+
let memberships: [Membership]?
|
|
556
|
+
|
|
557
|
+
enum CodingKeys: String, CodingKey {
|
|
558
|
+
case emailAddress = "email_address"
|
|
559
|
+
case memberships
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
struct Membership: Decodable {
|
|
563
|
+
let organization: Organization
|
|
564
|
+
|
|
565
|
+
struct Organization: Decodable {
|
|
566
|
+
let uuid: String?
|
|
567
|
+
let name: String?
|
|
568
|
+
let rateLimitTier: String?
|
|
569
|
+
let billingType: String?
|
|
570
|
+
|
|
571
|
+
enum CodingKeys: String, CodingKey {
|
|
572
|
+
case uuid
|
|
573
|
+
case name
|
|
574
|
+
case rateLimitTier = "rate_limit_tier"
|
|
575
|
+
case billingType = "billing_type"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private static func fetchAccountInfo(
|
|
582
|
+
sessionKey: String,
|
|
583
|
+
orgId: String?,
|
|
584
|
+
logger: ((String) -> Void)? = nil) async -> WebAccountInfo?
|
|
585
|
+
{
|
|
586
|
+
let url = URL(string: "\(baseURL)/account")!
|
|
587
|
+
var request = URLRequest(url: url)
|
|
588
|
+
request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie")
|
|
589
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
590
|
+
request.httpMethod = "GET"
|
|
591
|
+
request.timeoutInterval = 15
|
|
592
|
+
|
|
593
|
+
do {
|
|
594
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
595
|
+
guard let httpResponse = response as? HTTPURLResponse else { return nil }
|
|
596
|
+
logger?("Account API status: \(httpResponse.statusCode)")
|
|
597
|
+
guard httpResponse.statusCode == 200 else { return nil }
|
|
598
|
+
return Self.parseAccountInfo(data, orgId: orgId)
|
|
599
|
+
} catch {
|
|
600
|
+
return nil
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private static func parseAccountInfo(_ data: Data, orgId: String?) -> WebAccountInfo? {
|
|
605
|
+
guard let response = try? JSONDecoder().decode(AccountResponse.self, from: data) else { return nil }
|
|
606
|
+
let email = response.emailAddress?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
607
|
+
let membership = Self.selectMembership(response.memberships, orgId: orgId)
|
|
608
|
+
let plan = Self.inferPlan(
|
|
609
|
+
rateLimitTier: membership?.organization.rateLimitTier,
|
|
610
|
+
billingType: membership?.organization.billingType)
|
|
611
|
+
return WebAccountInfo(email: email, loginMethod: plan)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private static func selectMembership(
|
|
615
|
+
_ memberships: [AccountResponse.Membership]?,
|
|
616
|
+
orgId: String?) -> AccountResponse.Membership?
|
|
617
|
+
{
|
|
618
|
+
guard let memberships, !memberships.isEmpty else { return nil }
|
|
619
|
+
if let orgId {
|
|
620
|
+
if let match = memberships.first(where: { $0.organization.uuid == orgId }) { return match }
|
|
621
|
+
}
|
|
622
|
+
return memberships.first
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private static func inferPlan(rateLimitTier: String?, billingType: String?) -> String? {
|
|
626
|
+
let tier = rateLimitTier?.lowercased() ?? ""
|
|
627
|
+
let billing = billingType?.lowercased() ?? ""
|
|
628
|
+
if tier.contains("max") { return "Claude Max" }
|
|
629
|
+
if tier.contains("pro") { return "Claude Pro" }
|
|
630
|
+
if tier.contains("team") { return "Claude Team" }
|
|
631
|
+
if tier.contains("enterprise") { return "Claude Enterprise" }
|
|
632
|
+
if billing.contains("stripe"), tier.contains("claude") { return "Claude Pro" }
|
|
633
|
+
return nil
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private struct ProbeParseResult: Sendable {
|
|
637
|
+
let keys: [String]
|
|
638
|
+
let emails: [String]
|
|
639
|
+
let planHints: [String]
|
|
640
|
+
let notableFields: [String]
|
|
641
|
+
let preview: String?
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private static func parseProbeBody(
|
|
645
|
+
data: Data,
|
|
646
|
+
fallbackText: String,
|
|
647
|
+
contentType: String?) -> ProbeParseResult
|
|
648
|
+
{
|
|
649
|
+
let trimmed = fallbackText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
650
|
+
let looksJSON = (contentType?.lowercased().contains("application/json") ?? false) ||
|
|
651
|
+
trimmed.hasPrefix("{") || trimmed.hasPrefix("[")
|
|
652
|
+
|
|
653
|
+
var keys: [String] = []
|
|
654
|
+
var notableFields: [String] = []
|
|
655
|
+
if looksJSON, let json = try? JSONSerialization.jsonObject(with: data) {
|
|
656
|
+
if let dict = json as? [String: Any] {
|
|
657
|
+
keys = dict.keys.sorted()
|
|
658
|
+
} else if let array = json as? [[String: Any]], let first = array.first {
|
|
659
|
+
keys = first.keys.sorted()
|
|
660
|
+
}
|
|
661
|
+
notableFields = Self.extractNotableFields(from: json)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let emails = Self.extractEmails(from: trimmed)
|
|
665
|
+
let planHints = Self.extractPlanHints(from: trimmed)
|
|
666
|
+
let preview = trimmed.isEmpty ? nil : String(trimmed.prefix(500))
|
|
667
|
+
return ProbeParseResult(
|
|
668
|
+
keys: keys,
|
|
669
|
+
emails: emails,
|
|
670
|
+
planHints: planHints,
|
|
671
|
+
notableFields: notableFields,
|
|
672
|
+
preview: preview)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private static func extractEmails(from text: String) -> [String] {
|
|
676
|
+
let pattern = #"(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}"#
|
|
677
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] }
|
|
678
|
+
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
|
679
|
+
var results: [String] = []
|
|
680
|
+
regex.enumerateMatches(in: text, options: [], range: range) { match, _, _ in
|
|
681
|
+
guard let match, let r = Range(match.range(at: 0), in: text) else { return }
|
|
682
|
+
let value = String(text[r]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
683
|
+
if !value.isEmpty { results.append(value) }
|
|
684
|
+
}
|
|
685
|
+
return Array(Set(results)).sorted()
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private static func extractPlanHints(from text: String) -> [String] {
|
|
689
|
+
let pattern = #"(?i)\b(max|pro|team|ultra|enterprise)\b"#
|
|
690
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] }
|
|
691
|
+
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
|
692
|
+
var results: [String] = []
|
|
693
|
+
regex.enumerateMatches(in: text, options: [], range: range) { match, _, _ in
|
|
694
|
+
guard let match, let r = Range(match.range(at: 1), in: text) else { return }
|
|
695
|
+
let value = String(text[r]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
696
|
+
if !value.isEmpty { results.append(value) }
|
|
697
|
+
}
|
|
698
|
+
return Array(Set(results)).sorted()
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private static func extractNotableFields(from json: Any) -> [String] {
|
|
702
|
+
let pattern = #"(?i)(plan|tier|subscription|seat|billing|product)"#
|
|
703
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return [] }
|
|
704
|
+
var results: [String] = []
|
|
705
|
+
|
|
706
|
+
func keyMatches(_ key: String) -> Bool {
|
|
707
|
+
let range = NSRange(key.startIndex..<key.endIndex, in: key)
|
|
708
|
+
return regex.firstMatch(in: key, options: [], range: range) != nil
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
func appendValue(_ keyPath: String, value: Any) {
|
|
712
|
+
if results.count >= 40 { return }
|
|
713
|
+
let rendered: String
|
|
714
|
+
switch value {
|
|
715
|
+
case let str as String:
|
|
716
|
+
rendered = str
|
|
717
|
+
case let num as NSNumber:
|
|
718
|
+
rendered = num.stringValue
|
|
719
|
+
case let bool as Bool:
|
|
720
|
+
rendered = bool ? "true" : "false"
|
|
721
|
+
default:
|
|
722
|
+
return
|
|
723
|
+
}
|
|
724
|
+
let trimmed = rendered.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
725
|
+
guard !trimmed.isEmpty else { return }
|
|
726
|
+
results.append("\(keyPath)=\(trimmed)")
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
func walk(_ value: Any, path: String) {
|
|
730
|
+
if let dict = value as? [String: Any] {
|
|
731
|
+
for (key, nested) in dict {
|
|
732
|
+
let nextPath = path.isEmpty ? key : "\(path).\(key)"
|
|
733
|
+
if keyMatches(key) {
|
|
734
|
+
appendValue(nextPath, value: nested)
|
|
735
|
+
}
|
|
736
|
+
walk(nested, path: nextPath)
|
|
737
|
+
}
|
|
738
|
+
} else if let array = value as? [Any] {
|
|
739
|
+
for (idx, nested) in array.enumerated() {
|
|
740
|
+
let nextPath = "\(path)[\(idx)]"
|
|
741
|
+
walk(nested, path: nextPath)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
walk(json, path: "")
|
|
747
|
+
return results
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#else
|
|
751
|
+
|
|
752
|
+
public static func fetchUsage(logger: ((String) -> Void)? = nil) async throws -> WebUsageData {
|
|
753
|
+
throw FetchError.notSupportedOnThisPlatform
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
public static func fetchUsage(
|
|
757
|
+
using sessionKeyInfo: SessionKeyInfo,
|
|
758
|
+
logger: ((String) -> Void)? = nil) async throws -> WebUsageData
|
|
759
|
+
{
|
|
760
|
+
throw FetchError.notSupportedOnThisPlatform
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public static func probeEndpoints(
|
|
764
|
+
_ endpoints: [String],
|
|
765
|
+
includePreview: Bool = false,
|
|
766
|
+
logger: ((String) -> Void)? = nil) async throws -> [ProbeResult]
|
|
767
|
+
{
|
|
768
|
+
throw FetchError.notSupportedOnThisPlatform
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
public static func hasSessionKey(logger: ((String) -> Void)? = nil) -> Bool {
|
|
772
|
+
false
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
public static func sessionKeyInfo(logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo {
|
|
776
|
+
throw FetchError.notSupportedOnThisPlatform
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#endif
|
|
780
|
+
}
|