@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
|
@@ -2,8 +2,8 @@ import Foundation
|
|
|
2
2
|
|
|
3
3
|
// MARK: - Example Usage of Runic Sync Infrastructure
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// This file demonstrates how to integrate and use the Runic sync infrastructure
|
|
6
|
+
// in your macOS/iOS application. These examples are for documentation purposes.
|
|
7
7
|
|
|
8
8
|
#if DEBUG
|
|
9
9
|
|
|
@@ -16,8 +16,7 @@ func setupSyncExample() async {
|
|
|
16
16
|
// Create background sync manager with default configuration
|
|
17
17
|
let syncManager = BackgroundSyncManager(
|
|
18
18
|
syncEngine: syncEngine,
|
|
19
|
-
config: .default
|
|
20
|
-
)
|
|
19
|
+
config: .default)
|
|
21
20
|
|
|
22
21
|
// Start automatic background sync
|
|
23
22
|
await syncManager.start()
|
|
@@ -30,19 +29,17 @@ func setupSyncExample() async {
|
|
|
30
29
|
func customConfigurationExample() async {
|
|
31
30
|
// Create custom sync configuration
|
|
32
31
|
let config = SyncConfiguration(
|
|
33
|
-
activeSyncInterval: 180,
|
|
32
|
+
activeSyncInterval: 180, // 3 minutes when app is active
|
|
34
33
|
backgroundSyncInterval: 1800, // 30 minutes in background
|
|
35
|
-
syncOnForeground: true,
|
|
36
|
-
syncOnBackground: true,
|
|
34
|
+
syncOnForeground: true, // Sync when app comes to foreground
|
|
35
|
+
syncOnBackground: true, // Sync when app goes to background
|
|
37
36
|
defaultSyncOptions: SyncOptions(
|
|
38
37
|
forceFullSync: false,
|
|
39
38
|
batchSize: 50,
|
|
40
39
|
conflictStrategy: .lastWriteWins,
|
|
41
40
|
backgroundMode: true,
|
|
42
41
|
timeout: 30.0,
|
|
43
|
-
encryptSensitiveData: true
|
|
44
|
-
)
|
|
45
|
-
)
|
|
42
|
+
encryptSensitiveData: true))
|
|
46
43
|
|
|
47
44
|
let syncEngine = iCloudSyncEngine()
|
|
48
45
|
let syncManager = BackgroundSyncManager(syncEngine: syncEngine, config: config)
|
|
@@ -59,15 +56,14 @@ func syncUsageDataExample() async {
|
|
|
59
56
|
let usageRecord = UsageSnapshotSyncRecord(
|
|
60
57
|
providerID: "claude",
|
|
61
58
|
primaryUsed: 75000,
|
|
62
|
-
primaryLimit:
|
|
59
|
+
primaryLimit: 100_000,
|
|
63
60
|
secondaryUsed: 50,
|
|
64
61
|
secondaryLimit: 100,
|
|
65
62
|
costUSD: 3.75,
|
|
66
63
|
accountEmail: "user@example.com",
|
|
67
64
|
updatedAt: Date(),
|
|
68
65
|
deviceName: "MacBook Pro",
|
|
69
|
-
platform: "macOS"
|
|
70
|
-
)
|
|
66
|
+
platform: "macOS")
|
|
71
67
|
|
|
72
68
|
// Queue for high-priority sync
|
|
73
69
|
await syncEngine.enqueue(usageRecord, priority: .high)
|
|
@@ -76,9 +72,9 @@ func syncUsageDataExample() async {
|
|
|
76
72
|
let result = await syncEngine.push(records: [usageRecord])
|
|
77
73
|
|
|
78
74
|
switch result {
|
|
79
|
-
case .success(
|
|
75
|
+
case let .success(recordIDs):
|
|
80
76
|
print("Successfully synced \(recordIDs.count) records")
|
|
81
|
-
case .failure(
|
|
77
|
+
case let .failure(error):
|
|
82
78
|
print("Sync failed: \(error.localizedDescription)")
|
|
83
79
|
}
|
|
84
80
|
}
|
|
@@ -95,8 +91,7 @@ func syncPreferencesExample() async {
|
|
|
95
91
|
notificationsEnabled: true,
|
|
96
92
|
autoRefreshEnabled: true,
|
|
97
93
|
theme: "dark",
|
|
98
|
-
displayFormat: "detailed"
|
|
99
|
-
)
|
|
94
|
+
displayFormat: "detailed")
|
|
100
95
|
|
|
101
96
|
// Push to CloudKit
|
|
102
97
|
let result = await syncEngine.push(records: [preferences])
|
|
@@ -104,7 +99,7 @@ func syncPreferencesExample() async {
|
|
|
104
99
|
switch result {
|
|
105
100
|
case .success:
|
|
106
101
|
print("Preferences synced successfully")
|
|
107
|
-
case .failure(
|
|
102
|
+
case let .failure(error):
|
|
108
103
|
print("Failed to sync preferences: \(error)")
|
|
109
104
|
}
|
|
110
105
|
}
|
|
@@ -117,11 +112,10 @@ func syncAlertConfigExample() async {
|
|
|
117
112
|
// Create alert configuration for Claude
|
|
118
113
|
let alertConfig = AlertConfigurationSyncRecord(
|
|
119
114
|
providerID: "claude",
|
|
120
|
-
warningThreshold: 0.75,
|
|
121
|
-
criticalThreshold: 0.90,
|
|
115
|
+
warningThreshold: 0.75, // Warn at 75%
|
|
116
|
+
criticalThreshold: 0.90, // Critical at 90%
|
|
122
117
|
notificationChannels: ["system", "email"],
|
|
123
|
-
enabled: true
|
|
124
|
-
)
|
|
118
|
+
enabled: true)
|
|
125
119
|
|
|
126
120
|
await syncEngine.enqueue(alertConfig, priority: .normal)
|
|
127
121
|
}
|
|
@@ -135,21 +129,20 @@ func manualSyncExample() async {
|
|
|
135
129
|
// Perform manual sync with custom options
|
|
136
130
|
let options = SyncOptions(
|
|
137
131
|
forceFullSync: true,
|
|
138
|
-
conflictStrategy: .lastWriteWins
|
|
139
|
-
)
|
|
132
|
+
conflictStrategy: .lastWriteWins)
|
|
140
133
|
|
|
141
134
|
let result = await syncManager.syncNow(options: options)
|
|
142
135
|
|
|
143
136
|
switch result {
|
|
144
|
-
case .success(
|
|
137
|
+
case let .success(syncResult):
|
|
145
138
|
print("""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
case .failure(
|
|
139
|
+
Sync completed:
|
|
140
|
+
- Pushed: \(syncResult.pushedCount)
|
|
141
|
+
- Fetched: \(syncResult.fetchedCount)
|
|
142
|
+
- Conflicts resolved: \(syncResult.conflictsResolved)
|
|
143
|
+
- Duration: \(syncResult.duration)s
|
|
144
|
+
""")
|
|
145
|
+
case let .failure(error):
|
|
153
146
|
print("Sync failed: \(error)")
|
|
154
147
|
}
|
|
155
148
|
}
|
|
@@ -165,7 +158,8 @@ func conflictResolutionExample() async {
|
|
|
165
158
|
// Register custom merge handler for usage snapshots
|
|
166
159
|
await resolver.registerMergeHandler(for: CloudKitRecordType.usageSnapshot) { local, remote in
|
|
167
160
|
guard let localUsage = local as? UsageSnapshotSyncRecord,
|
|
168
|
-
let remoteUsage = remote as? UsageSnapshotSyncRecord
|
|
161
|
+
let remoteUsage = remote as? UsageSnapshotSyncRecord
|
|
162
|
+
else {
|
|
169
163
|
return local
|
|
170
164
|
}
|
|
171
165
|
|
|
@@ -196,10 +190,10 @@ final class SyncProgressObserver: SyncObserver {
|
|
|
196
190
|
|
|
197
191
|
func syncDidComplete(result: SyncResult) {
|
|
198
192
|
print("""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
193
|
+
✅ Sync completed successfully:
|
|
194
|
+
- Duration: \(String(format: "%.2f", result.duration))s
|
|
195
|
+
- Records synced: \(result.pushedCount + result.fetchedCount)
|
|
196
|
+
""")
|
|
203
197
|
}
|
|
204
198
|
|
|
205
199
|
func syncDidFail(error: SyncError) {
|
|
@@ -232,15 +226,15 @@ func statisticsExample() async {
|
|
|
232
226
|
let stats = await syncManager.statistics
|
|
233
227
|
|
|
234
228
|
print("""
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
229
|
+
Sync Statistics:
|
|
230
|
+
================
|
|
231
|
+
Total syncs: \(stats.totalSyncs)
|
|
232
|
+
Successful: \(stats.successfulSyncs)
|
|
233
|
+
Failed: \(stats.failedSyncs)
|
|
234
|
+
Success rate: \(String(format: "%.1f%%", stats.successRate))
|
|
235
|
+
Average duration: \(String(format: "%.2f", stats.averageDuration))s
|
|
236
|
+
Last sync: \(stats.lastSyncDate?.description ?? "Never")
|
|
237
|
+
""")
|
|
244
238
|
|
|
245
239
|
// Check pending operations
|
|
246
240
|
let pendingCount = await syncEngine.pendingOperationCount
|
|
@@ -255,7 +249,7 @@ func errorHandlingExample() async {
|
|
|
255
249
|
let result = await syncEngine.sync(options: SyncOptions())
|
|
256
250
|
|
|
257
251
|
switch result {
|
|
258
|
-
case .success(
|
|
252
|
+
case let .success(syncResult):
|
|
259
253
|
// Check for warnings
|
|
260
254
|
if !syncResult.warnings.isEmpty {
|
|
261
255
|
print("⚠️ Sync completed with warnings:")
|
|
@@ -264,7 +258,7 @@ func errorHandlingExample() async {
|
|
|
264
258
|
}
|
|
265
259
|
}
|
|
266
260
|
|
|
267
|
-
case .failure(
|
|
261
|
+
case let .failure(error):
|
|
268
262
|
// Handle specific errors
|
|
269
263
|
switch error {
|
|
270
264
|
case .iCloudAccountUnavailable:
|
|
@@ -279,10 +273,10 @@ func errorHandlingExample() async {
|
|
|
279
273
|
case .timeout:
|
|
280
274
|
print("Sync timed out. Please try again.")
|
|
281
275
|
|
|
282
|
-
case .conflictResolutionFailed(
|
|
276
|
+
case let .conflictResolutionFailed(details):
|
|
283
277
|
print("Conflict resolution failed: \(details)")
|
|
284
278
|
|
|
285
|
-
case .cloudKitError(
|
|
279
|
+
case let .cloudKitError(ckError):
|
|
286
280
|
print("CloudKit error: \(ckError.localizedDescription)")
|
|
287
281
|
|
|
288
282
|
default:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Foundation
|
|
2
1
|
import CloudKit
|
|
3
2
|
import CryptoKit
|
|
3
|
+
import Foundation
|
|
4
4
|
import LocalAuthentication
|
|
5
5
|
|
|
6
6
|
// MARK: - CloudKit Record Type Constants
|
|
@@ -18,7 +18,7 @@ extension CloudKitRecordType {
|
|
|
18
18
|
///
|
|
19
19
|
/// Using a custom zone allows for atomic batch operations and better
|
|
20
20
|
/// change tracking for team-related records.
|
|
21
|
-
public
|
|
21
|
+
public enum TeamSyncZone {
|
|
22
22
|
public static let zoneName = "TeamCollaborationZone"
|
|
23
23
|
public static let ownerName = CKCurrentUserDefaultName
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ public struct TeamSyncZone {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
public static func createZone() -> CKRecordZone {
|
|
30
|
-
CKRecordZone(zoneID: zoneID)
|
|
30
|
+
CKRecordZone(zoneID: self.zoneID)
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -72,8 +72,8 @@ public struct TeamRecord: SyncableRecord {
|
|
|
72
72
|
updatedAt: Date = Date(),
|
|
73
73
|
description: String? = nil,
|
|
74
74
|
avatarURL: String? = nil,
|
|
75
|
-
isActive: Bool = true
|
|
76
|
-
|
|
75
|
+
isActive: Bool = true)
|
|
76
|
+
{
|
|
77
77
|
self.recordID = recordID
|
|
78
78
|
self.version = version
|
|
79
79
|
self.modifiedAt = modifiedAt
|
|
@@ -93,26 +93,25 @@ public struct TeamRecord: SyncableRecord {
|
|
|
93
93
|
let zoneID = TeamSyncZone.zoneID
|
|
94
94
|
let record = CKRecord(
|
|
95
95
|
recordType: recordType,
|
|
96
|
-
recordID: CKRecord.ID(recordName: recordID, zoneID: zoneID)
|
|
97
|
-
)
|
|
96
|
+
recordID: CKRecord.ID(recordName: self.recordID, zoneID: zoneID))
|
|
98
97
|
|
|
99
98
|
// Sync metadata
|
|
100
|
-
record["version"] = version as CKRecordValue
|
|
101
|
-
record["modifiedAt"] = modifiedAt as CKRecordValue
|
|
102
|
-
record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
|
|
99
|
+
record["version"] = self.version as CKRecordValue
|
|
100
|
+
record["modifiedAt"] = self.modifiedAt as CKRecordValue
|
|
101
|
+
record["lastModifiedDeviceID"] = self.lastModifiedDeviceID as? CKRecordValue
|
|
103
102
|
|
|
104
103
|
// Team data
|
|
105
|
-
record["teamID"] = teamID as CKRecordValue
|
|
106
|
-
record["name"] = name as CKRecordValue
|
|
107
|
-
record["ownerUserID"] = ownerUserID as CKRecordValue
|
|
108
|
-
record["totalQuota"] = totalQuota as CKRecordValue
|
|
109
|
-
record["createdAt"] = createdAt as CKRecordValue
|
|
110
|
-
record["updatedAt"] = updatedAt as CKRecordValue
|
|
111
|
-
record["isActive"] = (isActive ? 1 : 0) as CKRecordValue
|
|
104
|
+
record["teamID"] = self.teamID as CKRecordValue
|
|
105
|
+
record["name"] = self.name as CKRecordValue
|
|
106
|
+
record["ownerUserID"] = self.ownerUserID as CKRecordValue
|
|
107
|
+
record["totalQuota"] = self.totalQuota as CKRecordValue
|
|
108
|
+
record["createdAt"] = self.createdAt as CKRecordValue
|
|
109
|
+
record["updatedAt"] = self.updatedAt as CKRecordValue
|
|
110
|
+
record["isActive"] = (self.isActive ? 1 : 0) as CKRecordValue
|
|
112
111
|
|
|
113
112
|
// Optional fields
|
|
114
|
-
record["description"] = description as? CKRecordValue
|
|
115
|
-
record["avatarURL"] = avatarURL as? CKRecordValue
|
|
113
|
+
record["description"] = self.description as? CKRecordValue
|
|
114
|
+
record["avatarURL"] = self.avatarURL as? CKRecordValue
|
|
116
115
|
|
|
117
116
|
return record
|
|
118
117
|
}
|
|
@@ -144,8 +143,7 @@ public struct TeamRecord: SyncableRecord {
|
|
|
144
143
|
updatedAt: updatedAt,
|
|
145
144
|
description: ckRecord["description"] as? String,
|
|
146
145
|
avatarURL: ckRecord["avatarURL"] as? String,
|
|
147
|
-
isActive: isActiveInt == 1
|
|
148
|
-
)
|
|
146
|
+
isActive: isActiveInt == 1)
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
@@ -180,14 +178,14 @@ public struct TeamMembershipRecord: SyncableRecord {
|
|
|
180
178
|
public let acceptedAt: Date?
|
|
181
179
|
public let isActive: Bool
|
|
182
180
|
|
|
183
|
-
|
|
181
|
+
/// Reference to parent team
|
|
184
182
|
public let teamRecordName: String
|
|
185
183
|
|
|
186
184
|
public enum TeamRole: String, Codable, Sendable {
|
|
187
|
-
case owner
|
|
188
|
-
case admin
|
|
189
|
-
case member
|
|
190
|
-
case viewer
|
|
185
|
+
case owner
|
|
186
|
+
case admin
|
|
187
|
+
case member
|
|
188
|
+
case viewer
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
public init(
|
|
@@ -206,8 +204,8 @@ public struct TeamMembershipRecord: SyncableRecord {
|
|
|
206
204
|
invitedBy: String? = nil,
|
|
207
205
|
acceptedAt: Date? = nil,
|
|
208
206
|
isActive: Bool = true,
|
|
209
|
-
teamRecordName: String
|
|
210
|
-
|
|
207
|
+
teamRecordName: String)
|
|
208
|
+
{
|
|
211
209
|
self.recordID = recordID
|
|
212
210
|
self.version = version
|
|
213
211
|
self.modifiedAt = modifiedAt
|
|
@@ -230,37 +228,35 @@ public struct TeamMembershipRecord: SyncableRecord {
|
|
|
230
228
|
let zoneID = TeamSyncZone.zoneID
|
|
231
229
|
let record = CKRecord(
|
|
232
230
|
recordType: recordType,
|
|
233
|
-
recordID: CKRecord.ID(recordName: recordID, zoneID: zoneID)
|
|
234
|
-
)
|
|
231
|
+
recordID: CKRecord.ID(recordName: self.recordID, zoneID: zoneID))
|
|
235
232
|
|
|
236
233
|
// Sync metadata
|
|
237
|
-
record["version"] = version as CKRecordValue
|
|
238
|
-
record["modifiedAt"] = modifiedAt as CKRecordValue
|
|
239
|
-
record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
|
|
234
|
+
record["version"] = self.version as CKRecordValue
|
|
235
|
+
record["modifiedAt"] = self.modifiedAt as CKRecordValue
|
|
236
|
+
record["lastModifiedDeviceID"] = self.lastModifiedDeviceID as? CKRecordValue
|
|
240
237
|
|
|
241
238
|
// Membership data
|
|
242
|
-
record["membershipID"] = membershipID as CKRecordValue
|
|
243
|
-
record["teamID"] = teamID as CKRecordValue
|
|
244
|
-
record["userID"] = userID as CKRecordValue
|
|
245
|
-
record["role"] = role.rawValue as CKRecordValue
|
|
246
|
-
record["quotaLimit"] = quotaLimit as? CKRecordValue
|
|
247
|
-
record["joinedAt"] = joinedAt as CKRecordValue
|
|
248
|
-
record["isActive"] = (isActive ? 1 : 0) as CKRecordValue
|
|
239
|
+
record["membershipID"] = self.membershipID as CKRecordValue
|
|
240
|
+
record["teamID"] = self.teamID as CKRecordValue
|
|
241
|
+
record["userID"] = self.userID as CKRecordValue
|
|
242
|
+
record["role"] = self.role.rawValue as CKRecordValue
|
|
243
|
+
record["quotaLimit"] = self.quotaLimit as? CKRecordValue
|
|
244
|
+
record["joinedAt"] = self.joinedAt as CKRecordValue
|
|
245
|
+
record["isActive"] = (self.isActive ? 1 : 0) as CKRecordValue
|
|
249
246
|
|
|
250
247
|
// Optional encrypted fields
|
|
251
|
-
if let email
|
|
248
|
+
if let email {
|
|
252
249
|
let encrypted = try encryptString(email)
|
|
253
250
|
record["email"] = encrypted as CKRecordValue
|
|
254
251
|
}
|
|
255
|
-
record["displayName"] = displayName as? CKRecordValue
|
|
256
|
-
record["invitedBy"] = invitedBy as? CKRecordValue
|
|
257
|
-
record["acceptedAt"] = acceptedAt as? CKRecordValue
|
|
252
|
+
record["displayName"] = self.displayName as? CKRecordValue
|
|
253
|
+
record["invitedBy"] = self.invitedBy as? CKRecordValue
|
|
254
|
+
record["acceptedAt"] = self.acceptedAt as? CKRecordValue
|
|
258
255
|
|
|
259
256
|
// Reference to parent team
|
|
260
257
|
let teamReference = CKRecord.Reference(
|
|
261
|
-
recordID: CKRecord.ID(recordName: teamRecordName, zoneID: zoneID),
|
|
262
|
-
action: .deleteSelf
|
|
263
|
-
)
|
|
258
|
+
recordID: CKRecord.ID(recordName: self.teamRecordName, zoneID: zoneID),
|
|
259
|
+
action: .deleteSelf)
|
|
264
260
|
record["teamReference"] = teamReference as CKRecordValue
|
|
265
261
|
|
|
266
262
|
return record
|
|
@@ -302,8 +298,7 @@ public struct TeamMembershipRecord: SyncableRecord {
|
|
|
302
298
|
invitedBy: ckRecord["invitedBy"] as? String,
|
|
303
299
|
acceptedAt: ckRecord["acceptedAt"] as? Date,
|
|
304
300
|
isActive: isActiveInt == 1,
|
|
305
|
-
teamRecordName: teamReference.recordID.recordName
|
|
306
|
-
)
|
|
301
|
+
teamRecordName: teamReference.recordID.recordName)
|
|
307
302
|
}
|
|
308
303
|
}
|
|
309
304
|
|
|
@@ -337,14 +332,14 @@ public struct ProjectOwnershipRecord: SyncableRecord {
|
|
|
337
332
|
public let tags: [String]
|
|
338
333
|
public let isArchived: Bool
|
|
339
334
|
|
|
340
|
-
|
|
335
|
+
/// Reference to team (if team-owned)
|
|
341
336
|
public let teamRecordName: String?
|
|
342
337
|
|
|
343
338
|
public enum AccessLevel: String, Codable, Sendable {
|
|
344
|
-
case `private`
|
|
339
|
+
case `private`
|
|
345
340
|
case teamReadOnly = "team_read_only"
|
|
346
341
|
case teamReadWrite = "team_read_write"
|
|
347
|
-
case `public`
|
|
342
|
+
case `public`
|
|
348
343
|
}
|
|
349
344
|
|
|
350
345
|
public init(
|
|
@@ -362,8 +357,8 @@ public struct ProjectOwnershipRecord: SyncableRecord {
|
|
|
362
357
|
projectDescription: String? = nil,
|
|
363
358
|
tags: [String] = [],
|
|
364
359
|
isArchived: Bool = false,
|
|
365
|
-
teamRecordName: String? = nil
|
|
366
|
-
|
|
360
|
+
teamRecordName: String? = nil)
|
|
361
|
+
{
|
|
367
362
|
self.recordID = recordID
|
|
368
363
|
self.version = version
|
|
369
364
|
self.modifiedAt = modifiedAt
|
|
@@ -385,34 +380,32 @@ public struct ProjectOwnershipRecord: SyncableRecord {
|
|
|
385
380
|
let zoneID = TeamSyncZone.zoneID
|
|
386
381
|
let record = CKRecord(
|
|
387
382
|
recordType: recordType,
|
|
388
|
-
recordID: CKRecord.ID(recordName: recordID, zoneID: zoneID)
|
|
389
|
-
)
|
|
383
|
+
recordID: CKRecord.ID(recordName: self.recordID, zoneID: zoneID))
|
|
390
384
|
|
|
391
385
|
// Sync metadata
|
|
392
|
-
record["version"] = version as CKRecordValue
|
|
393
|
-
record["modifiedAt"] = modifiedAt as CKRecordValue
|
|
394
|
-
record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
|
|
386
|
+
record["version"] = self.version as CKRecordValue
|
|
387
|
+
record["modifiedAt"] = self.modifiedAt as CKRecordValue
|
|
388
|
+
record["lastModifiedDeviceID"] = self.lastModifiedDeviceID as? CKRecordValue
|
|
395
389
|
|
|
396
390
|
// Project ownership data
|
|
397
|
-
record["projectID"] = projectID as CKRecordValue
|
|
398
|
-
record["teamID"] = teamID as? CKRecordValue
|
|
399
|
-
record["ownerUserID"] = ownerUserID as CKRecordValue
|
|
400
|
-
record["sharedWith"] = sharedWith as CKRecordValue
|
|
401
|
-
record["accessLevel"] = accessLevel.rawValue as CKRecordValue
|
|
402
|
-
record["createdAt"] = createdAt as CKRecordValue
|
|
403
|
-
record["isArchived"] = (isArchived ? 1 : 0) as CKRecordValue
|
|
391
|
+
record["projectID"] = self.projectID as CKRecordValue
|
|
392
|
+
record["teamID"] = self.teamID as? CKRecordValue
|
|
393
|
+
record["ownerUserID"] = self.ownerUserID as CKRecordValue
|
|
394
|
+
record["sharedWith"] = self.sharedWith as CKRecordValue
|
|
395
|
+
record["accessLevel"] = self.accessLevel.rawValue as CKRecordValue
|
|
396
|
+
record["createdAt"] = self.createdAt as CKRecordValue
|
|
397
|
+
record["isArchived"] = (self.isArchived ? 1 : 0) as CKRecordValue
|
|
404
398
|
|
|
405
399
|
// Optional metadata
|
|
406
|
-
record["projectName"] = projectName as? CKRecordValue
|
|
407
|
-
record["projectDescription"] = projectDescription as? CKRecordValue
|
|
408
|
-
record["tags"] = tags as CKRecordValue
|
|
400
|
+
record["projectName"] = self.projectName as? CKRecordValue
|
|
401
|
+
record["projectDescription"] = self.projectDescription as? CKRecordValue
|
|
402
|
+
record["tags"] = self.tags as CKRecordValue
|
|
409
403
|
|
|
410
404
|
// Reference to team (if team-owned)
|
|
411
|
-
if let teamRecordName
|
|
405
|
+
if let teamRecordName {
|
|
412
406
|
let teamReference = CKRecord.Reference(
|
|
413
407
|
recordID: CKRecord.ID(recordName: teamRecordName, zoneID: zoneID),
|
|
414
|
-
action: .none
|
|
415
|
-
)
|
|
408
|
+
action: .none)
|
|
416
409
|
record["teamReference"] = teamReference as CKRecordValue
|
|
417
410
|
}
|
|
418
411
|
|
|
@@ -450,8 +443,7 @@ public struct ProjectOwnershipRecord: SyncableRecord {
|
|
|
450
443
|
projectDescription: ckRecord["projectDescription"] as? String,
|
|
451
444
|
tags: ckRecord["tags"] as? [String] ?? [],
|
|
452
445
|
isArchived: isArchivedInt == 1,
|
|
453
|
-
teamRecordName: teamReference?.recordID.recordName
|
|
454
|
-
)
|
|
446
|
+
teamRecordName: teamReference?.recordID.recordName)
|
|
455
447
|
}
|
|
456
448
|
}
|
|
457
449
|
|
|
@@ -484,7 +476,7 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
484
476
|
public let mostActiveUser: String?
|
|
485
477
|
public let averageTokensPerMember: Double
|
|
486
478
|
|
|
487
|
-
|
|
479
|
+
/// Reference to parent team
|
|
488
480
|
public let teamRecordName: String
|
|
489
481
|
|
|
490
482
|
public struct MemberUsageData: Codable, Sendable, Hashable {
|
|
@@ -515,8 +507,8 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
515
507
|
primaryProvider: String? = nil,
|
|
516
508
|
mostActiveUser: String? = nil,
|
|
517
509
|
averageTokensPerMember: Double = 0.0,
|
|
518
|
-
teamRecordName: String
|
|
519
|
-
|
|
510
|
+
teamRecordName: String)
|
|
511
|
+
{
|
|
520
512
|
self.recordID = recordID
|
|
521
513
|
self.version = version
|
|
522
514
|
self.modifiedAt = modifiedAt
|
|
@@ -537,36 +529,34 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
537
529
|
let zoneID = TeamSyncZone.zoneID
|
|
538
530
|
let record = CKRecord(
|
|
539
531
|
recordType: recordType,
|
|
540
|
-
recordID: CKRecord.ID(recordName: recordID, zoneID: zoneID)
|
|
541
|
-
)
|
|
532
|
+
recordID: CKRecord.ID(recordName: self.recordID, zoneID: zoneID))
|
|
542
533
|
|
|
543
534
|
// Sync metadata
|
|
544
|
-
record["version"] = version as CKRecordValue
|
|
545
|
-
record["modifiedAt"] = modifiedAt as CKRecordValue
|
|
546
|
-
record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
|
|
535
|
+
record["version"] = self.version as CKRecordValue
|
|
536
|
+
record["modifiedAt"] = self.modifiedAt as CKRecordValue
|
|
537
|
+
record["lastModifiedDeviceID"] = self.lastModifiedDeviceID as? CKRecordValue
|
|
547
538
|
|
|
548
539
|
// Team usage data
|
|
549
|
-
record["teamID"] = teamID as CKRecordValue
|
|
550
|
-
record["date"] = date as CKRecordValue
|
|
551
|
-
record["totalTokens"] = totalTokens as CKRecordValue
|
|
552
|
-
record["totalCost"] = totalCost as CKRecordValue
|
|
553
|
-
record["quotaUsedPercent"] = quotaUsedPercent as CKRecordValue
|
|
540
|
+
record["teamID"] = self.teamID as CKRecordValue
|
|
541
|
+
record["date"] = self.date as CKRecordValue
|
|
542
|
+
record["totalTokens"] = self.totalTokens as CKRecordValue
|
|
543
|
+
record["totalCost"] = self.totalCost as CKRecordValue
|
|
544
|
+
record["quotaUsedPercent"] = self.quotaUsedPercent as CKRecordValue
|
|
554
545
|
|
|
555
546
|
// Encode member usage as JSON
|
|
556
547
|
let encoder = JSONEncoder()
|
|
557
|
-
let memberUsageData = try encoder.encode(memberUsage)
|
|
548
|
+
let memberUsageData = try encoder.encode(self.memberUsage)
|
|
558
549
|
record["memberUsage"] = String(data: memberUsageData, encoding: .utf8) as? CKRecordValue
|
|
559
550
|
|
|
560
551
|
// Optional metrics
|
|
561
|
-
record["primaryProvider"] = primaryProvider as? CKRecordValue
|
|
562
|
-
record["mostActiveUser"] = mostActiveUser as? CKRecordValue
|
|
563
|
-
record["averageTokensPerMember"] = averageTokensPerMember as CKRecordValue
|
|
552
|
+
record["primaryProvider"] = self.primaryProvider as? CKRecordValue
|
|
553
|
+
record["mostActiveUser"] = self.mostActiveUser as? CKRecordValue
|
|
554
|
+
record["averageTokensPerMember"] = self.averageTokensPerMember as CKRecordValue
|
|
564
555
|
|
|
565
556
|
// Reference to parent team
|
|
566
557
|
let teamReference = CKRecord.Reference(
|
|
567
|
-
recordID: CKRecord.ID(recordName: teamRecordName, zoneID: zoneID),
|
|
568
|
-
action: .deleteSelf
|
|
569
|
-
)
|
|
558
|
+
recordID: CKRecord.ID(recordName: self.teamRecordName, zoneID: zoneID),
|
|
559
|
+
action: .deleteSelf)
|
|
570
560
|
record["teamReference"] = teamReference as CKRecordValue
|
|
571
561
|
|
|
572
562
|
return record
|
|
@@ -588,7 +578,8 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
588
578
|
// Decode member usage from JSON
|
|
589
579
|
var memberUsage: [String: MemberUsageData] = [:]
|
|
590
580
|
if let memberUsageJSON = ckRecord["memberUsage"] as? String,
|
|
591
|
-
let data = memberUsageJSON.data(using: .utf8)
|
|
581
|
+
let data = memberUsageJSON.data(using: .utf8)
|
|
582
|
+
{
|
|
592
583
|
let decoder = JSONDecoder()
|
|
593
584
|
memberUsage = (try? decoder.decode([String: MemberUsageData].self, from: data)) ?? [:]
|
|
594
585
|
}
|
|
@@ -607,8 +598,7 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
607
598
|
primaryProvider: ckRecord["primaryProvider"] as? String,
|
|
608
599
|
mostActiveUser: ckRecord["mostActiveUser"] as? String,
|
|
609
600
|
averageTokensPerMember: ckRecord["averageTokensPerMember"] as? Double ?? 0.0,
|
|
610
|
-
teamRecordName: teamReference.recordID.recordName
|
|
611
|
-
)
|
|
601
|
+
teamRecordName: teamReference.recordID.recordName)
|
|
612
602
|
}
|
|
613
603
|
}
|
|
614
604
|
|
|
@@ -616,7 +606,6 @@ public struct TeamUsageSnapshotRecord: SyncableRecord {
|
|
|
616
606
|
|
|
617
607
|
/// Helper methods for querying team-related records
|
|
618
608
|
public enum TeamRecordQuery {
|
|
619
|
-
|
|
620
609
|
/// Creates a predicate to query teams by owner
|
|
621
610
|
public static func teamsByOwner(_ ownerUserID: String) -> NSPredicate {
|
|
622
611
|
NSPredicate(format: "ownerUserID == %@", ownerUserID)
|
|
@@ -644,7 +633,11 @@ public enum TeamRecordQuery {
|
|
|
644
633
|
|
|
645
634
|
/// Creates a predicate to query team usage snapshots by date range
|
|
646
635
|
public static func teamUsageByDateRange(teamID: String, startDate: Date, endDate: Date) -> NSPredicate {
|
|
647
|
-
NSPredicate(
|
|
636
|
+
NSPredicate(
|
|
637
|
+
format: "teamID == %@ AND date >= %@ AND date <= %@",
|
|
638
|
+
teamID,
|
|
639
|
+
startDate as NSDate,
|
|
640
|
+
endDate as NSDate)
|
|
648
641
|
}
|
|
649
642
|
|
|
650
643
|
/// Creates a predicate to query latest team usage
|
|
@@ -748,7 +741,7 @@ private func saveToKeychain(key: String, data: Data) {
|
|
|
748
741
|
kSecAttrAccount as String: key,
|
|
749
742
|
kSecUseDataProtectionKeychain as String: true,
|
|
750
743
|
kSecValueData as String: data,
|
|
751
|
-
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
|
744
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
752
745
|
]
|
|
753
746
|
SecItemDelete(query as CFDictionary)
|
|
754
747
|
SecItemAdd(query as CFDictionary, nil)
|
|
@@ -761,7 +754,7 @@ private func loadFromKeychain(key: String) -> Data? {
|
|
|
761
754
|
kSecAttrAccount as String: key,
|
|
762
755
|
kSecUseDataProtectionKeychain as String: true,
|
|
763
756
|
kSecUseAuthenticationUI as String: "kSecUseAuthenticationUIFail" as CFString,
|
|
764
|
-
kSecReturnData as String: true
|
|
757
|
+
kSecReturnData as String: true,
|
|
765
758
|
]
|
|
766
759
|
let authContext = LAContext()
|
|
767
760
|
authContext.interactionNotAllowed = true
|