@sriinnu/runic 1.2.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/README.md +8 -1
- 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/Sources/Runic/About.swift +3 -3
- package/Sources/Runic/AppNotifications.swift +5 -3
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +7 -3
- 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 +18 -13
- 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 +1 -1
- 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 +1 -3
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/SettingsStore.swift +89 -51
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +1 -3
- 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 +187 -161
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +1 -3
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +1 -2
- 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/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 +12 -10
- 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 +18 -15
- 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 +22 -17
- 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 +44 -39
- 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 +73 -75
- 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/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 +3 -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/CodexOAuthUsageFetcher.swift +1 -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/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 +2 -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 +5 -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 +7 -7
- 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 +23 -10
- 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 +26 -13
- 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 +48 -21
- 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 +27 -13
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +11 -3
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +5 -2
- 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 -76
- package/package.json +1 -1
- package/runic.png +0 -0
- package/runic.svg +15 -0
- package/version.env +2 -2
|
@@ -97,7 +97,7 @@ public enum UsageFormatter {
|
|
|
97
97
|
guard tokenCount > 0 else { return nil }
|
|
98
98
|
let rate = costUSD / (Double(tokenCount) / 1000.0)
|
|
99
99
|
guard rate.isFinite, rate >= 0 else { return nil }
|
|
100
|
-
return "\(usdRateString(rate))/1K"
|
|
100
|
+
return "\(self.usdRateString(rate))/1K"
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
public static func usdPerRequestString(costUSD: Double?, requestCount: Int) -> String? {
|
|
@@ -105,7 +105,7 @@ public enum UsageFormatter {
|
|
|
105
105
|
guard requestCount > 0 else { return nil }
|
|
106
106
|
let rate = costUSD / Double(requestCount)
|
|
107
107
|
guard rate.isFinite, rate >= 0 else { return nil }
|
|
108
|
-
return "\(usdRateString(rate))/req"
|
|
108
|
+
return "\(self.usdRateString(rate))/req"
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
public static func usdPerHourFromTokensString(
|
|
@@ -120,7 +120,7 @@ public enum UsageFormatter {
|
|
|
120
120
|
guard per1KValue.isFinite, per1KValue >= 0 else { return nil }
|
|
121
121
|
let hourly = per1KValue * (tokensPerMinute * 60.0 / 1000.0)
|
|
122
122
|
guard hourly.isFinite, hourly >= 0 else { return nil }
|
|
123
|
-
return "\(usdRateString(hourly))/hr"
|
|
123
|
+
return "\(self.usdRateString(hourly))/hr"
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
public static func currencyString(_ value: Double, currencyCode: String) -> String {
|
|
@@ -164,21 +164,21 @@ public enum UsageFormatter {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
public static func tokenSummaryString(_ totals: UsageLedgerTotals, includeBreakdown: Bool = true) -> String {
|
|
167
|
-
let total = tokenCountString(totals.totalTokens)
|
|
167
|
+
let total = self.tokenCountString(totals.totalTokens)
|
|
168
168
|
guard includeBreakdown else {
|
|
169
169
|
return "\(total) tok"
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
var parts: [String] = []
|
|
173
173
|
if totals.inputTokens > 0 {
|
|
174
|
-
parts.append("in \(tokenCountString(totals.inputTokens))")
|
|
174
|
+
parts.append("in \(self.tokenCountString(totals.inputTokens))")
|
|
175
175
|
}
|
|
176
176
|
if totals.outputTokens > 0 {
|
|
177
|
-
parts.append("out \(tokenCountString(totals.outputTokens))")
|
|
177
|
+
parts.append("out \(self.tokenCountString(totals.outputTokens))")
|
|
178
178
|
}
|
|
179
179
|
let cached = totals.cacheCreationTokens + totals.cacheReadTokens
|
|
180
180
|
if cached > 0 {
|
|
181
|
-
parts.append("cache \(tokenCountString(cached))")
|
|
181
|
+
parts.append("cache \(self.tokenCountString(cached))")
|
|
182
182
|
}
|
|
183
183
|
guard !parts.isEmpty else { return "\(total) tok" }
|
|
184
184
|
return "\(total) tok (\(parts.joined(separator: ", ")))"
|
|
@@ -248,8 +248,8 @@ public enum UsageFormatter {
|
|
|
248
248
|
/// Returns a human-friendly context window label for a model when known.
|
|
249
249
|
/// Examples: "ctx 128k", "ctx 1M".
|
|
250
250
|
public static func modelContextLabel(for model: String) -> String? {
|
|
251
|
-
guard let context =
|
|
252
|
-
return "ctx \(tokenCountString(context))"
|
|
251
|
+
guard let context = modelContextWindow(for: model) else { return nil }
|
|
252
|
+
return "ctx \(self.tokenCountString(context))"
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/// Resolves an approximate context window (in tokens) for common model families.
|
|
@@ -285,7 +285,7 @@ public enum UsageFormatter {
|
|
|
285
285
|
"gpt-4o-mini": 128_000,
|
|
286
286
|
"gpt-4-turbo": 128_000,
|
|
287
287
|
"gpt-4": 128_000,
|
|
288
|
-
"gpt-3.5-turbo":
|
|
288
|
+
"gpt-3.5-turbo": 16385,
|
|
289
289
|
"gpt-4.1": 1_000_000,
|
|
290
290
|
"gpt-4.1-mini": 1_000_000,
|
|
291
291
|
"gpt-4.1-nano": 1_000_000,
|
|
@@ -315,33 +315,33 @@ public enum UsageFormatter {
|
|
|
315
315
|
"llama-3-1-8b": 128_000,
|
|
316
316
|
"llama-3-3-70b": 128_000,
|
|
317
317
|
"mistral-large": 128_000,
|
|
318
|
-
"mistral-small":
|
|
318
|
+
"mistral-small": 32000,
|
|
319
319
|
"mistral-medium": 131_072,
|
|
320
|
-
"deepseek-chat":
|
|
321
|
-
"deepseek-coder":
|
|
320
|
+
"deepseek-chat": 64000,
|
|
321
|
+
"deepseek-coder": 32000,
|
|
322
322
|
"cohere-command": 128_000,
|
|
323
323
|
"cohere-command-r": 128_000,
|
|
324
324
|
"cohere-command-r-plus": 128_000,
|
|
325
325
|
"qwen-2-5-72b": 128_000,
|
|
326
326
|
"qwen-2-5-32b": 128_000,
|
|
327
|
-
"qwen1-5-32b":
|
|
328
|
-
"qwen1-5-14b":
|
|
327
|
+
"qwen1-5-32b": 8192,
|
|
328
|
+
"qwen1-5-14b": 8192,
|
|
329
329
|
"grok-2": 128_000,
|
|
330
330
|
"grok-2-mini": 131_072,
|
|
331
331
|
"grok-2-vision": 131_072,
|
|
332
|
-
"mixtral-8x7b":
|
|
333
|
-
"mixtral-8x22b":
|
|
334
|
-
"llama-3-8b":
|
|
335
|
-
"llama-3-70b":
|
|
336
|
-
"llama-2-70b":
|
|
337
|
-
"llama-2-13b":
|
|
332
|
+
"mixtral-8x7b": 32768,
|
|
333
|
+
"mixtral-8x22b": 64000,
|
|
334
|
+
"llama-3-8b": 8192,
|
|
335
|
+
"llama-3-70b": 8192,
|
|
336
|
+
"llama-2-70b": 4096,
|
|
337
|
+
"llama-2-13b": 4096,
|
|
338
338
|
]
|
|
339
339
|
|
|
340
340
|
private static let modelContextPrefixes: [(String, Int)] = [
|
|
341
341
|
("gpt-4.1", 1_000_000),
|
|
342
342
|
("gpt-4o", 128_000),
|
|
343
343
|
("gpt-4-", 128_000),
|
|
344
|
-
("gpt-3.5",
|
|
344
|
+
("gpt-3.5", 16385),
|
|
345
345
|
("gpt-5", 400_000),
|
|
346
346
|
("gpt-5-", 400_000),
|
|
347
347
|
("claude-3", 200_000),
|
|
@@ -351,9 +351,9 @@ public enum UsageFormatter {
|
|
|
351
351
|
("gemini-1-5", 1_000_000),
|
|
352
352
|
("gemini-2-0", 1_000_000),
|
|
353
353
|
("gemini-2-5", 1_000_000),
|
|
354
|
-
("llama-3",
|
|
354
|
+
("llama-3", 8192),
|
|
355
355
|
("qwen-2-5", 128_000),
|
|
356
|
-
("qwen1-5",
|
|
356
|
+
("qwen1-5", 8192),
|
|
357
357
|
]
|
|
358
358
|
|
|
359
359
|
private static func normalizedModelIdentifier(for model: String) -> String {
|
|
@@ -370,7 +370,7 @@ public enum UsageFormatter {
|
|
|
370
370
|
for token in tokens.reversed() {
|
|
371
371
|
let lower = token.lowercased()
|
|
372
372
|
if lower.hasSuffix("k"), let numeric = Int(lower.dropLast()), numeric > 0 {
|
|
373
|
-
return numeric *
|
|
373
|
+
return numeric * 1000
|
|
374
374
|
}
|
|
375
375
|
if lower.hasSuffix("m"), let numeric = Int(lower.dropLast()), numeric > 0 {
|
|
376
376
|
return numeric * 1_000_000
|
|
@@ -56,7 +56,9 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
56
56
|
// Filter to only files modified since last scan (plus today's files which may have grown).
|
|
57
57
|
let todayStart = Calendar.current.startOfDay(for: self.now)
|
|
58
58
|
let filesToScan = allFiles.filter { file in
|
|
59
|
-
guard let modDate = (try? self.fileManager
|
|
59
|
+
guard let modDate = (try? self.fileManager
|
|
60
|
+
.attributesOfItem(atPath: file.url.path))?[.modificationDate] as? Date
|
|
61
|
+
else {
|
|
60
62
|
return true
|
|
61
63
|
}
|
|
62
64
|
return modDate >= incrementalCutoff || modDate >= todayStart
|
|
@@ -66,7 +68,9 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
66
68
|
return []
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
self.log
|
|
71
|
+
self.log
|
|
72
|
+
.info(
|
|
73
|
+
"Scanning \(filesToScan.count) of \(allFiles.count) JSONL files (incremental since \(incrementalCutoff))")
|
|
70
74
|
|
|
71
75
|
// Parse files concurrently (max 16 at a time).
|
|
72
76
|
let maxConcurrency = 16
|
|
@@ -118,7 +122,14 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
118
122
|
dailyFormatter.dateFormat = "yyyy-MM-dd"
|
|
119
123
|
dailyFormatter.timeZone = .current
|
|
120
124
|
|
|
121
|
-
var dailyBuckets: [String: (
|
|
125
|
+
var dailyBuckets: [String: (
|
|
126
|
+
input: Int,
|
|
127
|
+
output: Int,
|
|
128
|
+
cacheCreate: Int,
|
|
129
|
+
cacheRead: Int,
|
|
130
|
+
cost: Double,
|
|
131
|
+
requests: Int,
|
|
132
|
+
models: Set<String>)] = [:]
|
|
122
133
|
for entry in entries {
|
|
123
134
|
let key = dailyFormatter.string(from: entry.timestamp)
|
|
124
135
|
var bucket = dailyBuckets[key] ?? (0, 0, 0, 0, 0, 0, [])
|
|
@@ -188,24 +199,22 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
188
199
|
|
|
189
200
|
var candidates: [URL] = []
|
|
190
201
|
if !envPaths.isEmpty {
|
|
191
|
-
candidates = envPaths.map { expandTilde(path: String($0)) }
|
|
202
|
+
candidates = envPaths.map { self.expandTilde(path: String($0)) }
|
|
192
203
|
} else {
|
|
193
|
-
let home = fileManager.homeDirectoryForCurrentUser
|
|
204
|
+
let home = self.fileManager.homeDirectoryForCurrentUser
|
|
194
205
|
candidates = [
|
|
195
206
|
home.appendingPathComponent(".config/claude", isDirectory: true),
|
|
196
207
|
home.appendingPathComponent(".claude", isDirectory: true),
|
|
197
208
|
]
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
|
|
211
|
+
return candidates
|
|
201
212
|
.map { $0.appendingPathComponent("projects", isDirectory: true) }
|
|
202
213
|
.filter { self.fileManager.directoryExists(at: $0) }
|
|
203
|
-
|
|
204
|
-
return projectsDirs
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
private func normalizePaths(_ urls: [URL]) -> [URL] {
|
|
208
|
-
urls.map
|
|
217
|
+
urls.map(\.standardizedFileURL)
|
|
209
218
|
.filter { self.fileManager.directoryExists(at: $0) }
|
|
210
219
|
.map { $0.appendingPathComponent("projects", isDirectory: true) }
|
|
211
220
|
.filter { self.fileManager.directoryExists(at: $0) }
|
|
@@ -236,7 +245,11 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
236
245
|
let projectID = pathComponents[projectsIndex + 1]
|
|
237
246
|
let projectName = self.derivedProjectName(from: projectID)
|
|
238
247
|
let sessionID = pathComponents[projectsIndex + 2]
|
|
239
|
-
results.append(UsageFile(
|
|
248
|
+
results.append(UsageFile(
|
|
249
|
+
url: fileURL,
|
|
250
|
+
projectID: projectID,
|
|
251
|
+
projectName: projectName,
|
|
252
|
+
sessionID: sessionID))
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
|
|
@@ -355,7 +368,7 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
355
368
|
|
|
356
369
|
private func expandTilde(path: String) -> URL {
|
|
357
370
|
if path.hasPrefix("~/") {
|
|
358
|
-
let home = fileManager.homeDirectoryForCurrentUser
|
|
371
|
+
let home = self.fileManager.homeDirectoryForCurrentUser
|
|
359
372
|
let trimmed = String(path.dropFirst(2))
|
|
360
373
|
return home.appendingPathComponent(trimmed, isDirectory: true)
|
|
361
374
|
}
|
|
@@ -410,6 +423,7 @@ private final class ClaudeISO8601FormatterBox: @unchecked Sendable {
|
|
|
410
423
|
formatter.formatOptions = [.withInternetDateTime]
|
|
411
424
|
return formatter
|
|
412
425
|
}()
|
|
426
|
+
|
|
413
427
|
let fractional: ISO8601DateFormatter = {
|
|
414
428
|
let formatter = ISO8601DateFormatter()
|
|
415
429
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
@@ -419,8 +433,8 @@ private final class ClaudeISO8601FormatterBox: @unchecked Sendable {
|
|
|
419
433
|
|
|
420
434
|
private let claudeISOFormatterBox = ClaudeISO8601FormatterBox()
|
|
421
435
|
|
|
422
|
-
|
|
423
|
-
func directoryExists(at url: URL) -> Bool {
|
|
436
|
+
extension FileManager {
|
|
437
|
+
fileprivate func directoryExists(at url: URL) -> Bool {
|
|
424
438
|
var isDir: ObjCBool = false
|
|
425
439
|
return self.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue
|
|
426
440
|
}
|
|
@@ -123,7 +123,14 @@ public struct CodexUsageLogSource: UsageLedgerSource, @unchecked Sendable {
|
|
|
123
123
|
dailyFormatter.dateFormat = "yyyy-MM-dd"
|
|
124
124
|
dailyFormatter.timeZone = .current
|
|
125
125
|
|
|
126
|
-
var dailyBuckets: [String: (
|
|
126
|
+
var dailyBuckets: [String: (
|
|
127
|
+
input: Int,
|
|
128
|
+
output: Int,
|
|
129
|
+
cacheCreate: Int,
|
|
130
|
+
cacheRead: Int,
|
|
131
|
+
cost: Double,
|
|
132
|
+
requests: Int,
|
|
133
|
+
models: Set<String>)] = [:]
|
|
127
134
|
for entry in entries {
|
|
128
135
|
let key = dailyFormatter.string(from: entry.timestamp)
|
|
129
136
|
var bucket = dailyBuckets[key] ?? (0, 0, 0, 0, 0, 0, [])
|
|
@@ -477,6 +484,7 @@ private final class CodexISO8601FormatterBox: @unchecked Sendable {
|
|
|
477
484
|
formatter.formatOptions = [.withInternetDateTime]
|
|
478
485
|
return formatter
|
|
479
486
|
}()
|
|
487
|
+
|
|
480
488
|
let fractional: ISO8601DateFormatter = {
|
|
481
489
|
let formatter = ISO8601DateFormatter()
|
|
482
490
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
@@ -486,8 +494,8 @@ private final class CodexISO8601FormatterBox: @unchecked Sendable {
|
|
|
486
494
|
|
|
487
495
|
private let codexISOFormatterBox = CodexISO8601FormatterBox()
|
|
488
496
|
|
|
489
|
-
|
|
490
|
-
func directoryExists(at url: URL) -> Bool {
|
|
497
|
+
extension FileManager {
|
|
498
|
+
fileprivate func directoryExists(at url: URL) -> Bool {
|
|
491
499
|
var isDir: ObjCBool = false
|
|
492
500
|
return self.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue
|
|
493
501
|
}
|
|
@@ -49,7 +49,10 @@ public actor LedgerCache {
|
|
|
49
49
|
/// - Today's entry is ACCUMULATED (adds to existing totals for partial scans)
|
|
50
50
|
/// - New days are appended
|
|
51
51
|
public func mergeDailies(provider: String, newDailies: [CachedDaily], scanDate: Date, todayKey: String? = nil) {
|
|
52
|
-
var ledger = self.loadCachedDailies(provider: provider) ?? CachedLedger(
|
|
52
|
+
var ledger = self.loadCachedDailies(provider: provider) ?? CachedLedger(
|
|
53
|
+
lastScanDate: scanDate,
|
|
54
|
+
lastFullScanDate: nil,
|
|
55
|
+
dailies: [])
|
|
53
56
|
|
|
54
57
|
let resolvedTodayKey = todayKey ?? Self.dayKey(for: Date())
|
|
55
58
|
var existingByKey: [String: CachedDaily] = [:]
|
|
@@ -119,7 +122,7 @@ public struct CachedLedger: Codable, Sendable {
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
public struct CachedDaily: Codable, Sendable {
|
|
122
|
-
public let dayKey: String
|
|
125
|
+
public let dayKey: String // "2026-03-23"
|
|
123
126
|
public let inputTokens: Int
|
|
124
127
|
public let outputTokens: Int
|
|
125
128
|
public let cacheCreationTokens: Int
|
|
@@ -29,7 +29,7 @@ public enum OTelGenAILedgerAdapterError: LocalizedError, Sendable, Equatable {
|
|
|
29
29
|
switch self {
|
|
30
30
|
case .invalidUTF8:
|
|
31
31
|
"OTel payload is not valid UTF-8."
|
|
32
|
-
case .invalidJSON(
|
|
32
|
+
case let .invalidJSON(message):
|
|
33
33
|
"OTel JSON parse failed: \(message)"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -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
|