@sriinnu/runic 1.0.0 → 2.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/CHANGELOG.md +11 -0
- package/LICENSE +1 -1
- package/README.md +47 -29
- package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
- package/RuniciOS/Views/ProviderDetailView.swift +9 -9
- package/RuniciOS/Views/ProviderListView.swift +10 -10
- package/RuniciOS/Widgets/LargeWidget.swift +14 -14
- package/RuniciOS/Widgets/LiveActivity.swift +19 -19
- package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
- package/RuniciOS/Widgets/MediumWidget.swift +10 -10
- package/RuniciOS/Widgets/RunicWidget.swift +5 -5
- package/RuniciOS/Widgets/SmallWidget.swift +9 -9
- package/Scripts/package_app.sh +2 -2
- package/Sources/Runic/About.swift +3 -3
- package/Sources/Runic/AppNotifications.swift +5 -3
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +17 -4
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +47 -14
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
- package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
- package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +3 -6
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
- package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
- package/Sources/Runic/Core/RunicColors.swift +6 -6
- package/Sources/Runic/Core/RunicSpacing.swift +7 -7
- package/Sources/Runic/Core/RunicTypography.swift +206 -0
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/SettingsStore.swift +99 -52
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore.swift +234 -167
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +10 -36
- package/Sources/Runic/Core/UsageExporter.swift +8 -4
- package/Sources/Runic/ProviderRegistry.swift +1 -1
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
- package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/provider-context-windows.json +20 -0
- package/Sources/Runic/RunicApp.swift +46 -36
- package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
- package/Sources/Runic/UsagePaceText.swift +1 -1
- package/Sources/Runic/UsageProgressBar.swift +13 -9
- package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
- package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +15 -20
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
- package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
- package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +198 -58
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +28 -76
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +108 -33
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
- package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
- package/Sources/RunicCLI/CLIEntry.swift +83 -42
- package/Sources/RunicCLI/CLIRenderer.swift +1 -1
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
- package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
- package/Sources/RunicClaudeWebProbe/main.swift +1 -1
- package/Sources/RunicCore/AlertRuleStore.swift +20 -20
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
- package/Sources/RunicCore/CostUsageModels.swift +1 -1
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +80 -71
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
- package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
- package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +5 -3
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +3 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +6 -2
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +8 -11
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +25 -16
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +27 -16
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +51 -30
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
- package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
- package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
- package/Sources/RunicCore/Team/TeamModels.swift +111 -110
- package/Sources/RunicCore/Team/TeamStore.swift +126 -109
- package/Sources/RunicCore/UsageFetcher.swift +2 -2
- package/Sources/RunicCore/UsageFormatter.swift +25 -25
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +141 -29
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +59 -5
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +161 -0
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
- package/Tests/RunicTests/AppDelegateTests.swift +8 -3
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
- package/Tests/RunicTests/CLICostTests.swift +6 -7
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
- package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
- package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
- package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
- package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
- package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
- package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
- package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
- package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
- package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
- package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
- package/Tests/RunicTests/InstallOriginTests.swift +1 -2
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
- package/Tests/RunicTests/LiveAccountTests.swift +3 -3
- package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
- package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
- package/Tests/RunicTests/PathBuilderTests.swift +14 -15
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
- package/Tests/RunicTests/RunicTests.swift +6 -7
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
- package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
- package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
- package/Tests/RunicTests/StatusMenuTests.swift +63 -22
- package/Tests/RunicTests/StatusProbeTests.swift +28 -29
- package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
- package/Tests/RunicTests/TextParsingTests.swift +1 -2
- package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
- package/Tests/RunicTests/UsagePaceTests.swift +5 -6
- package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
- package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
- package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
- package/appcast.xml +36 -37
- package/package.json +1 -1
- package/runic.png +0 -0
- package/runic.svg +15 -0
- package/version.env +2 -2
|
@@ -46,7 +46,7 @@ public enum UsageLedgerAggregator {
|
|
|
46
46
|
timeZone: TimeZone,
|
|
47
47
|
groupByProject: Bool = true) -> [UsageLedgerDailySummary]
|
|
48
48
|
{
|
|
49
|
-
let calendar = calendarFor(timeZone)
|
|
49
|
+
let calendar = self.calendarFor(timeZone)
|
|
50
50
|
var buckets: [DailyKey: AggregateAccumulator] = [:]
|
|
51
51
|
|
|
52
52
|
for entry in entries {
|
|
@@ -59,7 +59,7 @@ public enum UsageLedgerAggregator {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
return buckets.map { key, acc in
|
|
62
|
-
let dayKey = dayKeyString(from: key.dayStart, timeZone: timeZone)
|
|
62
|
+
let dayKey = self.dayKeyString(from: key.dayStart, timeZone: timeZone)
|
|
63
63
|
return UsageLedgerDailySummary(
|
|
64
64
|
provider: key.provider,
|
|
65
65
|
projectID: key.projectID,
|
|
@@ -138,7 +138,10 @@ public enum UsageLedgerAggregator {
|
|
|
138
138
|
let isActive = now <= blockEnd && now.timeIntervalSince(lastTimestamp) <= blockDuration
|
|
139
139
|
let acc = AggregateAccumulator(entries: currentEntries)
|
|
140
140
|
let tokensPerMinute = isActive ? acc.tokensPerMinute(since: currentStart, now: now) : nil
|
|
141
|
-
let projected = isActive ? acc.projectedTotalTokens(
|
|
141
|
+
let projected = isActive ? acc.projectedTotalTokens(
|
|
142
|
+
since: currentStart,
|
|
143
|
+
now: now,
|
|
144
|
+
duration: blockDuration) : nil
|
|
142
145
|
summaries.append(UsageLedgerBlockSummary(
|
|
143
146
|
provider: key.provider,
|
|
144
147
|
sessionID: key.sessionID,
|
|
@@ -283,7 +286,7 @@ public enum UsageLedgerAggregator {
|
|
|
283
286
|
groupByProject: Bool) -> [UsageLedgerSpendForecast]
|
|
284
287
|
{
|
|
285
288
|
guard projectionDays > 0 else { return [] }
|
|
286
|
-
let calendar = calendarFor(timeZone)
|
|
289
|
+
let calendar = self.calendarFor(timeZone)
|
|
287
290
|
var buckets: [SpendForecastKey: SpendForecastAccumulator] = [:]
|
|
288
291
|
|
|
289
292
|
for entry in entries {
|
|
@@ -334,7 +337,7 @@ public enum UsageLedgerAggregator {
|
|
|
334
337
|
|
|
335
338
|
private static func dayKeyString(from date: Date, timeZone: TimeZone) -> String {
|
|
336
339
|
let formatter = DateFormatter()
|
|
337
|
-
formatter.calendar = calendarFor(timeZone)
|
|
340
|
+
formatter.calendar = self.calendarFor(timeZone)
|
|
338
341
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
339
342
|
formatter.timeZone = timeZone
|
|
340
343
|
formatter.dateFormat = "yyyy-MM-dd"
|
|
@@ -343,7 +346,7 @@ public enum UsageLedgerAggregator {
|
|
|
343
346
|
|
|
344
347
|
private static func hourKeyString(from date: Date, timeZone: TimeZone) -> String {
|
|
345
348
|
let formatter = DateFormatter()
|
|
346
|
-
formatter.calendar = calendarFor(timeZone)
|
|
349
|
+
formatter.calendar = self.calendarFor(timeZone)
|
|
347
350
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
348
351
|
formatter.timeZone = timeZone
|
|
349
352
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:00:00"
|
|
@@ -355,7 +358,7 @@ public enum UsageLedgerAggregator {
|
|
|
355
358
|
timeZone: TimeZone,
|
|
356
359
|
groupByProject: Bool = true) -> [UsageLedgerHourlySummary]
|
|
357
360
|
{
|
|
358
|
-
let calendar = calendarFor(timeZone)
|
|
361
|
+
let calendar = self.calendarFor(timeZone)
|
|
359
362
|
var buckets: [HourlyKey: AggregateAccumulator] = [:]
|
|
360
363
|
|
|
361
364
|
for entry in entries {
|
|
@@ -371,7 +374,7 @@ public enum UsageLedgerAggregator {
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
return buckets.map { key, acc in
|
|
374
|
-
let hourKey = hourKeyString(from: key.hourStart, timeZone: timeZone)
|
|
377
|
+
let hourKey = self.hourKeyString(from: key.hourStart, timeZone: timeZone)
|
|
375
378
|
return UsageLedgerHourlySummary(
|
|
376
379
|
provider: key.provider,
|
|
377
380
|
projectID: key.projectID,
|
|
@@ -445,7 +448,10 @@ private struct AggregateAccumulator {
|
|
|
445
448
|
self.modelSet.sorted()
|
|
446
449
|
}
|
|
447
450
|
|
|
448
|
-
private mutating func mergeProjectIdentity(
|
|
451
|
+
private mutating func mergeProjectIdentity(
|
|
452
|
+
from identity: UsageLedgerProjectIdentity?,
|
|
453
|
+
fallbackEntry: UsageLedgerEntry)
|
|
454
|
+
{
|
|
449
455
|
if let key = identity?.key, self.projectKey == nil {
|
|
450
456
|
self.projectKey = key
|
|
451
457
|
}
|
|
@@ -468,17 +474,16 @@ private struct AggregateAccumulator {
|
|
|
468
474
|
let candidateProvenance = identity?.provenance
|
|
469
475
|
|
|
470
476
|
guard let candidateName, !candidateName.isEmpty else { return }
|
|
471
|
-
let shouldAdopt
|
|
472
|
-
|
|
473
|
-
shouldAdopt = true
|
|
477
|
+
let shouldAdopt = if self.projectName == nil {
|
|
478
|
+
true
|
|
474
479
|
} else if candidateConfidence.rank > self.projectNameConfidence.rank {
|
|
475
|
-
|
|
480
|
+
true
|
|
476
481
|
} else if candidateConfidence.rank == self.projectNameConfidence.rank,
|
|
477
482
|
candidateName.count > (self.projectName?.count ?? 0)
|
|
478
483
|
{
|
|
479
|
-
|
|
484
|
+
true
|
|
480
485
|
} else {
|
|
481
|
-
|
|
486
|
+
false
|
|
482
487
|
}
|
|
483
488
|
|
|
484
489
|
if shouldAdopt {
|
|
@@ -586,7 +591,10 @@ private struct SpendForecastAccumulator {
|
|
|
586
591
|
return lowerValue + ((upperValue - lowerValue) * weight)
|
|
587
592
|
}
|
|
588
593
|
|
|
589
|
-
private mutating func mergeProjectIdentity(
|
|
594
|
+
private mutating func mergeProjectIdentity(
|
|
595
|
+
from identity: UsageLedgerProjectIdentity?,
|
|
596
|
+
fallbackEntry: UsageLedgerEntry)
|
|
597
|
+
{
|
|
590
598
|
if self.projectID == nil {
|
|
591
599
|
let preferredID = identity?.projectID?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
592
600
|
if let preferredID, !preferredID.isEmpty {
|
|
@@ -605,17 +613,16 @@ private struct SpendForecastAccumulator {
|
|
|
605
613
|
let candidateProvenance = identity?.provenance
|
|
606
614
|
|
|
607
615
|
guard let candidateName, !candidateName.isEmpty else { return }
|
|
608
|
-
let shouldAdopt
|
|
609
|
-
|
|
610
|
-
shouldAdopt = true
|
|
616
|
+
let shouldAdopt = if self.projectName == nil {
|
|
617
|
+
true
|
|
611
618
|
} else if candidateConfidence.rank > self.projectNameConfidence.rank {
|
|
612
|
-
|
|
619
|
+
true
|
|
613
620
|
} else if candidateConfidence.rank == self.projectNameConfidence.rank,
|
|
614
621
|
candidateName.count > (self.projectName?.count ?? 0)
|
|
615
622
|
{
|
|
616
|
-
|
|
623
|
+
true
|
|
617
624
|
} else {
|
|
618
|
-
|
|
625
|
+
false
|
|
619
626
|
}
|
|
620
627
|
|
|
621
628
|
if shouldAdopt {
|
|
@@ -654,7 +661,15 @@ private struct SessionAccumulator {
|
|
|
654
661
|
}
|
|
655
662
|
}
|
|
656
663
|
|
|
657
|
-
var totals: UsageLedgerTotals {
|
|
658
|
-
|
|
659
|
-
|
|
664
|
+
var totals: UsageLedgerTotals {
|
|
665
|
+
self.aggregate.totals
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
var models: [String] {
|
|
669
|
+
self.modelSet.sorted()
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
var versions: [String] {
|
|
673
|
+
self.versionSet.sorted()
|
|
674
|
+
}
|
|
660
675
|
}
|
|
@@ -97,7 +97,7 @@ public enum UsageLedgerInsightsAdvisor {
|
|
|
97
97
|
score -= 10
|
|
98
98
|
signals.append("No project-level grouping is available.")
|
|
99
99
|
} else {
|
|
100
|
-
let unknownProjects = projectBreakdown.
|
|
100
|
+
let unknownProjects = projectBreakdown.count(where: { $0.displayProjectName == "Unknown project" })
|
|
101
101
|
let unknownRatio = Double(unknownProjects) / Double(max(1, projectBreakdown.count))
|
|
102
102
|
if unknownRatio > 0.40 {
|
|
103
103
|
score -= 10
|
|
@@ -60,11 +60,11 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
60
60
|
projectName: String?,
|
|
61
61
|
budgetNameOverride: String? = nil) -> UsageLedgerProjectIdentity
|
|
62
62
|
{
|
|
63
|
-
let normalizedProjectID = normalizedIdentifier(projectID)
|
|
64
|
-
let normalizedProjectName = normalizedDisplay(projectName)
|
|
65
|
-
let budgetName = normalizedDisplay(budgetNameOverride)
|
|
66
|
-
let workspaceCandidate = firstPathLikeValue(candidates: [projectID, projectName])
|
|
67
|
-
let workspaceHash = workspaceCandidate.map(stablePathHash)
|
|
63
|
+
let normalizedProjectID = self.normalizedIdentifier(projectID)
|
|
64
|
+
let normalizedProjectName = self.normalizedDisplay(projectName)
|
|
65
|
+
let budgetName = self.normalizedDisplay(budgetNameOverride)
|
|
66
|
+
let workspaceCandidate = self.firstPathLikeValue(candidates: [projectID, projectName])
|
|
67
|
+
let workspaceHash = workspaceCandidate.map(self.stablePathHash)
|
|
68
68
|
let workspaceSuffix = workspaceHash.map { "#w:\($0.prefix(8))" } ?? ""
|
|
69
69
|
|
|
70
70
|
var displayName: String?
|
|
@@ -103,7 +103,7 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
103
103
|
if let normalizedProjectID {
|
|
104
104
|
key = "id:\(normalizedProjectID.lowercased())\(workspaceSuffix)"
|
|
105
105
|
} else if let seed = displayName {
|
|
106
|
-
let slug = slugify(seed)
|
|
106
|
+
let slug = self.slugify(seed)
|
|
107
107
|
key = slug.isEmpty ? nil : "name:\(slug)\(workspaceSuffix)"
|
|
108
108
|
} else {
|
|
109
109
|
key = nil
|
|
@@ -129,11 +129,11 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
129
129
|
if normalized.contains("/") || normalized.contains("\\") {
|
|
130
130
|
return nil
|
|
131
131
|
}
|
|
132
|
-
return readableIdentifier(normalized)
|
|
132
|
+
return self.readableIdentifier(normalized)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
private static func normalizedIdentifier(_ value: String?) -> String? {
|
|
136
|
-
let cleaned = normalizedDisplay(value)
|
|
136
|
+
let cleaned = self.normalizedDisplay(value)
|
|
137
137
|
guard let cleaned, !cleaned.isEmpty else { return nil }
|
|
138
138
|
return cleaned
|
|
139
139
|
}
|
|
@@ -149,6 +149,43 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
149
149
|
|
|
150
150
|
private static func inferredPathTail(from value: String?) -> String? {
|
|
151
151
|
guard let value else { return nil }
|
|
152
|
+
|
|
153
|
+
// Claude Code uses dash-separated path IDs like "-Users-srinivaspendela-Sriinnu-AI-Runic".
|
|
154
|
+
// Extract the last meaningful segment(s) as the project name.
|
|
155
|
+
if value.hasPrefix("-Users-") || value.hasPrefix("-home-") {
|
|
156
|
+
let parts = value.split(separator: "-").map(String.init)
|
|
157
|
+
// Find the last non-user-path segment — skip Users, username, common dirs
|
|
158
|
+
let skipPrefixes: Set = [
|
|
159
|
+
"Users",
|
|
160
|
+
"home",
|
|
161
|
+
"Documents",
|
|
162
|
+
"Desktop",
|
|
163
|
+
"Projects",
|
|
164
|
+
"Personal",
|
|
165
|
+
"repos",
|
|
166
|
+
"code",
|
|
167
|
+
"src",
|
|
168
|
+
"dev",
|
|
169
|
+
"Work",
|
|
170
|
+
"Sites",
|
|
171
|
+
]
|
|
172
|
+
var meaningful: [String] = []
|
|
173
|
+
var pastUserPath = false
|
|
174
|
+
for part in parts where !part.isEmpty {
|
|
175
|
+
if !pastUserPath {
|
|
176
|
+
if skipPrefixes.contains(part) || part.count <= 2 { continue }
|
|
177
|
+
pastUserPath = true
|
|
178
|
+
}
|
|
179
|
+
meaningful.append(part)
|
|
180
|
+
}
|
|
181
|
+
// Take the last 1-2 segments as the project name
|
|
182
|
+
if meaningful.count >= 2 {
|
|
183
|
+
return meaningful.suffix(2).joined(separator: "/")
|
|
184
|
+
} else if let last = meaningful.last {
|
|
185
|
+
return last
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
let separators = CharacterSet(charactersIn: "/\\")
|
|
153
190
|
guard value.rangeOfCharacter(from: separators) != nil else { return nil }
|
|
154
191
|
|
|
@@ -156,23 +193,23 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
156
193
|
ch == "/" || ch == "\\"
|
|
157
194
|
}
|
|
158
195
|
guard let last = parts.last else { return nil }
|
|
159
|
-
let tail = normalizedDisplay(String(last))
|
|
196
|
+
let tail = self.normalizedDisplay(String(last))
|
|
160
197
|
guard let tail else { return nil }
|
|
161
|
-
guard !looksOpaqueIdentifier(tail) else { return nil }
|
|
198
|
+
guard !self.looksOpaqueIdentifier(tail) else { return nil }
|
|
162
199
|
return tail
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
private static func readableIdentifier(_ value: String?) -> String? {
|
|
166
203
|
guard let value else { return nil }
|
|
167
204
|
guard !value.isEmpty else { return nil }
|
|
168
|
-
guard !looksOpaqueIdentifier(value) else { return nil }
|
|
205
|
+
guard !self.looksOpaqueIdentifier(value) else { return nil }
|
|
169
206
|
return value
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
private static func inferredNameToken(from value: String?) -> String? {
|
|
173
|
-
let normalized = normalizedDisplay(value)
|
|
210
|
+
let normalized = self.normalizedDisplay(value)
|
|
174
211
|
guard let normalized, !normalized.isEmpty else { return nil }
|
|
175
|
-
let slug = slugify(normalized)
|
|
212
|
+
let slug = self.slugify(normalized)
|
|
176
213
|
guard !slug.isEmpty else { return nil }
|
|
177
214
|
let titleCased = slug.split(separator: "-")
|
|
178
215
|
.map { token -> String in
|
|
@@ -197,7 +234,9 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
197
234
|
}
|
|
198
235
|
|
|
199
236
|
private static func slugify(_ value: String) -> String {
|
|
200
|
-
let folded = value.folding(
|
|
237
|
+
let folded = value.folding(
|
|
238
|
+
options: [.diacriticInsensitive, .caseInsensitive, .widthInsensitive],
|
|
239
|
+
locale: .init(identifier: "en_US_POSIX"))
|
|
201
240
|
var scalarBuffer: [UnicodeScalar] = []
|
|
202
241
|
scalarBuffer.reserveCapacity(folded.unicodeScalars.count)
|
|
203
242
|
|
|
@@ -235,10 +274,10 @@ public enum UsageLedgerProjectIdentityResolver {
|
|
|
235
274
|
|
|
236
275
|
private static func stablePathHash(_ value: String) -> String {
|
|
237
276
|
let normalized = value.lowercased()
|
|
238
|
-
var hash: UInt64 =
|
|
277
|
+
var hash: UInt64 = 0xCBF2_9CE4_8422_2325
|
|
239
278
|
for byte in normalized.utf8 {
|
|
240
279
|
hash ^= UInt64(byte)
|
|
241
|
-
hash = hash &*
|
|
280
|
+
hash = hash &* 0x100_0000_01B3
|
|
242
281
|
}
|
|
243
282
|
return String(format: "%016llx", hash)
|
|
244
283
|
}
|
|
@@ -43,21 +43,21 @@ enum CostUsageCacheIO {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
struct CostUsageCache: Codable
|
|
46
|
+
struct CostUsageCache: Codable {
|
|
47
47
|
var version: Int = 1
|
|
48
48
|
var lastScanUnixMs: Int64 = 0
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/// filePath -> file usage
|
|
51
51
|
var files: [String: CostUsageFileUsage] = [:]
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/// dayKey -> model -> packed usage
|
|
54
54
|
var days: [String: [String: [Int]]] = [:]
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/// rootPath -> mtime (for Claude roots)
|
|
57
57
|
var roots: [String: Int64]?
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
struct CostUsageFileUsage: Codable
|
|
60
|
+
struct CostUsageFileUsage: Codable {
|
|
61
61
|
var mtimeUnixMs: Int64
|
|
62
62
|
var size: Int64
|
|
63
63
|
var days: [String: [String: [Int]]]
|
|
@@ -66,7 +66,7 @@ struct CostUsageFileUsage: Codable, Sendable {
|
|
|
66
66
|
var lastTotals: CostUsageCodexTotals?
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
struct CostUsageCodexTotals: Codable
|
|
69
|
+
struct CostUsageCodexTotals: Codable {
|
|
70
70
|
var input: Int
|
|
71
71
|
var cached: Int
|
|
72
72
|
var output: Int
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
enum CostUsagePricing {
|
|
4
|
-
struct CodexPricing
|
|
4
|
+
struct CodexPricing {
|
|
5
5
|
let inputCostPerToken: Double
|
|
6
6
|
let outputCostPerToken: Double
|
|
7
7
|
let cacheReadInputCostPerToken: Double
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
struct ClaudePricing
|
|
10
|
+
struct ClaudePricing {
|
|
11
11
|
let inputCostPerToken: Double
|
|
12
12
|
let outputCostPerToken: Double
|
|
13
13
|
let cacheCreationInputCostPerToken: Double
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
enum CostUsageScanner {
|
|
4
|
-
struct Options
|
|
4
|
+
struct Options {
|
|
5
5
|
var codexSessionsRoot: URL?
|
|
6
6
|
var claudeProjectsRoots: [URL]?
|
|
7
7
|
var cacheRoot: URL?
|
|
8
8
|
var refreshMinIntervalSeconds: TimeInterval = 60
|
|
9
|
-
|
|
9
|
+
/// Force a full rescan, ignoring per-file cache and incremental offsets.
|
|
10
10
|
var forceRescan: Bool = false
|
|
11
11
|
|
|
12
12
|
init(
|
|
@@ -22,14 +22,14 @@ enum CostUsageScanner {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
struct CodexParseResult
|
|
25
|
+
struct CodexParseResult {
|
|
26
26
|
let days: [String: [String: [Int]]]
|
|
27
27
|
let parsedBytes: Int64
|
|
28
28
|
let lastModel: String?
|
|
29
29
|
let lastTotals: CostUsageCodexTotals?
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
struct ClaudeParseResult
|
|
32
|
+
struct ClaudeParseResult {
|
|
33
33
|
let days: [String: [String: [Int]]]
|
|
34
34
|
let parsedBytes: Int64
|
|
35
35
|
}
|
|
@@ -101,7 +101,7 @@ enum CostUsageScanner {
|
|
|
101
101
|
|
|
102
102
|
// MARK: - Day keys
|
|
103
103
|
|
|
104
|
-
struct CostUsageDayRange
|
|
104
|
+
struct CostUsageDayRange {
|
|
105
105
|
let sinceKey: String
|
|
106
106
|
let untilKey: String
|
|
107
107
|
let scanSinceKey: String
|
|
@@ -4,10 +4,9 @@ import Testing
|
|
|
4
4
|
@testable import Runic
|
|
5
5
|
|
|
6
6
|
@MainActor
|
|
7
|
-
@Suite
|
|
8
7
|
struct AppDelegateTests {
|
|
9
8
|
@Test
|
|
10
|
-
func
|
|
9
|
+
func `builds status controller after launch`() {
|
|
11
10
|
let appDelegate = AppDelegate()
|
|
12
11
|
var factoryCalls = 0
|
|
13
12
|
|
|
@@ -18,7 +17,13 @@ struct AppDelegateTests {
|
|
|
18
17
|
}
|
|
19
18
|
defer { StatusItemController.factory = StatusItemController.defaultFactory }
|
|
20
19
|
|
|
21
|
-
let settings = SettingsStore(
|
|
20
|
+
let settings = SettingsStore(
|
|
21
|
+
zaiTokenStore: NoopZaiTokenStore(),
|
|
22
|
+
minimaxTokenStore: NoopMiniMaxTokenStore(),
|
|
23
|
+
minimaxCookieHeaderStore: NoopMiniMaxCookieHeaderStore(),
|
|
24
|
+
minimaxGroupIDStore: NoopMiniMaxGroupIDStore(),
|
|
25
|
+
openRouterTokenStore: NoopOpenRouterTokenStore(),
|
|
26
|
+
groqTokenStore: NoopGroqTokenStore())
|
|
22
27
|
let fetcher = UsageFetcher()
|
|
23
28
|
let store = UsageStore(fetcher: fetcher, settings: settings)
|
|
24
29
|
let account = fetcher.loadAccountInfo()
|
|
@@ -2,18 +2,17 @@ import RunicCore
|
|
|
2
2
|
import Silo
|
|
3
3
|
import Testing
|
|
4
4
|
|
|
5
|
-
@Suite
|
|
6
5
|
struct BrowserCookieOrderStatusStringTests {
|
|
7
6
|
#if os(macOS)
|
|
8
7
|
@Test
|
|
9
|
-
func
|
|
8
|
+
func `cursor no session includes browser login hint`() {
|
|
10
9
|
let order = ProviderDefaults.metadata[.cursor]?.browserCookieOrder ?? Browser.defaultImportOrder
|
|
11
10
|
let message = CursorStatusProbeError.noSessionCookie.errorDescription ?? ""
|
|
12
11
|
#expect(message.contains(order.loginHint))
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
@Test
|
|
16
|
-
func
|
|
15
|
+
func `factory no session includes browser login hint`() {
|
|
17
16
|
let order = ProviderDefaults.metadata[.factory]?.browserCookieOrder ?? Browser.defaultImportOrder
|
|
18
17
|
let message = FactoryStatusProbeError.noSessionCookie.errorDescription ?? ""
|
|
19
18
|
#expect(message.contains(order.loginHint))
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import Helix
|
|
2
2
|
import Testing
|
|
3
3
|
|
|
4
|
-
@Suite
|
|
5
4
|
struct CLIArgumentParsingTests {
|
|
6
5
|
@Test
|
|
7
|
-
func
|
|
6
|
+
func `usage command parses provider and json flags`() throws {
|
|
8
7
|
let signature = CommandSignature(
|
|
9
8
|
options: [
|
|
10
9
|
OptionDefinition(label: "provider", names: [.long("provider")], help: nil),
|
|
@@ -25,7 +24,7 @@ struct CLIArgumentParsingTests {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
@Test
|
|
28
|
-
func
|
|
27
|
+
func `cost command parses refresh flag`() throws {
|
|
29
28
|
let signature = CommandSignature(
|
|
30
29
|
options: [
|
|
31
30
|
OptionDefinition(label: "provider", names: [.long("provider")], help: nil),
|
|
@@ -41,7 +40,7 @@ struct CLIArgumentParsingTests {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
@Test
|
|
44
|
-
func
|
|
43
|
+
func `insights command parses view and budget flags`() throws {
|
|
45
44
|
let signature = CommandSignature(
|
|
46
45
|
options: [
|
|
47
46
|
OptionDefinition(label: "view", names: [.long("view")], help: nil),
|
|
@@ -2,26 +2,25 @@ import Foundation
|
|
|
2
2
|
import RunicCore
|
|
3
3
|
import Testing
|
|
4
4
|
|
|
5
|
-
@Suite
|
|
6
5
|
struct CLICostTests {
|
|
7
6
|
@Test
|
|
8
|
-
func
|
|
7
|
+
func `cost snapshot stores session and month fields`() {
|
|
9
8
|
let snapshot = CostUsageTokenSnapshot(
|
|
10
|
-
sessionTokens:
|
|
9
|
+
sessionTokens: 1200,
|
|
11
10
|
sessionCostUSD: 1.25,
|
|
12
|
-
last30DaysTokens:
|
|
11
|
+
last30DaysTokens: 9000,
|
|
13
12
|
last30DaysCostUSD: 9.99,
|
|
14
13
|
daily: [],
|
|
15
14
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
|
|
16
15
|
|
|
17
|
-
#expect(snapshot.sessionTokens ==
|
|
16
|
+
#expect(snapshot.sessionTokens == 1200)
|
|
18
17
|
#expect(snapshot.sessionCostUSD == 1.25)
|
|
19
|
-
#expect(snapshot.last30DaysTokens ==
|
|
18
|
+
#expect(snapshot.last30DaysTokens == 9000)
|
|
20
19
|
#expect(snapshot.last30DaysCostUSD == 9.99)
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
@Test
|
|
24
|
-
func
|
|
23
|
+
func `daily report decodes legacy cache token keys`() throws {
|
|
25
24
|
let json = """
|
|
26
25
|
{
|
|
27
26
|
"daily": [
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import RunicCore
|
|
2
2
|
import Testing
|
|
3
3
|
|
|
4
|
-
@Suite
|
|
5
4
|
struct CLIProviderSelectionTests {
|
|
6
5
|
@Test
|
|
7
|
-
func
|
|
6
|
+
func `provider registry includes core providers`() {
|
|
8
7
|
let providers = Set(ProviderDescriptorRegistry.all.map(\.id))
|
|
9
8
|
|
|
10
9
|
#expect(providers.contains(.codex))
|
|
@@ -15,7 +14,7 @@ struct CLIProviderSelectionTests {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
@Test
|
|
18
|
-
func
|
|
17
|
+
func `provider enum resolves insights supported values`() {
|
|
19
18
|
#expect(UsageProvider(rawValue: "codex") == .codex)
|
|
20
19
|
#expect(UsageProvider(rawValue: "claude") == .claude)
|
|
21
20
|
}
|
|
@@ -3,10 +3,9 @@ import RunicCore
|
|
|
3
3
|
import Testing
|
|
4
4
|
@testable import RunicCLI
|
|
5
5
|
|
|
6
|
-
@Suite
|
|
7
6
|
struct CLISnapshotTests {
|
|
8
7
|
@Test
|
|
9
|
-
func
|
|
8
|
+
func `renders codex text snapshot`() {
|
|
10
9
|
let identity = ProviderIdentitySnapshot(
|
|
11
10
|
providerID: .codex,
|
|
12
11
|
accountEmail: "user@example.com",
|
|
@@ -34,7 +33,7 @@ struct CLISnapshotTests {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
@Test
|
|
37
|
-
func
|
|
36
|
+
func `renders claude snapshot without weekly when missing`() {
|
|
38
37
|
let snapshot = UsageSnapshot(
|
|
39
38
|
primary: .init(usedPercent: 2, windowMinutes: nil, resetsAt: nil, resetDescription: "3pm (Europe/Vienna)"),
|
|
40
39
|
secondary: nil,
|
|
@@ -52,7 +51,7 @@ struct CLISnapshotTests {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
@Test
|
|
55
|
-
func
|
|
54
|
+
func `applies ansi colors when enabled`() {
|
|
56
55
|
let snapshot = UsageSnapshot(
|
|
57
56
|
primary: .init(usedPercent: 95, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
58
57
|
secondary: .init(usedPercent: 80, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
@@ -2,7 +2,6 @@ import Testing
|
|
|
2
2
|
@testable import RunicCLI
|
|
3
3
|
@testable import RunicCore
|
|
4
4
|
|
|
5
|
-
@Suite
|
|
6
5
|
struct CLIWebFallbackTests {
|
|
7
6
|
private func makeContext(sourceMode: ProviderSourceMode = .auto) -> ProviderFetchContext {
|
|
8
7
|
ProviderFetchContext(
|
|
@@ -19,7 +18,7 @@ struct CLIWebFallbackTests {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
@Test
|
|
22
|
-
func
|
|
21
|
+
func `codex falls back when cookies missing`() {
|
|
23
22
|
let context = self.makeContext()
|
|
24
23
|
let strategy = CodexWebDashboardStrategy()
|
|
25
24
|
#expect(strategy.shouldFallback(
|
|
@@ -40,7 +39,7 @@ struct CLIWebFallbackTests {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
@Test
|
|
43
|
-
func
|
|
42
|
+
func `codex does not fallback for dashboard data errors`() {
|
|
44
43
|
let context = self.makeContext()
|
|
45
44
|
let strategy = CodexWebDashboardStrategy()
|
|
46
45
|
#expect(!strategy.shouldFallback(
|
|
@@ -49,7 +48,7 @@ struct CLIWebFallbackTests {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
@Test
|
|
52
|
-
func
|
|
51
|
+
func `claude falls back when no session key`() {
|
|
53
52
|
let context = self.makeContext()
|
|
54
53
|
let strategy = ClaudeWebFetchStrategy()
|
|
55
54
|
#expect(strategy.shouldFallback(on: ClaudeWebAPIFetcher.FetchError.noSessionKeyFound, context: context))
|