@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,143 @@
|
|
|
1
|
+
import RunicCore
|
|
2
|
+
import Foundation
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
protocol XAITokenStoring: Sendable {
|
|
6
|
+
func loadToken() throws -> String?
|
|
7
|
+
func storeToken(_ token: String?) throws
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum XAITokenStoreError: 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 KeychainXAITokenStore: XAITokenStoring {
|
|
25
|
+
private static let log = RunicLog.logger("xai-token-store")
|
|
26
|
+
|
|
27
|
+
private let service = "com.sriinnu.athena.Runic"
|
|
28
|
+
private let account = "xai-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 xAI 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 XAITokenStoreError.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 XAITokenStoreError.keychainStatus(status)
|
|
102
|
+
}
|
|
103
|
+
guard let data = result as? Data else {
|
|
104
|
+
throw XAITokenStoreError.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 XAITokenStoreError.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 ZaiTokenStoring: Sendable {
|
|
6
|
+
func loadToken() throws -> String?
|
|
7
|
+
func storeToken(_ token: String?) throws
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum ZaiTokenStoreError: 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 KeychainZaiTokenStore: ZaiTokenStoring {
|
|
25
|
+
private static let log = RunicLog.logger("zai-token-store")
|
|
26
|
+
|
|
27
|
+
private let service = "com.sriinnu.athena.Runic"
|
|
28
|
+
private let account = "zai-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 z.ai 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 ZaiTokenStoreError.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 ZaiTokenStoreError.keychainStatus(status)
|
|
102
|
+
}
|
|
103
|
+
guard let data = result as? Data else {
|
|
104
|
+
throw ZaiTokenStoreError.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 ZaiTokenStoreError.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,193 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import RunicCore
|
|
3
|
+
|
|
4
|
+
/// Exports usage data from the ledger as CSV or JSON.
|
|
5
|
+
@MainActor
|
|
6
|
+
enum UsageExporter {
|
|
7
|
+
enum Format: String {
|
|
8
|
+
case csv
|
|
9
|
+
case json
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Build an export string for the given provider in the requested format.
|
|
13
|
+
static func export(store: UsageStore, provider: UsageProvider, format: Format) -> String {
|
|
14
|
+
switch format {
|
|
15
|
+
case .csv:
|
|
16
|
+
self.exportCSV(store: store, provider: provider)
|
|
17
|
+
case .json:
|
|
18
|
+
self.exportJSON(store: store, provider: provider)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - CSV
|
|
23
|
+
|
|
24
|
+
private static func exportCSV(store: UsageStore, provider: UsageProvider) -> String {
|
|
25
|
+
var lines: [String] = []
|
|
26
|
+
|
|
27
|
+
// Header
|
|
28
|
+
lines.append("date,input_tokens,output_tokens,cache_creation_tokens,cache_read_tokens,total_tokens,cost_usd,models_used")
|
|
29
|
+
|
|
30
|
+
// Daily summaries from token snapshot
|
|
31
|
+
let daily = store.tokenSnapshot(for: provider)?.daily ?? []
|
|
32
|
+
|
|
33
|
+
for day in daily {
|
|
34
|
+
let costStr = day.costUSD.map { String(format: "%.4f", $0) } ?? ""
|
|
35
|
+
let modelsStr = Self.csvEscape((day.modelsUsed ?? []).joined(separator: "; "))
|
|
36
|
+
let input = day.inputTokens ?? 0
|
|
37
|
+
let output = day.outputTokens ?? 0
|
|
38
|
+
let cacheCreate = day.cacheCreationTokens ?? 0
|
|
39
|
+
let cacheRead = day.cacheReadTokens ?? 0
|
|
40
|
+
let total = day.totalTokens ?? (input + output + cacheCreate + cacheRead)
|
|
41
|
+
lines.append("\(day.date),\(input),\(output),\(cacheCreate),\(cacheRead),\(total),\(costStr),\(modelsStr)")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If no token snapshot daily data, fall back to ledger daily summary
|
|
45
|
+
if daily.isEmpty, let summary = store.ledgerDailySummary(for: provider) {
|
|
46
|
+
let costStr = summary.totals.costUSD.map { String(format: "%.4f", $0) } ?? ""
|
|
47
|
+
let modelsStr = Self.csvEscape(summary.modelsUsed.joined(separator: "; "))
|
|
48
|
+
lines.append("\(summary.dayKey),\(summary.totals.inputTokens),\(summary.totals.outputTokens),\(summary.totals.cacheCreationTokens),\(summary.totals.cacheReadTokens),\(summary.totals.totalTokens),\(costStr),\(modelsStr)")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return lines.joined(separator: "\n")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - JSON
|
|
55
|
+
|
|
56
|
+
private static func exportJSON(store: UsageStore, provider: UsageProvider) -> String {
|
|
57
|
+
let metadata = store.metadata(for: provider)
|
|
58
|
+
let dateFormatter = DateFormatter()
|
|
59
|
+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
|
60
|
+
dateFormatter.timeZone = TimeZone.current
|
|
61
|
+
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
|
62
|
+
|
|
63
|
+
let nowStr = dateFormatter.string(from: Date())
|
|
64
|
+
|
|
65
|
+
var root: [String: Any] = [
|
|
66
|
+
"provider": provider.rawValue,
|
|
67
|
+
"providerDisplayName": metadata.displayName,
|
|
68
|
+
"exportedAt": nowStr,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
// Daily summaries from token snapshot
|
|
72
|
+
var dailyArray: [[String: Any]] = []
|
|
73
|
+
let daily = store.tokenSnapshot(for: provider)?.daily ?? []
|
|
74
|
+
|
|
75
|
+
for day in daily {
|
|
76
|
+
let input = day.inputTokens ?? 0
|
|
77
|
+
let output = day.outputTokens ?? 0
|
|
78
|
+
let cacheCreate = day.cacheCreationTokens ?? 0
|
|
79
|
+
let cacheRead = day.cacheReadTokens ?? 0
|
|
80
|
+
let total = day.totalTokens ?? (input + output + cacheCreate + cacheRead)
|
|
81
|
+
var entry: [String: Any] = [
|
|
82
|
+
"date": day.date,
|
|
83
|
+
"inputTokens": input,
|
|
84
|
+
"outputTokens": output,
|
|
85
|
+
"cacheCreationTokens": cacheCreate,
|
|
86
|
+
"cacheReadTokens": cacheRead,
|
|
87
|
+
"totalTokens": total,
|
|
88
|
+
"models": day.modelsUsed ?? [],
|
|
89
|
+
]
|
|
90
|
+
if let cost = day.costUSD {
|
|
91
|
+
entry["costUSD"] = cost
|
|
92
|
+
}
|
|
93
|
+
dailyArray.append(entry)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback to ledger summary if no token snapshot data
|
|
97
|
+
if dailyArray.isEmpty, let summary = store.ledgerDailySummary(for: provider) {
|
|
98
|
+
var entry: [String: Any] = [
|
|
99
|
+
"date": summary.dayKey,
|
|
100
|
+
"inputTokens": summary.totals.inputTokens,
|
|
101
|
+
"outputTokens": summary.totals.outputTokens,
|
|
102
|
+
"cacheCreationTokens": summary.totals.cacheCreationTokens,
|
|
103
|
+
"cacheReadTokens": summary.totals.cacheReadTokens,
|
|
104
|
+
"totalTokens": summary.totals.totalTokens,
|
|
105
|
+
"models": summary.modelsUsed,
|
|
106
|
+
]
|
|
107
|
+
if let cost = summary.totals.costUSD {
|
|
108
|
+
entry["costUSD"] = cost
|
|
109
|
+
}
|
|
110
|
+
dailyArray.append(entry)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
root["dailySummaries"] = dailyArray
|
|
114
|
+
|
|
115
|
+
// Model breakdowns
|
|
116
|
+
let modelBreakdowns = store.ledgerModelBreakdown(for: provider)
|
|
117
|
+
if !modelBreakdowns.isEmpty {
|
|
118
|
+
root["modelBreakdowns"] = modelBreakdowns.map { model -> [String: Any] in
|
|
119
|
+
var entry: [String: Any] = [
|
|
120
|
+
"model": model.model,
|
|
121
|
+
"requestCount": model.entryCount,
|
|
122
|
+
"inputTokens": model.totals.inputTokens,
|
|
123
|
+
"outputTokens": model.totals.outputTokens,
|
|
124
|
+
"totalTokens": model.totals.totalTokens,
|
|
125
|
+
]
|
|
126
|
+
if let cost = model.totals.costUSD {
|
|
127
|
+
entry["costUSD"] = cost
|
|
128
|
+
}
|
|
129
|
+
if let projectName = model.projectName {
|
|
130
|
+
entry["projectName"] = projectName
|
|
131
|
+
}
|
|
132
|
+
if let projectID = model.projectID {
|
|
133
|
+
entry["projectID"] = projectID
|
|
134
|
+
}
|
|
135
|
+
return entry
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Project breakdowns
|
|
140
|
+
let projectBreakdowns = store.ledgerProjectBreakdown(for: provider)
|
|
141
|
+
if !projectBreakdowns.isEmpty {
|
|
142
|
+
root["projectBreakdowns"] = projectBreakdowns.map { project -> [String: Any] in
|
|
143
|
+
var entry: [String: Any] = [
|
|
144
|
+
"projectName": project.displayProjectName,
|
|
145
|
+
"requestCount": project.entryCount,
|
|
146
|
+
"inputTokens": project.totals.inputTokens,
|
|
147
|
+
"outputTokens": project.totals.outputTokens,
|
|
148
|
+
"totalTokens": project.totals.totalTokens,
|
|
149
|
+
"models": project.modelsUsed,
|
|
150
|
+
]
|
|
151
|
+
if let cost = project.totals.costUSD {
|
|
152
|
+
entry["costUSD"] = cost
|
|
153
|
+
}
|
|
154
|
+
if let projectID = project.projectID {
|
|
155
|
+
entry["projectID"] = projectID
|
|
156
|
+
}
|
|
157
|
+
return entry
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Spend forecast
|
|
162
|
+
if let forecast = store.ledgerSpendForecast(for: provider) {
|
|
163
|
+
root["spendForecast"] = [
|
|
164
|
+
"observedDays": forecast.observedDays,
|
|
165
|
+
"observedCostUSD": forecast.observedCostUSD,
|
|
166
|
+
"averageDailyCostUSD": forecast.averageDailyCostUSD,
|
|
167
|
+
"projected30DayCostUSD": forecast.projected30DayCostUSD,
|
|
168
|
+
"budgetWillBreach": forecast.budgetWillBreach,
|
|
169
|
+
] as [String: Any]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
guard let data = try? JSONSerialization.data(
|
|
173
|
+
withJSONObject: root,
|
|
174
|
+
options: [.prettyPrinted, .sortedKeys]),
|
|
175
|
+
let string = String(data: data, encoding: .utf8) else
|
|
176
|
+
{
|
|
177
|
+
return "{}"
|
|
178
|
+
}
|
|
179
|
+
return string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// MARK: - Helpers
|
|
183
|
+
|
|
184
|
+
/// Escape a value for CSV, quoting if it contains commas, quotes, or newlines.
|
|
185
|
+
private static func csvEscape(_ value: String) -> String {
|
|
186
|
+
let needsQuoting = value.contains(",") || value.contains("\"") || value.contains("\n")
|
|
187
|
+
if needsQuoting {
|
|
188
|
+
let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
|
|
189
|
+
return "\"\(escaped)\""
|
|
190
|
+
}
|
|
191
|
+
return value
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum RelativeTimeFormatters {
|
|
4
|
+
@MainActor
|
|
5
|
+
static let full: RelativeDateTimeFormatter = {
|
|
6
|
+
let formatter = RelativeDateTimeFormatter()
|
|
7
|
+
formatter.unitsStyle = .full
|
|
8
|
+
return formatter
|
|
9
|
+
}()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
extension Date {
|
|
13
|
+
@MainActor
|
|
14
|
+
func relativeDescription(now: Date = .now) -> String {
|
|
15
|
+
let seconds = abs(now.timeIntervalSince(self))
|
|
16
|
+
if seconds < 15 {
|
|
17
|
+
return "just now"
|
|
18
|
+
}
|
|
19
|
+
return RelativeTimeFormatters.full.localizedString(for: self, relativeTo: now)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct HiddenWindowView: View {
|
|
4
|
+
@Environment(\.openSettings) private var openSettings
|
|
5
|
+
|
|
6
|
+
var body: some View {
|
|
7
|
+
Color.clear
|
|
8
|
+
.frame(width: 20, height: 20)
|
|
9
|
+
.onReceive(NotificationCenter.default.publisher(for: .runicOpenSettings)) { _ in
|
|
10
|
+
Task { @MainActor in
|
|
11
|
+
self.openSettings()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
.onAppear {
|
|
15
|
+
if let window = NSApp.windows.first(where: { $0.title == "RunicLifecycleKeepalive" }) {
|
|
16
|
+
// Make the keepalive window truly invisible and non-interactive.
|
|
17
|
+
window.styleMask = [.borderless]
|
|
18
|
+
window.collectionBehavior = [.auxiliary, .ignoresCycle, .transient, .canJoinAllSpaces]
|
|
19
|
+
window.isExcludedFromWindowsMenu = true
|
|
20
|
+
window.level = .floating
|
|
21
|
+
window.isOpaque = false
|
|
22
|
+
window.alphaValue = 0
|
|
23
|
+
window.backgroundColor = .clear
|
|
24
|
+
window.hasShadow = false
|
|
25
|
+
window.ignoresMouseEvents = true
|
|
26
|
+
window.canHide = false
|
|
27
|
+
window.setContentSize(NSSize(width: 1, height: 1))
|
|
28
|
+
window.setFrameOrigin(NSPoint(x: -5000, y: -5000))
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum InstallOrigin {
|
|
4
|
+
static func isHomebrewCask(appBundleURL: URL) -> Bool {
|
|
5
|
+
let resolved = appBundleURL.resolvingSymlinksInPath()
|
|
6
|
+
let path = resolved.path
|
|
7
|
+
return path.contains("/Caskroom/") || path.contains("/Homebrew/Caskroom/")
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Lightweight NSView-based mouse tracking with local coordinates.
|
|
5
|
+
///
|
|
6
|
+
/// Why: SwiftUI's `onHover` doesn't provide location, but we want "hover a bar to see values" on macOS.
|
|
7
|
+
@MainActor
|
|
8
|
+
struct MouseLocationReader: NSViewRepresentable {
|
|
9
|
+
let onMoved: (CGPoint?) -> Void
|
|
10
|
+
|
|
11
|
+
func makeNSView(context: Context) -> TrackingView {
|
|
12
|
+
let view = TrackingView()
|
|
13
|
+
view.onMoved = self.onMoved
|
|
14
|
+
return view
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func updateNSView(_ nsView: TrackingView, context: Context) {
|
|
18
|
+
nsView.onMoved = self.onMoved
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
final class TrackingView: NSView {
|
|
22
|
+
var onMoved: ((CGPoint?) -> Void)?
|
|
23
|
+
private var trackingArea: NSTrackingArea?
|
|
24
|
+
|
|
25
|
+
override func viewDidMoveToWindow() {
|
|
26
|
+
super.viewDidMoveToWindow()
|
|
27
|
+
self.window?.acceptsMouseMovedEvents = true
|
|
28
|
+
self.updateTrackingAreas()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override func updateTrackingAreas() {
|
|
32
|
+
super.updateTrackingAreas()
|
|
33
|
+
if let trackingArea {
|
|
34
|
+
self.removeTrackingArea(trackingArea)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let options: NSTrackingArea.Options = [
|
|
38
|
+
// NSMenu popups aren't "key windows", so `.activeInKeyWindow` would drop events and cause hover
|
|
39
|
+
// state to flicker. `.activeAlways` keeps tracking stable while the menu is open.
|
|
40
|
+
.activeAlways,
|
|
41
|
+
.inVisibleRect,
|
|
42
|
+
.mouseEnteredAndExited,
|
|
43
|
+
.mouseMoved,
|
|
44
|
+
]
|
|
45
|
+
let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
|
|
46
|
+
self.addTrackingArea(area)
|
|
47
|
+
self.trackingArea = area
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override func mouseEntered(with event: NSEvent) {
|
|
51
|
+
super.mouseEntered(with: event)
|
|
52
|
+
self.onMoved?(self.convert(event.locationInWindow, from: nil))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override func mouseMoved(with event: NSEvent) {
|
|
56
|
+
super.mouseMoved(with: event)
|
|
57
|
+
self.onMoved?(self.convert(event.locationInWindow, from: nil))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override func mouseExited(with event: NSEvent) {
|
|
61
|
+
super.mouseExited(with: event)
|
|
62
|
+
self.onMoved?(nil)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|