@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,120 @@
|
|
|
1
|
+
import RunicCore
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
struct IconView: View {
|
|
6
|
+
let snapshot: UsageSnapshot?
|
|
7
|
+
let creditsRemaining: Double?
|
|
8
|
+
let isStale: Bool
|
|
9
|
+
let showLoadingAnimation: Bool
|
|
10
|
+
let style: IconStyle
|
|
11
|
+
@State private var phase: CGFloat = 0
|
|
12
|
+
@State private var displayLink = DisplayLinkDriver()
|
|
13
|
+
@State private var pattern: LoadingPattern = .knightRider
|
|
14
|
+
@State private var debugCycle = false
|
|
15
|
+
@State private var cycleIndex = 0
|
|
16
|
+
@State private var cycleCounter = 0
|
|
17
|
+
private let loadingFPS: Double = 12
|
|
18
|
+
// Advance to next pattern every N ticks when debug cycling.
|
|
19
|
+
private let cycleIntervalTicks = 20
|
|
20
|
+
private let patterns = LoadingPattern.allCases
|
|
21
|
+
|
|
22
|
+
private var isLoading: Bool { self.showLoadingAnimation && self.snapshot == nil }
|
|
23
|
+
|
|
24
|
+
var body: some View {
|
|
25
|
+
Group {
|
|
26
|
+
if let snapshot {
|
|
27
|
+
Image(nsImage: IconRenderer.makeIcon(
|
|
28
|
+
primaryRemaining: snapshot.primary.remainingPercent,
|
|
29
|
+
weeklyRemaining: snapshot.secondary?.remainingPercent,
|
|
30
|
+
creditsRemaining: self.creditsRemaining,
|
|
31
|
+
stale: self.isStale,
|
|
32
|
+
style: self.style))
|
|
33
|
+
.renderingMode(.original)
|
|
34
|
+
.interpolation(.none)
|
|
35
|
+
.frame(width: 20, height: 18, alignment: .center)
|
|
36
|
+
.padding(.horizontal, RunicSpacing.xxxs)
|
|
37
|
+
} else if self.showLoadingAnimation {
|
|
38
|
+
// Loading: animate bars with the current pattern until data arrives.
|
|
39
|
+
Image(nsImage: self.loadingImage)
|
|
40
|
+
.renderingMode(.original)
|
|
41
|
+
.interpolation(.none)
|
|
42
|
+
.frame(width: 20, height: 18, alignment: .center)
|
|
43
|
+
.padding(.horizontal, RunicSpacing.xxxs)
|
|
44
|
+
.onChange(of: self.displayLink.tick) { _, _ in
|
|
45
|
+
self.phase += 0.09 // half-speed animation
|
|
46
|
+
if self.debugCycle {
|
|
47
|
+
self.cycleCounter += 1
|
|
48
|
+
if self.cycleCounter >= self.cycleIntervalTicks {
|
|
49
|
+
self.cycleCounter = 0
|
|
50
|
+
self.cycleIndex = (self.cycleIndex + 1) % self.patterns.count
|
|
51
|
+
self.pattern = self.patterns[self.cycleIndex]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// No animation when usage/account is unavailable; show empty tracks.
|
|
57
|
+
Image(nsImage: IconRenderer.makeIcon(
|
|
58
|
+
primaryRemaining: nil,
|
|
59
|
+
weeklyRemaining: nil,
|
|
60
|
+
creditsRemaining: self.creditsRemaining,
|
|
61
|
+
stale: self.isStale,
|
|
62
|
+
style: self.style))
|
|
63
|
+
.renderingMode(.original)
|
|
64
|
+
.interpolation(.none)
|
|
65
|
+
.frame(width: 20, height: 18, alignment: .center)
|
|
66
|
+
.padding(.horizontal, RunicSpacing.xxxs)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
.onChange(of: self.isLoading, initial: true) { _, isLoading in
|
|
70
|
+
if isLoading {
|
|
71
|
+
self.displayLink.start(fps: self.loadingFPS)
|
|
72
|
+
if !self.debugCycle {
|
|
73
|
+
self.pattern = self.patterns.randomElement() ?? .knightRider
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
self.displayLink.stop()
|
|
77
|
+
self.debugCycle = false
|
|
78
|
+
self.phase = 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
.onDisappear { self.displayLink.stop() }
|
|
82
|
+
.onReceive(NotificationCenter.default.publisher(for: .runicDebugReplayAllAnimations)) { notification in
|
|
83
|
+
if let raw = notification.userInfo?["pattern"] as? String,
|
|
84
|
+
let selected = LoadingPattern(rawValue: raw)
|
|
85
|
+
{
|
|
86
|
+
self.debugCycle = false
|
|
87
|
+
self.pattern = selected
|
|
88
|
+
self.cycleIndex = self.patterns.firstIndex(of: selected) ?? 0
|
|
89
|
+
} else {
|
|
90
|
+
self.debugCycle = true
|
|
91
|
+
self.cycleIndex = 0
|
|
92
|
+
self.pattern = self.patterns.first ?? .knightRider
|
|
93
|
+
}
|
|
94
|
+
self.cycleCounter = 0
|
|
95
|
+
self.phase = 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private var loadingPrimary: Double {
|
|
100
|
+
self.pattern.value(phase: Double(self.phase))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private var loadingSecondary: Double {
|
|
104
|
+
self.pattern.value(phase: Double(self.phase + self.pattern.secondaryOffset))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private var loadingImage: NSImage {
|
|
108
|
+
if self.pattern == .unbraid {
|
|
109
|
+
let progress = self.loadingPrimary / 100
|
|
110
|
+
return IconRenderer.makeMorphIcon(progress: progress, style: self.style)
|
|
111
|
+
} else {
|
|
112
|
+
return IconRenderer.makeIcon(
|
|
113
|
+
primaryRemaining: self.loadingPrimary,
|
|
114
|
+
weeklyRemaining: self.loadingSecondary,
|
|
115
|
+
creditsRemaining: nil,
|
|
116
|
+
stale: false,
|
|
117
|
+
style: self.style)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import RunicCore
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
enum ProviderBrandIcon {
|
|
6
|
+
private static let cache = NSCache<NSString, NSImage>()
|
|
7
|
+
|
|
8
|
+
static func image(for provider: UsageProvider, size: CGFloat = 16) -> NSImage? {
|
|
9
|
+
let cacheSize = Int(size.rounded())
|
|
10
|
+
let key = "\(provider.rawValue)-\(cacheSize)" as NSString
|
|
11
|
+
if let cached = self.cache.object(forKey: key) {
|
|
12
|
+
return cached
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let baseName = ProviderDescriptorRegistry.descriptor(for: provider).branding.iconResourceName
|
|
16
|
+
guard let url = Bundle.main.url(forResource: baseName, withExtension: "svg"),
|
|
17
|
+
let image = NSImage(contentsOf: url)
|
|
18
|
+
else {
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let targetSize = NSSize(width: size, height: size)
|
|
23
|
+
image.size = targetSize
|
|
24
|
+
image.isTemplate = true
|
|
25
|
+
let tinted = self.tintedImage(image, color: self.brandColor(for: provider), size: targetSize)
|
|
26
|
+
self.cache.setObject(tinted, forKey: key)
|
|
27
|
+
return tinted
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func templateImage(for provider: UsageProvider, size: CGFloat = 16) -> NSImage? {
|
|
31
|
+
let cacheSize = Int(size.rounded())
|
|
32
|
+
let key = "\(provider.rawValue)-\(cacheSize)-template" as NSString
|
|
33
|
+
if let cached = self.cache.object(forKey: key) {
|
|
34
|
+
return cached
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let baseName = ProviderDescriptorRegistry.descriptor(for: provider).branding.iconResourceName
|
|
38
|
+
guard let url = Bundle.main.url(forResource: baseName, withExtension: "svg"),
|
|
39
|
+
let image = NSImage(contentsOf: url)
|
|
40
|
+
else {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
image.size = NSSize(width: size, height: size)
|
|
45
|
+
image.isTemplate = true
|
|
46
|
+
self.cache.setObject(image, forKey: key)
|
|
47
|
+
return image
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static func brandColor(for provider: UsageProvider) -> NSColor {
|
|
51
|
+
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
|
|
52
|
+
return NSColor(
|
|
53
|
+
calibratedRed: CGFloat(color.red),
|
|
54
|
+
green: CGFloat(color.green),
|
|
55
|
+
blue: CGFloat(color.blue),
|
|
56
|
+
alpha: 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static func tintedImage(_ image: NSImage, color: NSColor, size: NSSize) -> NSImage {
|
|
60
|
+
let rect = NSRect(origin: .zero, size: size)
|
|
61
|
+
// Use drawingHandler instead of deprecated lockFocus/unlockFocus.
|
|
62
|
+
let tinted = NSImage(size: size, flipped: false) { _ in
|
|
63
|
+
color.setFill()
|
|
64
|
+
rect.fill()
|
|
65
|
+
image.draw(in: rect, from: .zero, operation: .destinationIn, fraction: 1.0)
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
tinted.isTemplate = false
|
|
69
|
+
return tinted
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Animation presets used across the Runic UI.
|
|
4
|
+
enum RunicAnimation {
|
|
5
|
+
// MARK: - Spring Presets
|
|
6
|
+
|
|
7
|
+
/// Standard appear animation for progress bars and chart elements.
|
|
8
|
+
static let barAppear: Animation = .spring(response: 0.6, dampingFraction: 0.75)
|
|
9
|
+
|
|
10
|
+
/// Value-change animation for progress bars.
|
|
11
|
+
static let barUpdate: Animation = .spring(response: 0.5, dampingFraction: 0.8)
|
|
12
|
+
|
|
13
|
+
/// Menu content fade when switching providers.
|
|
14
|
+
static let providerSwitch: Animation = .easeInOut(duration: 0.25)
|
|
15
|
+
|
|
16
|
+
/// Highlight state transitions on menu items.
|
|
17
|
+
static let highlight: Animation = .easeOut(duration: 0.12)
|
|
18
|
+
|
|
19
|
+
// MARK: - Transition Durations
|
|
20
|
+
|
|
21
|
+
/// Duration for sheen / gloss sweeps on progress bars.
|
|
22
|
+
static let sheenDuration: Double = 0.9
|
|
23
|
+
|
|
24
|
+
/// Delay before sheen resets after sweeping.
|
|
25
|
+
static let sheenResetDelay: Duration = .seconds(1.1)
|
|
26
|
+
|
|
27
|
+
/// Duration for copy-confirmation checkmark display.
|
|
28
|
+
static let copyFeedbackDuration: Duration = .seconds(0.9)
|
|
29
|
+
|
|
30
|
+
/// Fade-out duration for copy confirmation revert.
|
|
31
|
+
static let copyFeedbackFadeOut: Animation = .easeOut(duration: 0.2)
|
|
32
|
+
|
|
33
|
+
// MARK: - Skeleton / Loading
|
|
34
|
+
|
|
35
|
+
/// Continuous shimmer animation for loading skeletons.
|
|
36
|
+
static let shimmer: Animation = .easeInOut(duration: 1.5).repeatForever(autoreverses: false)
|
|
37
|
+
|
|
38
|
+
/// Subtle hover feedback on interactive elements.
|
|
39
|
+
static let hoverFeedback: Animation = .easeInOut(duration: 0.15)
|
|
40
|
+
|
|
41
|
+
// MARK: - Chart Interaction
|
|
42
|
+
|
|
43
|
+
/// Fade animation for chart interaction hints.
|
|
44
|
+
static let chartHintFade: Animation = .easeOut(duration: 0.4)
|
|
45
|
+
|
|
46
|
+
// MARK: - Menu Card Entrance
|
|
47
|
+
|
|
48
|
+
/// Staggered spring entrance for menu card sections.
|
|
49
|
+
static let cardEntrance: Animation = .spring(response: 0.45, dampingFraction: 0.82)
|
|
50
|
+
|
|
51
|
+
/// Base delay between staggered card section entrances.
|
|
52
|
+
static let cardEntranceStagger: Double = 0.06
|
|
53
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Semantic color tokens for consistent theming across the Runic UI.
|
|
4
|
+
enum RunicColors {
|
|
5
|
+
// MARK: - Status Colors
|
|
6
|
+
|
|
7
|
+
static let success = Color(nsColor: .systemGreen)
|
|
8
|
+
static let warning = Color(nsColor: .systemOrange)
|
|
9
|
+
static let error = Color(nsColor: .systemRed)
|
|
10
|
+
static let info = Color(nsColor: .systemBlue)
|
|
11
|
+
|
|
12
|
+
// MARK: - Chart Palette (6 accessible colors)
|
|
13
|
+
|
|
14
|
+
/// Ordered palette for chart series. Colors chosen for distinguishability in
|
|
15
|
+
/// both light and dark mode and for common color-vision deficiencies.
|
|
16
|
+
static let chartPalette: [Color] = [
|
|
17
|
+
Color(red: 0.26, green: 0.55, blue: 0.96), // blue
|
|
18
|
+
Color(red: 0.94, green: 0.53, blue: 0.18), // orange
|
|
19
|
+
Color(red: 0.46, green: 0.75, blue: 0.36), // green
|
|
20
|
+
Color(red: 0.80, green: 0.45, blue: 0.92), // purple
|
|
21
|
+
Color(red: 0.26, green: 0.78, blue: 0.86), // teal
|
|
22
|
+
Color(red: 0.94, green: 0.74, blue: 0.26), // yellow
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/// Returns a chart color for the given index, wrapping around if needed.
|
|
26
|
+
static func chartColor(at index: Int) -> Color {
|
|
27
|
+
self.chartPalette[index % self.chartPalette.count]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - Model Family Colors
|
|
31
|
+
|
|
32
|
+
/// Color for Claude/Anthropic model family.
|
|
33
|
+
static let modelFamilyClaude = Color(red: 0.80, green: 0.45, blue: 0.30)
|
|
34
|
+
/// Color for GPT/OpenAI model family.
|
|
35
|
+
static let modelFamilyGPT = Color(red: 0.30, green: 0.70, blue: 0.40)
|
|
36
|
+
/// Color for Gemini/Google model family.
|
|
37
|
+
static let modelFamilyGemini = Color(red: 0.26, green: 0.55, blue: 0.96)
|
|
38
|
+
/// Default color for unknown model families.
|
|
39
|
+
static let modelFamilyDefault = Color(red: 0.55, green: 0.55, blue: 0.60)
|
|
40
|
+
|
|
41
|
+
/// Returns a color appropriate for the given model name based on its family.
|
|
42
|
+
static func colorForModel(_ model: String) -> Color {
|
|
43
|
+
let lower = model.lowercased()
|
|
44
|
+
if lower.contains("claude") || lower.contains("sonnet") || lower.contains("opus") || lower.contains("haiku") {
|
|
45
|
+
return self.modelFamilyClaude
|
|
46
|
+
}
|
|
47
|
+
if lower.contains("gpt") || lower.contains("o1") || lower.contains("o3") || lower.contains("o4") {
|
|
48
|
+
return self.modelFamilyGPT
|
|
49
|
+
}
|
|
50
|
+
if lower.contains("gemini") {
|
|
51
|
+
return self.modelFamilyGemini
|
|
52
|
+
}
|
|
53
|
+
return self.modelFamilyDefault
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Named Chart Colors
|
|
57
|
+
|
|
58
|
+
/// Credits history chart bar color
|
|
59
|
+
static let creditsChartBar = Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)
|
|
60
|
+
|
|
61
|
+
// MARK: - Opacity Tokens
|
|
62
|
+
|
|
63
|
+
enum Opacity {
|
|
64
|
+
static let nano: Double = 0.045
|
|
65
|
+
static let subtle: Double = 0.08
|
|
66
|
+
static let light: Double = 0.12
|
|
67
|
+
static let medium: Double = 0.22
|
|
68
|
+
static let strong: Double = 0.35
|
|
69
|
+
static let emphasis: Double = 0.55
|
|
70
|
+
static let prominent: Double = 0.70
|
|
71
|
+
static let vivid: Double = 0.90
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Corner radius tokens replacing scattered literal values throughout the UI.
|
|
4
|
+
enum RunicCornerRadius {
|
|
5
|
+
/// 4pt — badges, small tags, inline controls
|
|
6
|
+
static let xs: CGFloat = 4
|
|
7
|
+
/// 6pt — list rows, compact cards, buttons
|
|
8
|
+
static let sm: CGFloat = 6
|
|
9
|
+
/// 8pt — standard cards, menu sections
|
|
10
|
+
static let md: CGFloat = 8
|
|
11
|
+
/// 12pt — prominent panels, avatar frames, provider cards
|
|
12
|
+
static let lg: CGFloat = 12
|
|
13
|
+
/// 16pt — large icons, about pane, hero elements
|
|
14
|
+
static let xl: CGFloat = 16
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// **Runic Spacing System**
|
|
4
|
+
/// Defines global spacing constants following a 4pt grid system.
|
|
5
|
+
/// All spacing values are multiples of 4 for consistent visual rhythm.
|
|
6
|
+
enum RunicSpacing {
|
|
7
|
+
// MARK: - Base Spacing Scale
|
|
8
|
+
|
|
9
|
+
/// Extra extra extra small spacing (2pt)
|
|
10
|
+
static let xxxs: CGFloat = 2
|
|
11
|
+
|
|
12
|
+
/// Extra extra small spacing (4pt)
|
|
13
|
+
static let xxs: CGFloat = 4
|
|
14
|
+
|
|
15
|
+
/// Extra small spacing (8pt)
|
|
16
|
+
static let xs: CGFloat = 8
|
|
17
|
+
|
|
18
|
+
/// Small spacing (12pt)
|
|
19
|
+
static let sm: CGFloat = 12
|
|
20
|
+
|
|
21
|
+
/// Medium spacing (16pt)
|
|
22
|
+
static let md: CGFloat = 16
|
|
23
|
+
|
|
24
|
+
/// Large spacing (24pt)
|
|
25
|
+
static let lg: CGFloat = 24
|
|
26
|
+
|
|
27
|
+
/// Extra large spacing (32pt)
|
|
28
|
+
static let xl: CGFloat = 32
|
|
29
|
+
|
|
30
|
+
/// Extra extra large spacing (48pt)
|
|
31
|
+
static let xxl: CGFloat = 48
|
|
32
|
+
|
|
33
|
+
// MARK: - Semantic Spacing
|
|
34
|
+
|
|
35
|
+
/// Horizontal padding for menu content
|
|
36
|
+
static let menuHorizontalPadding: CGFloat = sm // 12pt
|
|
37
|
+
|
|
38
|
+
/// Vertical padding for menu content
|
|
39
|
+
static let menuVerticalPadding: CGFloat = xs // 8pt
|
|
40
|
+
|
|
41
|
+
/// Spacing between cards in menu
|
|
42
|
+
static let cardSpacing: CGFloat = xs // 8pt
|
|
43
|
+
|
|
44
|
+
/// Spacing between major sections
|
|
45
|
+
static let sectionSpacing: CGFloat = lg // 24pt
|
|
46
|
+
|
|
47
|
+
// MARK: - Preferences Spacing
|
|
48
|
+
|
|
49
|
+
/// Compact spacing (6pt) - Used between sm and xs, common in preference toggles
|
|
50
|
+
static let compact: CGFloat = 6
|
|
51
|
+
|
|
52
|
+
/// Standardized spacing for toggle rows
|
|
53
|
+
static let toggleRowSpacing: CGFloat = xs // 8pt
|
|
54
|
+
|
|
55
|
+
/// Spacing after section headers
|
|
56
|
+
static let sectionHeaderSpacing: CGFloat = md // 16pt
|
|
57
|
+
|
|
58
|
+
/// Spacing within stepper controls
|
|
59
|
+
static let stepperControlSpacing: CGFloat = xs // 8pt
|
|
60
|
+
|
|
61
|
+
// MARK: - Chart Metrics
|
|
62
|
+
|
|
63
|
+
/// Standard height for chart views in menus
|
|
64
|
+
static let chartHeight: CGFloat = 130
|
|
65
|
+
|
|
66
|
+
/// Diameter for legend dot indicators
|
|
67
|
+
static let chartLegendDot: CGFloat = 7
|
|
68
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import RunicCore
|
|
2
|
+
import Foundation
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
protocol AuggieTokenStoring: Sendable {
|
|
6
|
+
func loadToken() throws -> String?
|
|
7
|
+
func storeToken(_ token: String?) throws
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum AuggieTokenStoreError: LocalizedError {
|
|
11
|
+
case keychainStatus(OSStatus)
|
|
12
|
+
case invalidData
|
|
13
|
+
|
|
14
|
+
var errorDescription: String? {
|
|
15
|
+
switch self {
|
|
16
|
+
case let .keychainStatus(status):
|
|
17
|
+
"Keychain error: \(status)"
|
|
18
|
+
case .invalidData:
|
|
19
|
+
"Keychain returned invalid data."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct KeychainAuggieTokenStore: AuggieTokenStoring {
|
|
25
|
+
private static let log = RunicLog.logger("auggie-token-store")
|
|
26
|
+
|
|
27
|
+
private let service = "com.sriinnu.athena.Runic"
|
|
28
|
+
private let account = "auggie-api-token"
|
|
29
|
+
|
|
30
|
+
func loadToken() throws -> String? {
|
|
31
|
+
// Try standard keychain first.
|
|
32
|
+
if let token = try self.readToken(dataProtection: false) {
|
|
33
|
+
return token
|
|
34
|
+
}
|
|
35
|
+
// Migrate from the old Data Protection keychain if present.
|
|
36
|
+
if let token = try self.readToken(dataProtection: true) {
|
|
37
|
+
Self.log.info("Migrating Auggie token from Data Protection keychain")
|
|
38
|
+
// Store first, delete old only on success — prevents data loss.
|
|
39
|
+
if (try? self.storeToken(token)) != nil {
|
|
40
|
+
try? self.deleteToken(dataProtection: true)
|
|
41
|
+
}
|
|
42
|
+
return token
|
|
43
|
+
}
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func storeToken(_ token: String?) throws {
|
|
48
|
+
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
49
|
+
if cleaned == nil || cleaned?.isEmpty == true {
|
|
50
|
+
try? self.deleteToken(dataProtection: false)
|
|
51
|
+
try? self.deleteToken(dataProtection: true)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let data = cleaned!.data(using: .utf8)!
|
|
56
|
+
let query = self.baseQuery(dataProtection: false)
|
|
57
|
+
let attributes: [String: Any] = [
|
|
58
|
+
kSecValueData as String: data,
|
|
59
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
// Try update first.
|
|
63
|
+
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
64
|
+
if updateStatus == errSecSuccess { return }
|
|
65
|
+
|
|
66
|
+
if updateStatus != errSecItemNotFound {
|
|
67
|
+
// Item exists but update failed — delete and re-add to reset ACL.
|
|
68
|
+
try? self.deleteToken(dataProtection: false)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var addQuery = query
|
|
72
|
+
for (key, value) in attributes { addQuery[key] = value }
|
|
73
|
+
|
|
74
|
+
// Grant the calling app permanent access so macOS never shows a
|
|
75
|
+
// Keychain password dialog for this item.
|
|
76
|
+
if let access = Self.createSelfAccess() {
|
|
77
|
+
addQuery[kSecAttrAccess as String] = access
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
81
|
+
guard addStatus == errSecSuccess else {
|
|
82
|
+
Self.log.error("Keychain add failed: \(addStatus)")
|
|
83
|
+
throw AuggieTokenStoreError.keychainStatus(addStatus)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MARK: - Private
|
|
88
|
+
|
|
89
|
+
private func readToken(dataProtection: Bool) throws -> String? {
|
|
90
|
+
var result: CFTypeRef?
|
|
91
|
+
var query = self.baseQuery(dataProtection: dataProtection)
|
|
92
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
93
|
+
query[kSecReturnData as String] = true
|
|
94
|
+
|
|
95
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
96
|
+
if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
guard status == errSecSuccess else {
|
|
100
|
+
Self.log.error("Keychain read failed: \(status)")
|
|
101
|
+
throw AuggieTokenStoreError.keychainStatus(status)
|
|
102
|
+
}
|
|
103
|
+
guard let data = result as? Data else {
|
|
104
|
+
throw AuggieTokenStoreError.invalidData
|
|
105
|
+
}
|
|
106
|
+
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
107
|
+
return (token?.isEmpty ?? true) ? nil : token
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func deleteToken(dataProtection: Bool) throws {
|
|
111
|
+
let query = self.baseQuery(dataProtection: dataProtection)
|
|
112
|
+
let status = SecItemDelete(query as CFDictionary)
|
|
113
|
+
if status == errSecSuccess || status == errSecItemNotFound { return }
|
|
114
|
+
Self.log.error("Keychain delete failed: \(status)")
|
|
115
|
+
throw AuggieTokenStoreError.keychainStatus(status)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func baseQuery(dataProtection: Bool) -> [String: Any] {
|
|
119
|
+
var q: [String: Any] = [
|
|
120
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
121
|
+
kSecAttrService as String: self.service,
|
|
122
|
+
kSecAttrAccount as String: self.account,
|
|
123
|
+
]
|
|
124
|
+
if dataProtection {
|
|
125
|
+
q[kSecUseDataProtectionKeychain as String] = true
|
|
126
|
+
}
|
|
127
|
+
return q
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Creates a SecAccess that grants the calling application permanent
|
|
131
|
+
/// read/write access without triggering the macOS Keychain password dialog.
|
|
132
|
+
private static func createSelfAccess() -> SecAccess? {
|
|
133
|
+
var trustedSelf: SecTrustedApplication?
|
|
134
|
+
guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
|
|
135
|
+
let trustedSelf else { return nil }
|
|
136
|
+
var access: SecAccess?
|
|
137
|
+
guard SecAccessCreate(
|
|
138
|
+
"Runic API Token" as CFString,
|
|
139
|
+
[trustedSelf] as CFArray,
|
|
140
|
+
&access) == errSecSuccess else { return nil }
|
|
141
|
+
return access
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import RunicCore
|
|
2
|
+
import Foundation
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
protocol AzureOpenAITokenStoring: Sendable {
|
|
6
|
+
func loadToken() throws -> String?
|
|
7
|
+
func storeToken(_ token: String?) throws
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum AzureOpenAITokenStoreError: LocalizedError {
|
|
11
|
+
case keychainStatus(OSStatus)
|
|
12
|
+
case invalidData
|
|
13
|
+
|
|
14
|
+
var errorDescription: String? {
|
|
15
|
+
switch self {
|
|
16
|
+
case let .keychainStatus(status):
|
|
17
|
+
"Keychain error: \(status)"
|
|
18
|
+
case .invalidData:
|
|
19
|
+
"Keychain returned invalid data."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct KeychainAzureOpenAITokenStore: AzureOpenAITokenStoring {
|
|
25
|
+
private static let log = RunicLog.logger("azure-openai-token-store")
|
|
26
|
+
|
|
27
|
+
private let service = "com.sriinnu.athena.Runic"
|
|
28
|
+
private let account = "azure-openai-api-token"
|
|
29
|
+
|
|
30
|
+
func loadToken() throws -> String? {
|
|
31
|
+
// Try standard keychain first.
|
|
32
|
+
if let token = try self.readToken(dataProtection: false) {
|
|
33
|
+
return token
|
|
34
|
+
}
|
|
35
|
+
// Migrate from the old Data Protection keychain if present.
|
|
36
|
+
if let token = try self.readToken(dataProtection: true) {
|
|
37
|
+
Self.log.info("Migrating Azure OpenAI token from Data Protection keychain")
|
|
38
|
+
// Store first, delete old only on success — prevents data loss.
|
|
39
|
+
if (try? self.storeToken(token)) != nil {
|
|
40
|
+
try? self.deleteToken(dataProtection: true)
|
|
41
|
+
}
|
|
42
|
+
return token
|
|
43
|
+
}
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func storeToken(_ token: String?) throws {
|
|
48
|
+
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
49
|
+
if cleaned == nil || cleaned?.isEmpty == true {
|
|
50
|
+
try? self.deleteToken(dataProtection: false)
|
|
51
|
+
try? self.deleteToken(dataProtection: true)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let data = cleaned!.data(using: .utf8)!
|
|
56
|
+
let query = self.baseQuery(dataProtection: false)
|
|
57
|
+
let attributes: [String: Any] = [
|
|
58
|
+
kSecValueData as String: data,
|
|
59
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
// Try update first.
|
|
63
|
+
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
64
|
+
if updateStatus == errSecSuccess { return }
|
|
65
|
+
|
|
66
|
+
if updateStatus != errSecItemNotFound {
|
|
67
|
+
// Item exists but update failed — delete and re-add to reset ACL.
|
|
68
|
+
try? self.deleteToken(dataProtection: false)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var addQuery = query
|
|
72
|
+
for (key, value) in attributes { addQuery[key] = value }
|
|
73
|
+
|
|
74
|
+
// Grant the calling app permanent access so macOS never shows a
|
|
75
|
+
// Keychain password dialog for this item.
|
|
76
|
+
if let access = Self.createSelfAccess() {
|
|
77
|
+
addQuery[kSecAttrAccess as String] = access
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
81
|
+
guard addStatus == errSecSuccess else {
|
|
82
|
+
Self.log.error("Keychain add failed: \(addStatus)")
|
|
83
|
+
throw AzureOpenAITokenStoreError.keychainStatus(addStatus)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MARK: - Private
|
|
88
|
+
|
|
89
|
+
private func readToken(dataProtection: Bool) throws -> String? {
|
|
90
|
+
var result: CFTypeRef?
|
|
91
|
+
var query = self.baseQuery(dataProtection: dataProtection)
|
|
92
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
93
|
+
query[kSecReturnData as String] = true
|
|
94
|
+
|
|
95
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
96
|
+
if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
guard status == errSecSuccess else {
|
|
100
|
+
Self.log.error("Keychain read failed: \(status)")
|
|
101
|
+
throw AzureOpenAITokenStoreError.keychainStatus(status)
|
|
102
|
+
}
|
|
103
|
+
guard let data = result as? Data else {
|
|
104
|
+
throw AzureOpenAITokenStoreError.invalidData
|
|
105
|
+
}
|
|
106
|
+
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
107
|
+
return (token?.isEmpty ?? true) ? nil : token
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func deleteToken(dataProtection: Bool) throws {
|
|
111
|
+
let query = self.baseQuery(dataProtection: dataProtection)
|
|
112
|
+
let status = SecItemDelete(query as CFDictionary)
|
|
113
|
+
if status == errSecSuccess || status == errSecItemNotFound { return }
|
|
114
|
+
Self.log.error("Keychain delete failed: \(status)")
|
|
115
|
+
throw AzureOpenAITokenStoreError.keychainStatus(status)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func baseQuery(dataProtection: Bool) -> [String: Any] {
|
|
119
|
+
var q: [String: Any] = [
|
|
120
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
121
|
+
kSecAttrService as String: self.service,
|
|
122
|
+
kSecAttrAccount as String: self.account,
|
|
123
|
+
]
|
|
124
|
+
if dataProtection {
|
|
125
|
+
q[kSecUseDataProtectionKeychain as String] = true
|
|
126
|
+
}
|
|
127
|
+
return q
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Creates a SecAccess that grants the calling application permanent
|
|
131
|
+
/// read/write access without triggering the macOS Keychain password dialog.
|
|
132
|
+
private static func createSelfAccess() -> SecAccess? {
|
|
133
|
+
var trustedSelf: SecTrustedApplication?
|
|
134
|
+
guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
|
|
135
|
+
let trustedSelf else { return nil }
|
|
136
|
+
var access: SecAccess?
|
|
137
|
+
guard SecAccessCreate(
|
|
138
|
+
"Runic API Token" as CFString,
|
|
139
|
+
[trustedSelf] as CFArray,
|
|
140
|
+
&access) == errSecSuccess else { return nil }
|
|
141
|
+
return access
|
|
142
|
+
}
|
|
143
|
+
}
|