@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,676 @@
|
|
|
1
|
+
#if os(macOS)
|
|
2
|
+
import Foundation
|
|
3
|
+
import Silo
|
|
4
|
+
import WebKit
|
|
5
|
+
|
|
6
|
+
@MainActor
|
|
7
|
+
public struct OpenAIDashboardBrowserCookieImporter {
|
|
8
|
+
public struct FoundAccount: Sendable, Hashable {
|
|
9
|
+
public let sourceLabel: String
|
|
10
|
+
public let email: String
|
|
11
|
+
|
|
12
|
+
public init(sourceLabel: String, email: String) {
|
|
13
|
+
self.sourceLabel = sourceLabel
|
|
14
|
+
self.email = email
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public enum ImportError: LocalizedError {
|
|
19
|
+
case noCookiesFound
|
|
20
|
+
case browserAccessDenied(details: String)
|
|
21
|
+
case dashboardStillRequiresLogin
|
|
22
|
+
case noMatchingAccount(found: [FoundAccount])
|
|
23
|
+
|
|
24
|
+
public var errorDescription: String? {
|
|
25
|
+
switch self {
|
|
26
|
+
case .noCookiesFound:
|
|
27
|
+
return "No browser cookies found."
|
|
28
|
+
case let .browserAccessDenied(details):
|
|
29
|
+
return "Browser cookie access denied. \(details)"
|
|
30
|
+
case .dashboardStillRequiresLogin:
|
|
31
|
+
return "Browser cookies imported, but dashboard still requires login."
|
|
32
|
+
case let .noMatchingAccount(found):
|
|
33
|
+
if found.isEmpty { return "No matching OpenAI web session found in browsers." }
|
|
34
|
+
let display = found
|
|
35
|
+
.sorted { lhs, rhs in
|
|
36
|
+
if lhs.sourceLabel == rhs.sourceLabel { return lhs.email < rhs.email }
|
|
37
|
+
return lhs.sourceLabel < rhs.sourceLabel
|
|
38
|
+
}
|
|
39
|
+
.map { "\($0.sourceLabel)=\($0.email)" }
|
|
40
|
+
.joined(separator: ", ")
|
|
41
|
+
return "OpenAI web session does not match Codex account. Found: \(display)."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public struct ImportResult: Sendable {
|
|
47
|
+
public let sourceLabel: String
|
|
48
|
+
public let cookieCount: Int
|
|
49
|
+
public let signedInEmail: String?
|
|
50
|
+
public let matchesCodexEmail: Bool
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public init() {}
|
|
54
|
+
|
|
55
|
+
private struct ImportDiagnostics {
|
|
56
|
+
var mismatches: [FoundAccount] = []
|
|
57
|
+
var foundAnyCookies: Bool = false
|
|
58
|
+
var foundUnknownEmail: Bool = false
|
|
59
|
+
var accessDeniedHints: [String] = []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static let cookieDomains = ["chatgpt.com", "openai.com"]
|
|
63
|
+
private static let cookieClient = BrowserCookieClient()
|
|
64
|
+
private static let cookieImportOrder: BrowserCookieImportOrder =
|
|
65
|
+
ProviderDefaults.metadata[.codex]?.browserCookieOrder ?? Browser.defaultImportOrder
|
|
66
|
+
|
|
67
|
+
private enum CandidateEvaluation {
|
|
68
|
+
case match(candidate: Candidate, signedInEmail: String)
|
|
69
|
+
case mismatch(candidate: Candidate, signedInEmail: String)
|
|
70
|
+
case loggedIn(candidate: Candidate, signedInEmail: String)
|
|
71
|
+
case unknown(candidate: Candidate)
|
|
72
|
+
case loginRequired(candidate: Candidate)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public func importBestCookies(
|
|
76
|
+
intoAccountEmail targetEmail: String?,
|
|
77
|
+
allowAnyAccount: Bool = false,
|
|
78
|
+
logger: ((String) -> Void)? = nil) async throws -> ImportResult
|
|
79
|
+
{
|
|
80
|
+
let log: (String) -> Void = { message in
|
|
81
|
+
logger?("[web] \(message)")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let targetEmail = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
85
|
+
let normalizedTarget = targetEmail?.isEmpty == false ? targetEmail : nil
|
|
86
|
+
|
|
87
|
+
if let normalizedTarget {
|
|
88
|
+
log("Codex email: \(normalizedTarget)")
|
|
89
|
+
} else {
|
|
90
|
+
guard allowAnyAccount else {
|
|
91
|
+
throw ImportError.noCookiesFound
|
|
92
|
+
}
|
|
93
|
+
log("Codex email unknown; importing any signed-in session.")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
var diagnostics = ImportDiagnostics()
|
|
97
|
+
|
|
98
|
+
for browserSource in Self.cookieImportOrder {
|
|
99
|
+
if let match = await self.trySource(
|
|
100
|
+
browserSource,
|
|
101
|
+
targetEmail: normalizedTarget,
|
|
102
|
+
allowAnyAccount: allowAnyAccount,
|
|
103
|
+
log: log,
|
|
104
|
+
diagnostics: &diagnostics)
|
|
105
|
+
{
|
|
106
|
+
return match
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if !diagnostics.mismatches.isEmpty {
|
|
111
|
+
let found = Array(Set(diagnostics.mismatches)).sorted { lhs, rhs in
|
|
112
|
+
if lhs.sourceLabel == rhs.sourceLabel { return lhs.email < rhs.email }
|
|
113
|
+
return lhs.sourceLabel < rhs.sourceLabel
|
|
114
|
+
}
|
|
115
|
+
let emails = Array(Set(found.map(\.email))).sorted()
|
|
116
|
+
log("No matching browser session found. Candidates signed in as: \(emails.joined(separator: ", "))")
|
|
117
|
+
throw ImportError.noMatchingAccount(found: found)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if diagnostics.foundUnknownEmail || diagnostics.foundAnyCookies {
|
|
121
|
+
log("No matching browser session found (email unknown).")
|
|
122
|
+
throw ImportError.noMatchingAccount(found: [])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if !diagnostics.accessDeniedHints.isEmpty {
|
|
126
|
+
let details = diagnostics.accessDeniedHints.joined(separator: " ")
|
|
127
|
+
log("Cookie access denied: \(details)")
|
|
128
|
+
throw ImportError.browserAccessDenied(details: details)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw ImportError.noCookiesFound
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func trySafari(
|
|
135
|
+
targetEmail: String?,
|
|
136
|
+
allowAnyAccount: Bool,
|
|
137
|
+
log: @escaping (String) -> Void,
|
|
138
|
+
diagnostics: inout ImportDiagnostics) async -> ImportResult?
|
|
139
|
+
{
|
|
140
|
+
// Safari first: avoids touching Keychain ("Chrome Safe Storage") when Safari already matches.
|
|
141
|
+
do {
|
|
142
|
+
let query = BrowserCookieQuery(domains: Self.cookieDomains)
|
|
143
|
+
let sources = try Self.cookieClient.records(
|
|
144
|
+
matching: query,
|
|
145
|
+
in: .safari)
|
|
146
|
+
guard !sources.isEmpty else {
|
|
147
|
+
log("Safari contained 0 matching records.")
|
|
148
|
+
return nil
|
|
149
|
+
}
|
|
150
|
+
for source in sources {
|
|
151
|
+
let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
|
|
152
|
+
guard !cookies.isEmpty else {
|
|
153
|
+
log("\(source.label) produced 0 HTTPCookies.")
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
diagnostics.foundAnyCookies = true
|
|
158
|
+
log("Loaded \(cookies.count) cookies from \(source.label) (\(self.cookieSummary(cookies)))")
|
|
159
|
+
let candidate = Candidate(label: source.label, cookies: cookies)
|
|
160
|
+
if let match = await self.applyCandidate(
|
|
161
|
+
candidate,
|
|
162
|
+
targetEmail: targetEmail,
|
|
163
|
+
allowAnyAccount: allowAnyAccount,
|
|
164
|
+
log: log,
|
|
165
|
+
diagnostics: &diagnostics)
|
|
166
|
+
{
|
|
167
|
+
return match
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return nil
|
|
171
|
+
} catch let error as BrowserCookieError {
|
|
172
|
+
let hint = error.accessDeniedHint
|
|
173
|
+
if !hint.isEmpty {
|
|
174
|
+
diagnostics.accessDeniedHints.append(hint)
|
|
175
|
+
}
|
|
176
|
+
log("Safari cookie load failed: \(error.localizedDescription)")
|
|
177
|
+
return nil
|
|
178
|
+
} catch {
|
|
179
|
+
log("Safari cookie load failed: \(error.localizedDescription)")
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private func tryChrome(
|
|
185
|
+
targetEmail: String?,
|
|
186
|
+
allowAnyAccount: Bool,
|
|
187
|
+
log: @escaping (String) -> Void,
|
|
188
|
+
diagnostics: inout ImportDiagnostics) async -> ImportResult?
|
|
189
|
+
{
|
|
190
|
+
// Chrome fallback: may trigger Keychain prompt. Only do this if Safari didn't match.
|
|
191
|
+
do {
|
|
192
|
+
let query = BrowserCookieQuery(domains: Self.cookieDomains)
|
|
193
|
+
let chromeSources = try Self.cookieClient.records(
|
|
194
|
+
matching: query,
|
|
195
|
+
in: .chrome)
|
|
196
|
+
for source in chromeSources {
|
|
197
|
+
let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
|
|
198
|
+
if cookies.isEmpty {
|
|
199
|
+
log("\(source.label) produced 0 HTTPCookies.")
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
diagnostics.foundAnyCookies = true
|
|
203
|
+
log("Loaded \(cookies.count) cookies from \(source.label) (\(self.cookieSummary(cookies)))")
|
|
204
|
+
let candidate = Candidate(label: source.label, cookies: cookies)
|
|
205
|
+
if let match = await self.applyCandidate(
|
|
206
|
+
candidate,
|
|
207
|
+
targetEmail: targetEmail,
|
|
208
|
+
allowAnyAccount: allowAnyAccount,
|
|
209
|
+
log: log,
|
|
210
|
+
diagnostics: &diagnostics)
|
|
211
|
+
{
|
|
212
|
+
return match
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return nil
|
|
216
|
+
} catch let error as BrowserCookieError {
|
|
217
|
+
let hint = error.accessDeniedHint
|
|
218
|
+
if !hint.isEmpty {
|
|
219
|
+
diagnostics.accessDeniedHints.append(hint)
|
|
220
|
+
}
|
|
221
|
+
log("Chrome cookie load failed: \(error.localizedDescription)")
|
|
222
|
+
return nil
|
|
223
|
+
} catch {
|
|
224
|
+
log("Chrome cookie load failed: \(error.localizedDescription)")
|
|
225
|
+
return nil
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func tryFirefox(
|
|
230
|
+
targetEmail: String?,
|
|
231
|
+
allowAnyAccount: Bool,
|
|
232
|
+
log: @escaping (String) -> Void,
|
|
233
|
+
diagnostics: inout ImportDiagnostics) async -> ImportResult?
|
|
234
|
+
{
|
|
235
|
+
// Firefox fallback: no Keychain, but still only after Safari/Chrome.
|
|
236
|
+
do {
|
|
237
|
+
let query = BrowserCookieQuery(domains: Self.cookieDomains)
|
|
238
|
+
let firefoxSources = try Self.cookieClient.records(
|
|
239
|
+
matching: query,
|
|
240
|
+
in: .firefox)
|
|
241
|
+
for source in firefoxSources {
|
|
242
|
+
let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
|
|
243
|
+
if cookies.isEmpty {
|
|
244
|
+
log("\(source.label) produced 0 HTTPCookies.")
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
diagnostics.foundAnyCookies = true
|
|
248
|
+
log("Loaded \(cookies.count) cookies from \(source.label) (\(self.cookieSummary(cookies)))")
|
|
249
|
+
let candidate = Candidate(label: source.label, cookies: cookies)
|
|
250
|
+
if let match = await self.applyCandidate(
|
|
251
|
+
candidate,
|
|
252
|
+
targetEmail: targetEmail,
|
|
253
|
+
allowAnyAccount: allowAnyAccount,
|
|
254
|
+
log: log,
|
|
255
|
+
diagnostics: &diagnostics)
|
|
256
|
+
{
|
|
257
|
+
return match
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return nil
|
|
261
|
+
} catch let error as BrowserCookieError {
|
|
262
|
+
let hint = error.accessDeniedHint
|
|
263
|
+
if !hint.isEmpty {
|
|
264
|
+
diagnostics.accessDeniedHints.append(hint)
|
|
265
|
+
}
|
|
266
|
+
log("Firefox cookie load failed: \(error.localizedDescription)")
|
|
267
|
+
return nil
|
|
268
|
+
} catch {
|
|
269
|
+
log("Firefox cookie load failed: \(error.localizedDescription)")
|
|
270
|
+
return nil
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func trySource(
|
|
275
|
+
_ source: Browser,
|
|
276
|
+
targetEmail: String?,
|
|
277
|
+
allowAnyAccount: Bool,
|
|
278
|
+
log: @escaping (String) -> Void,
|
|
279
|
+
diagnostics: inout ImportDiagnostics) async -> ImportResult?
|
|
280
|
+
{
|
|
281
|
+
switch source {
|
|
282
|
+
case .safari:
|
|
283
|
+
await self.trySafari(
|
|
284
|
+
targetEmail: targetEmail,
|
|
285
|
+
allowAnyAccount: allowAnyAccount,
|
|
286
|
+
log: log,
|
|
287
|
+
diagnostics: &diagnostics)
|
|
288
|
+
case .chrome:
|
|
289
|
+
await self.tryChrome(
|
|
290
|
+
targetEmail: targetEmail,
|
|
291
|
+
allowAnyAccount: allowAnyAccount,
|
|
292
|
+
log: log,
|
|
293
|
+
diagnostics: &diagnostics)
|
|
294
|
+
case .firefox:
|
|
295
|
+
await self.tryFirefox(
|
|
296
|
+
targetEmail: targetEmail,
|
|
297
|
+
allowAnyAccount: allowAnyAccount,
|
|
298
|
+
log: log,
|
|
299
|
+
diagnostics: &diagnostics)
|
|
300
|
+
default:
|
|
301
|
+
nil
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private func applyCandidate(
|
|
306
|
+
_ candidate: Candidate,
|
|
307
|
+
targetEmail: String?,
|
|
308
|
+
allowAnyAccount: Bool,
|
|
309
|
+
log: @escaping (String) -> Void,
|
|
310
|
+
diagnostics: inout ImportDiagnostics) async -> ImportResult?
|
|
311
|
+
{
|
|
312
|
+
switch await self.evaluateCandidate(
|
|
313
|
+
candidate,
|
|
314
|
+
targetEmail: targetEmail,
|
|
315
|
+
allowAnyAccount: allowAnyAccount,
|
|
316
|
+
log: log)
|
|
317
|
+
{
|
|
318
|
+
case let .match(candidate, signedInEmail):
|
|
319
|
+
log("Selected \(candidate.label) (matches Codex: \(signedInEmail))")
|
|
320
|
+
guard let targetEmail else { return nil }
|
|
321
|
+
return try? await self.persist(candidate: candidate, targetEmail: targetEmail, logger: log)
|
|
322
|
+
case let .mismatch(candidate, signedInEmail):
|
|
323
|
+
await self.handleMismatch(
|
|
324
|
+
candidate: candidate,
|
|
325
|
+
signedInEmail: signedInEmail,
|
|
326
|
+
log: log,
|
|
327
|
+
diagnostics: &diagnostics)
|
|
328
|
+
return nil
|
|
329
|
+
case let .loggedIn(candidate, signedInEmail):
|
|
330
|
+
log("Selected \(candidate.label) (signed in: \(signedInEmail))")
|
|
331
|
+
return try? await self.persist(candidate: candidate, targetEmail: signedInEmail, logger: log)
|
|
332
|
+
case .unknown:
|
|
333
|
+
if allowAnyAccount {
|
|
334
|
+
log("Selected \(candidate.label) (signed in: unknown)")
|
|
335
|
+
return try? await self.persistToDefaultStore(candidate: candidate, logger: log)
|
|
336
|
+
}
|
|
337
|
+
diagnostics.foundUnknownEmail = true
|
|
338
|
+
return nil
|
|
339
|
+
case .loginRequired:
|
|
340
|
+
return nil
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private func evaluateCandidate(
|
|
345
|
+
_ candidate: Candidate,
|
|
346
|
+
targetEmail: String?,
|
|
347
|
+
allowAnyAccount: Bool,
|
|
348
|
+
log: @escaping (String) -> Void) async -> CandidateEvaluation
|
|
349
|
+
{
|
|
350
|
+
log("Trying candidate \(candidate.label) (\(candidate.cookies.count) cookies)")
|
|
351
|
+
|
|
352
|
+
let apiEmail = await self.fetchSignedInEmailFromAPI(cookies: candidate.cookies, logger: log)
|
|
353
|
+
if let apiEmail {
|
|
354
|
+
log("Candidate \(candidate.label) API email: \(apiEmail)")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Prefer the API email when available (fast; avoids WebKit hydration/timeout risks).
|
|
358
|
+
if let apiEmail, !apiEmail.isEmpty {
|
|
359
|
+
if let targetEmail {
|
|
360
|
+
if apiEmail.lowercased() == targetEmail.lowercased() {
|
|
361
|
+
return .match(candidate: candidate, signedInEmail: apiEmail)
|
|
362
|
+
}
|
|
363
|
+
return .mismatch(candidate: candidate, signedInEmail: apiEmail)
|
|
364
|
+
}
|
|
365
|
+
if allowAnyAccount { return .loggedIn(candidate: candidate, signedInEmail: apiEmail) }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if !self.hasSessionCookies(candidate.cookies) {
|
|
369
|
+
log("Candidate \(candidate.label) missing session cookies; skipping")
|
|
370
|
+
return .loginRequired(candidate: candidate)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let scratch = WKWebsiteDataStore.nonPersistent()
|
|
374
|
+
await self.setCookies(candidate.cookies, into: scratch)
|
|
375
|
+
|
|
376
|
+
do {
|
|
377
|
+
let probe = try await OpenAIDashboardFetcher().probeUsagePage(
|
|
378
|
+
websiteDataStore: scratch,
|
|
379
|
+
logger: log,
|
|
380
|
+
timeout: 25)
|
|
381
|
+
let signedInEmail = probe.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
382
|
+
log("Candidate \(candidate.label) DOM email: \(signedInEmail ?? "unknown")")
|
|
383
|
+
|
|
384
|
+
let resolvedEmail = signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
385
|
+
if let resolvedEmail, !resolvedEmail.isEmpty {
|
|
386
|
+
if let targetEmail {
|
|
387
|
+
if resolvedEmail.lowercased() == targetEmail.lowercased() {
|
|
388
|
+
return .match(candidate: candidate, signedInEmail: resolvedEmail)
|
|
389
|
+
}
|
|
390
|
+
return .mismatch(candidate: candidate, signedInEmail: resolvedEmail)
|
|
391
|
+
}
|
|
392
|
+
if allowAnyAccount { return .loggedIn(candidate: candidate, signedInEmail: resolvedEmail) }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return .unknown(candidate: candidate)
|
|
396
|
+
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
|
|
397
|
+
log("Candidate \(candidate.label) requires login.")
|
|
398
|
+
return .loginRequired(candidate: candidate)
|
|
399
|
+
} catch {
|
|
400
|
+
log("Candidate \(candidate.label) probe error: \(error.localizedDescription)")
|
|
401
|
+
return .unknown(candidate: candidate)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private func hasSessionCookies(_ cookies: [HTTPCookie]) -> Bool {
|
|
406
|
+
for cookie in cookies {
|
|
407
|
+
let name = cookie.name.lowercased()
|
|
408
|
+
if name.contains("session-token") || name.contains("authjs") || name.contains("next-auth") {
|
|
409
|
+
return true
|
|
410
|
+
}
|
|
411
|
+
if name == "_account" { return true }
|
|
412
|
+
}
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private func handleMismatch(
|
|
417
|
+
candidate: Candidate,
|
|
418
|
+
signedInEmail: String,
|
|
419
|
+
log: @escaping (String) -> Void,
|
|
420
|
+
diagnostics: inout ImportDiagnostics) async
|
|
421
|
+
{
|
|
422
|
+
log("Candidate \(candidate.label) mismatch (\(signedInEmail)); continuing browser search")
|
|
423
|
+
diagnostics.mismatches.append(FoundAccount(sourceLabel: candidate.label, email: signedInEmail))
|
|
424
|
+
// Mismatch still means we found a valid signed-in session. Persist it keyed by its email so if
|
|
425
|
+
// the user switches Codex accounts later, we can reuse this session immediately without another
|
|
426
|
+
// Keychain prompt.
|
|
427
|
+
await self.persistCookies(candidate: candidate, accountEmail: signedInEmail, logger: log)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private func fetchSignedInEmailFromAPI(
|
|
431
|
+
cookies: [HTTPCookie],
|
|
432
|
+
logger: (String) -> Void) async -> String?
|
|
433
|
+
{
|
|
434
|
+
let chatgptCookies = cookies.filter { $0.domain.lowercased().contains("chatgpt.com") }
|
|
435
|
+
guard !chatgptCookies.isEmpty else { return nil }
|
|
436
|
+
|
|
437
|
+
let cookieHeader = chatgptCookies
|
|
438
|
+
.map { "\($0.name)=\($0.value)" }
|
|
439
|
+
.joined(separator: "; ")
|
|
440
|
+
|
|
441
|
+
let endpoints = [
|
|
442
|
+
"https://chatgpt.com/backend-api/me",
|
|
443
|
+
"https://chatgpt.com/api/auth/session",
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
for urlString in endpoints {
|
|
447
|
+
guard let url = URL(string: urlString) else { continue }
|
|
448
|
+
var request = URLRequest(url: url)
|
|
449
|
+
request.httpMethod = "GET"
|
|
450
|
+
request.timeoutInterval = 10
|
|
451
|
+
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
|
|
452
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
453
|
+
|
|
454
|
+
do {
|
|
455
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
456
|
+
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
|
|
457
|
+
logger("API \(url.host ?? "chatgpt.com") \(url.path) status=\(status)")
|
|
458
|
+
guard status >= 200, status < 300 else { continue }
|
|
459
|
+
if let email = Self.findFirstEmail(inJSONData: data) {
|
|
460
|
+
return email.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
logger("API request failed: \(error.localizedDescription)")
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return nil
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private static func findFirstEmail(inJSONData data: Data) -> String? {
|
|
471
|
+
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
|
|
472
|
+
var queue: [Any] = [json]
|
|
473
|
+
var seen = 0
|
|
474
|
+
while !queue.isEmpty, seen < 2000 {
|
|
475
|
+
let cur = queue.removeFirst()
|
|
476
|
+
seen += 1
|
|
477
|
+
if let str = cur as? String, str.contains("@") {
|
|
478
|
+
return str
|
|
479
|
+
}
|
|
480
|
+
if let dict = cur as? [String: Any] {
|
|
481
|
+
for (k, v) in dict {
|
|
482
|
+
if k.lowercased() == "email", let s = v as? String, s.contains("@") { return s }
|
|
483
|
+
queue.append(v)
|
|
484
|
+
}
|
|
485
|
+
} else if let arr = cur as? [Any] {
|
|
486
|
+
queue.append(contentsOf: arr)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return nil
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private func persist(
|
|
493
|
+
candidate: Candidate,
|
|
494
|
+
targetEmail: String,
|
|
495
|
+
logger: @escaping (String) -> Void) async throws -> ImportResult
|
|
496
|
+
{
|
|
497
|
+
let persistent = OpenAIDashboardWebsiteDataStore.store(forAccountEmail: targetEmail)
|
|
498
|
+
await self.clearChatGPTCookies(in: persistent)
|
|
499
|
+
await self.setCookies(candidate.cookies, into: persistent)
|
|
500
|
+
|
|
501
|
+
// Validate against the persistent store (login + email sync).
|
|
502
|
+
do {
|
|
503
|
+
let probe = try await OpenAIDashboardFetcher().probeUsagePage(
|
|
504
|
+
websiteDataStore: persistent,
|
|
505
|
+
logger: logger,
|
|
506
|
+
timeout: 20)
|
|
507
|
+
let signed = probe.signedInEmail?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
508
|
+
let matches = signed?.lowercased() == targetEmail.lowercased()
|
|
509
|
+
logger("Persistent session signed in as: \(signed ?? "unknown")")
|
|
510
|
+
if signed != nil, matches == false {
|
|
511
|
+
let found = signed?.isEmpty == false
|
|
512
|
+
? [FoundAccount(sourceLabel: candidate.label, email: signed!)]
|
|
513
|
+
: []
|
|
514
|
+
throw ImportError.noMatchingAccount(found: found)
|
|
515
|
+
}
|
|
516
|
+
return ImportResult(
|
|
517
|
+
sourceLabel: candidate.label,
|
|
518
|
+
cookieCount: candidate.cookies.count,
|
|
519
|
+
signedInEmail: signed,
|
|
520
|
+
matchesCodexEmail: matches)
|
|
521
|
+
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
|
|
522
|
+
logger("Selected \(candidate.label) but dashboard still requires login.")
|
|
523
|
+
throw ImportError.dashboardStillRequiresLogin
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private func persistToDefaultStore(
|
|
528
|
+
candidate: Candidate,
|
|
529
|
+
logger: @escaping (String) -> Void) async throws -> ImportResult
|
|
530
|
+
{
|
|
531
|
+
let persistent = OpenAIDashboardWebsiteDataStore.store(forAccountEmail: nil)
|
|
532
|
+
await self.clearChatGPTCookies(in: persistent)
|
|
533
|
+
await self.setCookies(candidate.cookies, into: persistent)
|
|
534
|
+
|
|
535
|
+
do {
|
|
536
|
+
let probe = try await OpenAIDashboardFetcher().probeUsagePage(
|
|
537
|
+
websiteDataStore: persistent,
|
|
538
|
+
logger: logger,
|
|
539
|
+
timeout: 20)
|
|
540
|
+
let signed = probe.signedInEmail?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
541
|
+
logger("Persistent session signed in as: \(signed ?? "unknown")")
|
|
542
|
+
return ImportResult(
|
|
543
|
+
sourceLabel: candidate.label,
|
|
544
|
+
cookieCount: candidate.cookies.count,
|
|
545
|
+
signedInEmail: signed,
|
|
546
|
+
matchesCodexEmail: false)
|
|
547
|
+
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
|
|
548
|
+
logger("Selected \(candidate.label) but dashboard still requires login.")
|
|
549
|
+
throw ImportError.dashboardStillRequiresLogin
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// MARK: - Candidates
|
|
554
|
+
|
|
555
|
+
private struct Candidate: Sendable {
|
|
556
|
+
let label: String
|
|
557
|
+
let cookies: [HTTPCookie]
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// MARK: - WebKit cookie store
|
|
561
|
+
|
|
562
|
+
private func persistCookies(candidate: Candidate, accountEmail: String, logger: (String) -> Void) async {
|
|
563
|
+
let store = OpenAIDashboardWebsiteDataStore.store(forAccountEmail: accountEmail)
|
|
564
|
+
await self.clearChatGPTCookies(in: store)
|
|
565
|
+
await self.setCookies(candidate.cookies, into: store)
|
|
566
|
+
logger("Persisted cookies for \(accountEmail) (source=\(candidate.label))")
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private func clearChatGPTCookies(in store: WKWebsiteDataStore) async {
|
|
570
|
+
await withCheckedContinuation { cont in
|
|
571
|
+
store.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
|
|
572
|
+
let filtered = records.filter { record in
|
|
573
|
+
let name = record.displayName.lowercased()
|
|
574
|
+
return name.contains("chatgpt.com") || name.contains("openai.com")
|
|
575
|
+
}
|
|
576
|
+
store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: filtered) {
|
|
577
|
+
cont.resume()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private func setCookies(_ cookies: [HTTPCookie], into store: WKWebsiteDataStore) async {
|
|
584
|
+
for cookie in cookies {
|
|
585
|
+
await withCheckedContinuation { cont in
|
|
586
|
+
store.httpCookieStore.setCookie(cookie) { cont.resume() }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private func cookieSummary(_ cookies: [HTTPCookie]) -> String {
|
|
592
|
+
let nameCounts = Dictionary(grouping: cookies, by: \.name).mapValues { $0.count }
|
|
593
|
+
let important = [
|
|
594
|
+
"__Secure-next-auth.session-token",
|
|
595
|
+
"__Secure-next-auth.session-token.0",
|
|
596
|
+
"__Secure-next-auth.session-token.1",
|
|
597
|
+
"_account",
|
|
598
|
+
"oai-did",
|
|
599
|
+
"cf_clearance",
|
|
600
|
+
]
|
|
601
|
+
let parts: [String] = important.compactMap { name -> String? in
|
|
602
|
+
guard let c = nameCounts[name], c > 0 else { return nil }
|
|
603
|
+
return "\(name)=\(c)"
|
|
604
|
+
}
|
|
605
|
+
if parts.isEmpty { return "no key cookies detected" }
|
|
606
|
+
return parts.joined(separator: ", ")
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
#else
|
|
610
|
+
import Foundation
|
|
611
|
+
|
|
612
|
+
@MainActor
|
|
613
|
+
public struct OpenAIDashboardBrowserCookieImporter {
|
|
614
|
+
public struct FoundAccount: Sendable, Hashable {
|
|
615
|
+
public let sourceLabel: String
|
|
616
|
+
public let email: String
|
|
617
|
+
|
|
618
|
+
public init(sourceLabel: String, email: String) {
|
|
619
|
+
self.sourceLabel = sourceLabel
|
|
620
|
+
self.email = email
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
public enum ImportError: LocalizedError {
|
|
625
|
+
case noCookiesFound
|
|
626
|
+
case browserAccessDenied(details: String)
|
|
627
|
+
case dashboardStillRequiresLogin
|
|
628
|
+
case noMatchingAccount(found: [FoundAccount])
|
|
629
|
+
|
|
630
|
+
public var errorDescription: String? {
|
|
631
|
+
switch self {
|
|
632
|
+
case .noCookiesFound:
|
|
633
|
+
return "No browser cookies found."
|
|
634
|
+
case let .browserAccessDenied(details):
|
|
635
|
+
return "Browser cookie access denied. \(details)"
|
|
636
|
+
case .dashboardStillRequiresLogin:
|
|
637
|
+
return "Browser cookies imported, but dashboard still requires login."
|
|
638
|
+
case let .noMatchingAccount(found):
|
|
639
|
+
if found.isEmpty { return "No matching OpenAI web session found in browsers." }
|
|
640
|
+
let display = found
|
|
641
|
+
.sorted { lhs, rhs in
|
|
642
|
+
if lhs.sourceLabel == rhs.sourceLabel { return lhs.email < rhs.email }
|
|
643
|
+
return lhs.sourceLabel < rhs.sourceLabel
|
|
644
|
+
}
|
|
645
|
+
.map { "\($0.sourceLabel)=\($0.email)" }
|
|
646
|
+
.joined(separator: ", ")
|
|
647
|
+
return "OpenAI web session does not match Codex account. Found: \(display)."
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
public struct ImportResult: Sendable {
|
|
653
|
+
public let sourceLabel: String
|
|
654
|
+
public let cookieCount: Int
|
|
655
|
+
public let signedInEmail: String?
|
|
656
|
+
public let matchesCodexEmail: Bool
|
|
657
|
+
|
|
658
|
+
public init(sourceLabel: String, cookieCount: Int, signedInEmail: String?, matchesCodexEmail: Bool) {
|
|
659
|
+
self.sourceLabel = sourceLabel
|
|
660
|
+
self.cookieCount = cookieCount
|
|
661
|
+
self.signedInEmail = signedInEmail
|
|
662
|
+
self.matchesCodexEmail = matchesCodexEmail
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
public init() {}
|
|
667
|
+
|
|
668
|
+
public func importBestCookies(
|
|
669
|
+
intoAccountEmail _: String?,
|
|
670
|
+
allowAnyAccount _: Bool = false,
|
|
671
|
+
logger _: ((String) -> Void)? = nil) async throws -> ImportResult
|
|
672
|
+
{
|
|
673
|
+
throw ImportError.browserAccessDenied(details: "OpenAI web cookie import is only supported on macOS.")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
#endif
|