@sriinnu/runic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (584) hide show
  1. package/.swiftformat +50 -0
  2. package/.swiftlint.yml +160 -0
  3. package/CHANGELOG.md +307 -0
  4. package/CONTRIBUTING.md +456 -0
  5. package/Icon.icns +0 -0
  6. package/Icon.icon/Assets/runic.png +0 -0
  7. package/Icon.icon/icon.json +36 -0
  8. package/LICENSE +21 -0
  9. package/Package.resolved +51 -0
  10. package/Package.swift +136 -0
  11. package/Packages/Helix/LICENSE +21 -0
  12. package/Packages/Helix/Package.swift +29 -0
  13. package/Packages/Helix/README.md +976 -0
  14. package/Packages/Helix/Sources/Helix/Binding.swift +111 -0
  15. package/Packages/Helix/Sources/Helix/CommandParser.swift +160 -0
  16. package/Packages/Helix/Sources/Helix/CommandSignature.swift +117 -0
  17. package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +72 -0
  18. package/Packages/Helix/Sources/Helix/HelpGenerator.swift +175 -0
  19. package/Packages/Helix/Sources/Helix/NameSpecification.swift +98 -0
  20. package/Packages/Helix/Sources/Helix/ParsableCommand.swift +204 -0
  21. package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +44 -0
  22. package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +34 -0
  23. package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +91 -0
  24. package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +94 -0
  25. package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +167 -0
  26. package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +99 -0
  27. package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +146 -0
  28. package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +97 -0
  29. package/Packages/Helix/Sources/Helix/Program.swift +204 -0
  30. package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +367 -0
  31. package/Packages/Silo/LICENSE +21 -0
  32. package/Packages/Silo/Package.swift +34 -0
  33. package/Packages/Silo/README.md +466 -0
  34. package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +169 -0
  35. package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +365 -0
  36. package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +594 -0
  37. package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +326 -0
  38. package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +154 -0
  39. package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +119 -0
  40. package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +121 -0
  41. package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +391 -0
  42. package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +240 -0
  43. package/Packages/Silo/Sources/Silo/CryptoSupport.swift +86 -0
  44. package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +172 -0
  45. package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +172 -0
  46. package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +105 -0
  47. package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +191 -0
  48. package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +41 -0
  49. package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +110 -0
  50. package/README.md +109 -0
  51. package/RuniciOS/App/RuniciOSApp.swift +105 -0
  52. package/RuniciOS/Core/iOSUsageStore.swift +192 -0
  53. package/RuniciOS/Views/ProviderDetailView.swift +312 -0
  54. package/RuniciOS/Views/ProviderListView.swift +228 -0
  55. package/RuniciOS/Widgets/LargeWidget.swift +367 -0
  56. package/RuniciOS/Widgets/LiveActivity.swift +342 -0
  57. package/RuniciOS/Widgets/LockScreenWidget.swift +273 -0
  58. package/RuniciOS/Widgets/MediumWidget.swift +370 -0
  59. package/RuniciOS/Widgets/RunicWidget.swift +274 -0
  60. package/RuniciOS/Widgets/SmallWidget.swift +281 -0
  61. package/RuniciOS/Widgets/WidgetModels.swift +285 -0
  62. package/RuniciOS/Widgets/WidgetTimeline.swift +270 -0
  63. package/Scripts/CREATION-SUMMARY.md +367 -0
  64. package/Scripts/DIRECTORY-STRUCTURE.md +358 -0
  65. package/Scripts/GETTING-STARTED-CHECKLIST.md +364 -0
  66. package/Scripts/INDEX.md +365 -0
  67. package/Scripts/QUICK-REFERENCE.md +164 -0
  68. package/Scripts/README-SCRIPTS.md +926 -0
  69. package/Scripts/build-android.sh +526 -0
  70. package/Scripts/build-ios.sh +529 -0
  71. package/Scripts/build-macos.sh +603 -0
  72. package/Scripts/build-windows.sh +504 -0
  73. package/Scripts/build_icon.sh +49 -0
  74. package/Scripts/changelog-to-html.sh +89 -0
  75. package/Scripts/check-release-assets.sh +10 -0
  76. package/Scripts/ci-config.yml +474 -0
  77. package/Scripts/compile_and_run.sh +219 -0
  78. package/Scripts/compile_and_run_adhoc.sh +4 -0
  79. package/Scripts/docs-list.mjs +119 -0
  80. package/Scripts/make_appcast.sh +58 -0
  81. package/Scripts/package_app.sh +409 -0
  82. package/Scripts/release-lib.sh +214 -0
  83. package/Scripts/release.sh +76 -0
  84. package/Scripts/setup-all.sh +387 -0
  85. package/Scripts/setup-ios.sh +404 -0
  86. package/Scripts/setup-react-native.sh +523 -0
  87. package/Scripts/sign-and-notarize.sh +122 -0
  88. package/Scripts/test_live_update.sh +38 -0
  89. package/Scripts/validate_changelog.sh +23 -0
  90. package/Scripts/verify_appcast.sh +92 -0
  91. package/Sources/Runic/About.swift +93 -0
  92. package/Sources/Runic/AppNotifications.swift +115 -0
  93. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +657 -0
  94. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +518 -0
  95. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +810 -0
  96. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +87 -0
  97. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +1379 -0
  98. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +337 -0
  99. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +395 -0
  100. package/Sources/Runic/Controllers/StatusItemController.swift +360 -0
  101. package/Sources/Runic/Core/BudgetNotificationManager.swift +80 -0
  102. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +103 -0
  103. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +154 -0
  104. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +206 -0
  105. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +95 -0
  106. package/Sources/Runic/Core/Rendering/IconRenderer.swift +1216 -0
  107. package/Sources/Runic/Core/Rendering/IconView.swift +120 -0
  108. package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +71 -0
  109. package/Sources/Runic/Core/RunicAnimation.swift +53 -0
  110. package/Sources/Runic/Core/RunicColors.swift +73 -0
  111. package/Sources/Runic/Core/RunicCornerRadius.swift +15 -0
  112. package/Sources/Runic/Core/RunicSpacing.swift +68 -0
  113. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +143 -0
  114. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +143 -0
  115. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +143 -0
  116. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +143 -0
  117. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +143 -0
  118. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +158 -0
  119. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +143 -0
  120. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +143 -0
  121. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +143 -0
  122. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +143 -0
  123. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +143 -0
  124. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +143 -0
  125. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +143 -0
  126. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +143 -0
  127. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +143 -0
  128. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +143 -0
  129. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +30 -0
  130. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +143 -0
  131. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +143 -0
  132. package/Sources/Runic/Core/Stores/SettingsStore.swift +1702 -0
  133. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +143 -0
  134. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +20 -0
  135. package/Sources/Runic/Core/Stores/UsageStore+Status.swift +189 -0
  136. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +33 -0
  137. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +67 -0
  138. package/Sources/Runic/Core/Stores/UsageStore.swift +3564 -0
  139. package/Sources/Runic/Core/Stores/XAITokenStore.swift +143 -0
  140. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +143 -0
  141. package/Sources/Runic/Core/UsageExporter.swift +193 -0
  142. package/Sources/Runic/Date+RelativeDescription.swift +21 -0
  143. package/Sources/Runic/HiddenWindowView.swift +32 -0
  144. package/Sources/Runic/InstallOrigin.swift +9 -0
  145. package/Sources/Runic/KeyboardShortcuts+Names.swift +6 -0
  146. package/Sources/Runic/MouseLocationReader.swift +65 -0
  147. package/Sources/Runic/Notifications+Runic.swift +6 -0
  148. package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +503 -0
  149. package/Sources/Runic/ProviderRegistry.swift +100 -0
  150. package/Sources/Runic/ProviderSwitcherButtons.swift +267 -0
  151. package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +11 -0
  152. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +14 -0
  153. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +24 -0
  154. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +52 -0
  155. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +42 -0
  156. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +24 -0
  157. package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +26 -0
  158. package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +14 -0
  159. package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +18 -0
  160. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +15 -0
  161. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +24 -0
  162. package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +193 -0
  163. package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +110 -0
  164. package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +28 -0
  165. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +15 -0
  166. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +24 -0
  167. package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +12 -0
  168. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +15 -0
  169. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +24 -0
  170. package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +20 -0
  171. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +15 -0
  172. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +24 -0
  173. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +24 -0
  174. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +32 -0
  175. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +24 -0
  176. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +24 -0
  177. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +24 -0
  178. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +24 -0
  179. package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +15 -0
  180. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +48 -0
  181. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +66 -0
  182. package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +12 -0
  183. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +99 -0
  184. package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +18 -0
  185. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +24 -0
  186. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +24 -0
  187. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +24 -0
  188. package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +2 -0
  189. package/Sources/Runic/Resources/ProviderIcon-auggie.svg +3 -0
  190. package/Sources/Runic/Resources/ProviderIcon-azure.svg +5 -0
  191. package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +7 -0
  192. package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +3 -0
  193. package/Sources/Runic/Resources/ProviderIcon-claude.svg +1 -0
  194. package/Sources/Runic/Resources/ProviderIcon-codex.svg +1 -0
  195. package/Sources/Runic/Resources/ProviderIcon-cohere.svg +3 -0
  196. package/Sources/Runic/Resources/ProviderIcon-copilot.svg +1 -0
  197. package/Sources/Runic/Resources/ProviderIcon-cursor.svg +3 -0
  198. package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +4 -0
  199. package/Sources/Runic/Resources/ProviderIcon-factory.svg +3 -0
  200. package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +3 -0
  201. package/Sources/Runic/Resources/ProviderIcon-gemini.svg +1 -0
  202. package/Sources/Runic/Resources/ProviderIcon-groq.svg +1 -0
  203. package/Sources/Runic/Resources/ProviderIcon-kimi.svg +4 -0
  204. package/Sources/Runic/Resources/ProviderIcon-minimax.svg +1 -0
  205. package/Sources/Runic/Resources/ProviderIcon-mistral.svg +5 -0
  206. package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +7 -0
  207. package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +4 -0
  208. package/Sources/Runic/Resources/ProviderIcon-qwen.svg +9 -0
  209. package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +4 -0
  210. package/Sources/Runic/Resources/ProviderIcon-together.svg +5 -0
  211. package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +17 -0
  212. package/Sources/Runic/Resources/ProviderIcon-xai.svg +3 -0
  213. package/Sources/Runic/Resources/ProviderIcon-zai.svg +5 -0
  214. package/Sources/Runic/Resources/RunicMenubarIcon.svg +7 -0
  215. package/Sources/Runic/RunicApp.swift +357 -0
  216. package/Sources/Runic/SessionQuotaNotifications.swift +58 -0
  217. package/Sources/Runic/UsagePaceText.swift +52 -0
  218. package/Sources/Runic/UsageProgressBar.swift +224 -0
  219. package/Sources/Runic/Utilities/DisplayLink.swift +96 -0
  220. package/Sources/Runic/Utilities/LoadingPattern.swift +60 -0
  221. package/Sources/Runic/Utilities/PerformanceConstants.swift +85 -0
  222. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +256 -0
  223. package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +22 -0
  224. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +275 -0
  225. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +377 -0
  226. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +312 -0
  227. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +312 -0
  228. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +228 -0
  229. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +228 -0
  230. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +162 -0
  231. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +64 -0
  232. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +268 -0
  233. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +104 -0
  234. package/Sources/Runic/Views/Menu/MenuCardView.swift +1900 -0
  235. package/Sources/Runic/Views/Menu/MenuContent.swift +150 -0
  236. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +369 -0
  237. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +42 -0
  238. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +235 -0
  239. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +141 -0
  240. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +209 -0
  241. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +220 -0
  242. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +88 -0
  243. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +201 -0
  244. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +398 -0
  245. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +232 -0
  246. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +417 -0
  247. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +190 -0
  248. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +132 -0
  249. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +618 -0
  250. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +305 -0
  251. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +271 -0
  252. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +268 -0
  253. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +422 -0
  254. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +641 -0
  255. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +344 -0
  256. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +230 -0
  257. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +322 -0
  258. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +469 -0
  259. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +231 -0
  260. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +50 -0
  261. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +497 -0
  262. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +288 -0
  263. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +3435 -0
  264. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +25 -0
  265. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +59 -0
  266. package/Sources/Runic/Views/Preferences/PreferencesView.swift +206 -0
  267. package/Sources/Runic/Views/Rating/QualityRatingView.swift +238 -0
  268. package/Sources/Runic/Views/Team/TeamDashboardView.swift +371 -0
  269. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +175 -0
  270. package/Sources/Runic/Views/Team/TeamManagementView.swift +761 -0
  271. package/Sources/Runic/Views/Team/TeamMemberRow.swift +181 -0
  272. package/Sources/RunicCLI/CLIEntry.swift +942 -0
  273. package/Sources/RunicCLI/CLIRenderer.swift +87 -0
  274. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +588 -0
  275. package/Sources/RunicCLI/Commands/ModelsCommand.swift +362 -0
  276. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +439 -0
  277. package/Sources/RunicCLI/Commands/ResetCommand.swift +429 -0
  278. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +168 -0
  279. package/Sources/RunicClaudeWatchdog/main.swift +122 -0
  280. package/Sources/RunicClaudeWebProbe/main.swift +62 -0
  281. package/Sources/RunicCore/AlertRuleStore.swift +187 -0
  282. package/Sources/RunicCore/BrowserCookieImportOrder.swift +25 -0
  283. package/Sources/RunicCore/CopilotUsageModels.swift +84 -0
  284. package/Sources/RunicCore/CostUsageFetcher.swift +119 -0
  285. package/Sources/RunicCore/CostUsageModels.swift +479 -0
  286. package/Sources/RunicCore/CreditsModels.swift +50 -0
  287. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +269 -0
  288. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +622 -0
  289. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +500 -0
  290. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +679 -0
  291. package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +135 -0
  292. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +173 -0
  293. package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +79 -0
  294. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +86 -0
  295. package/Sources/RunicCore/Logging/RunicLog.swift +123 -0
  296. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +371 -0
  297. package/Sources/RunicCore/OpenAIDashboardModels.swift +210 -0
  298. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +676 -0
  299. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +492 -0
  300. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +51 -0
  301. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +483 -0
  302. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +558 -0
  303. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +204 -0
  304. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +78 -0
  305. package/Sources/RunicCore/PathEnvironment.swift +482 -0
  306. package/Sources/RunicCore/Performance/PerformanceModels.swift +371 -0
  307. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +409 -0
  308. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +178 -0
  309. package/Sources/RunicCore/ProjectBudgetStore.swift +99 -0
  310. package/Sources/RunicCore/ProviderCostSnapshot.swift +29 -0
  311. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +66 -0
  312. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +685 -0
  313. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +83 -0
  314. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +142 -0
  315. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +141 -0
  316. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +186 -0
  317. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +126 -0
  318. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +168 -0
  319. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +84 -0
  320. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +75 -0
  321. package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +332 -0
  322. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +180 -0
  323. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +137 -0
  324. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +207 -0
  325. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +654 -0
  326. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +28 -0
  327. package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +480 -0
  328. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +780 -0
  329. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +146 -0
  330. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +269 -0
  331. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +100 -0
  332. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +243 -0
  333. package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +125 -0
  334. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +24 -0
  335. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +181 -0
  336. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +83 -0
  337. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +85 -0
  338. package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +123 -0
  339. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +114 -0
  340. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +100 -0
  341. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +291 -0
  342. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +386 -0
  343. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +65 -0
  344. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +594 -0
  345. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +85 -0
  346. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +108 -0
  347. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +398 -0
  348. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +65 -0
  349. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1275 -0
  350. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +84 -0
  351. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +75 -0
  352. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +65 -0
  353. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +835 -0
  354. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +83 -0
  355. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +71 -0
  356. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +83 -0
  357. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +77 -0
  358. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +92 -0
  359. package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +54 -0
  360. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +171 -0
  361. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +911 -0
  362. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +84 -0
  363. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +75 -0
  364. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +87 -0
  365. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +207 -0
  366. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +84 -0
  367. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +75 -0
  368. package/Sources/RunicCore/Providers/ProviderBranding.swift +25 -0
  369. package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +17 -0
  370. package/Sources/RunicCore/Providers/ProviderDescriptor.swift +129 -0
  371. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +263 -0
  372. package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +98 -0
  373. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +501 -0
  374. package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +68 -0
  375. package/Sources/RunicCore/Providers/Providers.swift +184 -0
  376. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +84 -0
  377. package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +30 -0
  378. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +317 -0
  379. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +84 -0
  380. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +75 -0
  381. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +83 -0
  382. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +75 -0
  383. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +123 -0
  384. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +48 -0
  385. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +144 -0
  386. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +83 -0
  387. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +75 -0
  388. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +72 -0
  389. package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +41 -0
  390. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +724 -0
  391. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +385 -0
  392. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +310 -0
  393. package/Sources/RunicCore/Sync/SyncProtocol.swift +288 -0
  394. package/Sources/RunicCore/Sync/SyncRecord.swift +405 -0
  395. package/Sources/RunicCore/Sync/SyncUsageExample.swift +294 -0
  396. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +775 -0
  397. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +333 -0
  398. package/Sources/RunicCore/Team/TeamModels.swift +583 -0
  399. package/Sources/RunicCore/Team/TeamShowbackExporter.swift +495 -0
  400. package/Sources/RunicCore/Team/TeamStore.swift +792 -0
  401. package/Sources/RunicCore/TextParsing.swift +55 -0
  402. package/Sources/RunicCore/UsageFetcher.swift +694 -0
  403. package/Sources/RunicCore/UsageFormatter.swift +410 -0
  404. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +329 -0
  405. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +448 -0
  406. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +550 -0
  407. package/Sources/RunicCore/UsageLedger/UsageLedger.swift +29 -0
  408. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +660 -0
  409. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +209 -0
  410. package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +417 -0
  411. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +245 -0
  412. package/Sources/RunicCore/UsagePace.swift +81 -0
  413. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +73 -0
  414. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +74 -0
  415. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +208 -0
  416. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +455 -0
  417. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +132 -0
  418. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +589 -0
  419. package/Sources/RunicCore/WidgetSnapshot.swift +183 -0
  420. package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +14 -0
  421. package/Sources/RunicMacros/ProviderRegistrationMacros.swift +216 -0
  422. package/Sources/RunicWidget/RunicWidgetBundle.swift +79 -0
  423. package/Sources/RunicWidget/RunicWidgetProvider.swift +283 -0
  424. package/Sources/RunicWidget/RunicWidgetViews.swift +680 -0
  425. package/Tests/RunicTests/AntigravityStatusProbeTests.swift +50 -0
  426. package/Tests/RunicTests/AppDelegateTests.swift +43 -0
  427. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +22 -0
  428. package/Tests/RunicTests/CLIArgumentParsingTests.swift +70 -0
  429. package/Tests/RunicTests/CLICostTests.swift +65 -0
  430. package/Tests/RunicTests/CLIProviderSelectionTests.swift +22 -0
  431. package/Tests/RunicTests/CLISnapshotTests.swift +72 -0
  432. package/Tests/RunicTests/CLIWebFallbackTests.swift +58 -0
  433. package/Tests/RunicTests/ClaudeOAuthTests.swift +164 -0
  434. package/Tests/RunicTests/ClaudeResilienceTests.swift +30 -0
  435. package/Tests/RunicTests/ClaudeUsageTests.swift +370 -0
  436. package/Tests/RunicTests/CodexOAuthTests.swift +122 -0
  437. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +82 -0
  438. package/Tests/RunicTests/CopilotTokenResolverTests.swift +75 -0
  439. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +75 -0
  440. package/Tests/RunicTests/CostUsageDecodingTests.swift +394 -0
  441. package/Tests/RunicTests/CostUsagePricingTests.swift +50 -0
  442. package/Tests/RunicTests/CostUsageScannerTests.swift +700 -0
  443. package/Tests/RunicTests/CursorStatusProbeTests.swift +323 -0
  444. package/Tests/RunicTests/FactoryStatusProbeTests.swift +69 -0
  445. package/Tests/RunicTests/GeminiAPITestHelpers.swift +94 -0
  446. package/Tests/RunicTests/GeminiLoginAlertTests.swift +28 -0
  447. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +283 -0
  448. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +279 -0
  449. package/Tests/RunicTests/GeminiStatusProbeTests.swift +233 -0
  450. package/Tests/RunicTests/GeminiTestEnvironment.swift +87 -0
  451. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +73 -0
  452. package/Tests/RunicTests/InstallOriginTests.swift +18 -0
  453. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +9 -0
  454. package/Tests/RunicTests/LiveAccountTests.swift +24 -0
  455. package/Tests/RunicTests/LoadingPatternTests.swift +35 -0
  456. package/Tests/RunicTests/MenuCardModelTests.swift +545 -0
  457. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +8 -0
  458. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +8 -0
  459. package/Tests/RunicTests/MiniMaxParsingTests.swift +68 -0
  460. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +8 -0
  461. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +112 -0
  462. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +17 -0
  463. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +75 -0
  464. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +27 -0
  465. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +134 -0
  466. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +53 -0
  467. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +92 -0
  468. package/Tests/RunicTests/PathBuilderTests.swift +237 -0
  469. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +160 -0
  470. package/Tests/RunicTests/ProviderIconResourcesTests.swift +38 -0
  471. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +16 -0
  472. package/Tests/RunicTests/ProviderRegistryTests.swift +29 -0
  473. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +202 -0
  474. package/Tests/RunicTests/ProviderToggleStoreTests.swift +51 -0
  475. package/Tests/RunicTests/RunicTests.swift +120 -0
  476. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +36 -0
  477. package/Tests/RunicTests/SettingsStoreTests.swift +161 -0
  478. package/Tests/RunicTests/StatusItemAnimationTests.swift +49 -0
  479. package/Tests/RunicTests/StatusMenuTests.swift +434 -0
  480. package/Tests/RunicTests/StatusProbeTests.swift +377 -0
  481. package/Tests/RunicTests/SubprocessRunnerTests.swift +20 -0
  482. package/Tests/RunicTests/SubscriptionDetectionTests.swift +65 -0
  483. package/Tests/RunicTests/TTYCommandRunnerTests.swift +148 -0
  484. package/Tests/RunicTests/TTYIntegrationTests.swift +41 -0
  485. package/Tests/RunicTests/TeamShowbackExporterTests.swift +159 -0
  486. package/Tests/RunicTests/TextParsingTests.swift +12 -0
  487. package/Tests/RunicTests/UsageFormatterTests.swift +112 -0
  488. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +172 -0
  489. package/Tests/RunicTests/UsageLedgerForecastTests.swift +213 -0
  490. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +83 -0
  491. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +170 -0
  492. package/Tests/RunicTests/UsagePaceTests.swift +77 -0
  493. package/Tests/RunicTests/UsagePaceTextTests.swift +109 -0
  494. package/Tests/RunicTests/UsagePolicyEngineTests.swift +91 -0
  495. package/Tests/RunicTests/WidgetSnapshotTests.swift +45 -0
  496. package/Tests/RunicTests/ZaiProviderTests.swift +61 -0
  497. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +6 -0
  498. package/TestsLinux/PlatformGatingTests.swift +46 -0
  499. package/appcast.xml +105 -0
  500. package/assets/logo-concept-stack.svg +24 -0
  501. package/assets/logo-minimal-bars.svg +15 -0
  502. package/assets/logo.svg +20 -0
  503. package/assets/logo2.svg +43 -0
  504. package/bin/docs-list +5 -0
  505. package/bin/install-runic-cli.sh +32 -0
  506. package/build-and-run.sh +369 -0
  507. package/docs/.nojekyll +1 -0
  508. package/docs/CNAME +1 -0
  509. package/docs/antigravity.md +75 -0
  510. package/docs/architecture.md +32 -0
  511. package/docs/claude.md +93 -0
  512. package/docs/cli-commands/integration_guide.md +371 -0
  513. package/docs/cli-commands/quick_reference.md +362 -0
  514. package/docs/cli-commands/readme.md +361 -0
  515. package/docs/cli.md +156 -0
  516. package/docs/cloudkit_sync_guide.md +495 -0
  517. package/docs/codebase_review.md +288 -0
  518. package/docs/codex-oauth.md +635 -0
  519. package/docs/codex.md +104 -0
  520. package/docs/codexbar.png +0 -0
  521. package/docs/copilot.md +44 -0
  522. package/docs/cursor.md +46 -0
  523. package/docs/design/designcomparisonchart.md +510 -0
  524. package/docs/design/implementationguide.md +792 -0
  525. package/docs/design/menubaricondesignconcepts.md +796 -0
  526. package/docs/design/mockups/hybridiconconcept.svg +62 -0
  527. package/docs/design/mockups/infinityiconconcept.svg +41 -0
  528. package/docs/design/mockups/infinityiconstates.svg +129 -0
  529. package/docs/design/mockups/wavelogorefined.svg +48 -0
  530. package/docs/design/quick_summary.md +426 -0
  531. package/docs/design/readme.md +478 -0
  532. package/docs/design/sizespecifications.md +575 -0
  533. package/docs/design-system.md +836 -0
  534. package/docs/factory.md +103 -0
  535. package/docs/gemini.md +73 -0
  536. package/docs/icon.md +37 -0
  537. package/docs/icon.png +0 -0
  538. package/docs/index.html +175 -0
  539. package/docs/keychain_fix.md +274 -0
  540. package/docs/minimax.md +71 -0
  541. package/docs/packaging.md +25 -0
  542. package/docs/progress-bar-improvements.md +240 -0
  543. package/docs/progress-bar-quick-reference.md +283 -0
  544. package/docs/progress-bar-technical-details.md +432 -0
  545. package/docs/progress-bar-visual-test.swift +375 -0
  546. package/docs/provider.md +202 -0
  547. package/docs/providers.md +86 -0
  548. package/docs/refactor/macros.md +35 -0
  549. package/docs/refresh-loop.md +22 -0
  550. package/docs/releasing-homebrew.md +35 -0
  551. package/docs/releasing.md +121 -0
  552. package/docs/research/concrete_todo_2026-02-22.md +47 -0
  553. package/docs/research/core-feature-research.md +248 -0
  554. package/docs/research/feature_uiux_roadmap_2026-02-22.md +202 -0
  555. package/docs/research/parity_matrix.md +49 -0
  556. package/docs/research/provider_expansion_candidates_2026-02-22.md +80 -0
  557. package/docs/research/provider_model_token_project_matrix_2026-02-23.md +46 -0
  558. package/docs/research/release_gate_checklist.md +57 -0
  559. package/docs/site.css +505 -0
  560. package/docs/sparkle.md +28 -0
  561. package/docs/status.md +28 -0
  562. package/docs/sync/implementation.md +393 -0
  563. package/docs/sync/readme.md +317 -0
  564. package/docs/sync_files_summary.md +391 -0
  565. package/docs/sync_quick_reference.md +225 -0
  566. package/docs/todo.md +11 -0
  567. package/docs/ui.md +29 -0
  568. package/docs/web-integration.md +9 -0
  569. package/docs/widgets.md +85 -0
  570. package/docs/workflows/error-handling-improvements.md +434 -0
  571. package/docs/workflows/folder_structure.md +299 -0
  572. package/docs/workflows/loading_states.md +446 -0
  573. package/docs/workflows/loading_states_examples.md +565 -0
  574. package/docs/workflows/loading_states_flow.md +430 -0
  575. package/docs/workflows/performance_optimizations.md +457 -0
  576. package/docs/workflows/phase1-cli-testing-report.md +313 -0
  577. package/docs/workflows/quick_reference.md +228 -0
  578. package/docs/workflows/readme.md +59 -0
  579. package/docs/workflows/recent_improvements.md +275 -0
  580. package/docs/workflows/sync_enablement_summary.md +400 -0
  581. package/docs/zai.md +43 -0
  582. package/package.json +36 -0
  583. package/runic.png +0 -0
  584. package/version.env +2 -0
@@ -0,0 +1,385 @@
1
+ import Foundation
2
+ import Combine
3
+
4
+ #if canImport(UIKit)
5
+ import UIKit
6
+ #endif
7
+
8
+ #if canImport(AppKit)
9
+ import AppKit
10
+ #endif
11
+
12
+ // MARK: - Background Sync Manager
13
+
14
+ /// Manages background synchronization with configurable intervals
15
+ ///
16
+ /// This manager coordinates automatic synchronization operations based on
17
+ /// app lifecycle events and configured intervals. It supports:
18
+ /// - Active sync (5-minute intervals)
19
+ /// - Background sync (hourly intervals)
20
+ /// - Manual sync on demand
21
+ /// - Lifecycle-aware sync (app launch, foreground, background)
22
+ /// - Network reachability monitoring
23
+ public actor BackgroundSyncManager {
24
+
25
+ // MARK: - Properties
26
+
27
+ /// The underlying sync engine
28
+ private let syncEngine: any SyncEngine
29
+
30
+ /// Sync configuration
31
+ private let config: SyncConfiguration
32
+
33
+ /// Timer for periodic sync operations (must be accessed on main thread)
34
+ @MainActor
35
+ private var syncTimer: Timer?
36
+
37
+ /// Indicates if background sync is currently enabled
38
+ private var isEnabled: Bool = false
39
+
40
+ /// Last successful sync timestamp
41
+ private var lastSyncDate: Date?
42
+
43
+ /// Sync history for diagnostics
44
+ private var syncHistory: [SyncHistoryEntry] = []
45
+
46
+ /// Maximum history entries to retain
47
+ private let maxHistoryEntries: Int = 50
48
+
49
+ /// Observers registered for sync events
50
+ private var observers: [WeakSyncObserver] = []
51
+
52
+ /// Background task identifier (iOS only)
53
+ #if canImport(UIKit)
54
+ private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
55
+ #endif
56
+
57
+ // MARK: - Initialization
58
+
59
+ /// Initializes the background sync manager
60
+ ///
61
+ /// - Parameters:
62
+ /// - syncEngine: The sync engine to use for operations
63
+ /// - config: Configuration for sync intervals and behavior
64
+ public init(
65
+ syncEngine: any SyncEngine,
66
+ config: SyncConfiguration = .default
67
+ ) {
68
+ self.syncEngine = syncEngine
69
+ self.config = config
70
+
71
+ // Register for app lifecycle notifications
72
+ Task {
73
+ await registerLifecycleObservers()
74
+ }
75
+ }
76
+
77
+ // MARK: - Public Methods
78
+
79
+ /// Starts background synchronization
80
+ public func start() {
81
+ guard !isEnabled else { return }
82
+
83
+ isEnabled = true
84
+ scheduleNextSync()
85
+ notifyObservers { $0.syncDidStart() }
86
+ }
87
+
88
+ /// Stops background synchronization
89
+ public func stop() {
90
+ isEnabled = false
91
+ Task { @MainActor in
92
+ syncTimer?.invalidate()
93
+ syncTimer = nil
94
+ }
95
+ }
96
+
97
+ /// Performs a manual sync operation
98
+ ///
99
+ /// - Parameter options: Optional sync options (uses defaults if nil)
100
+ /// - Returns: Result of the sync operation
101
+ @discardableResult
102
+ public func syncNow(options: SyncOptions? = nil) async -> Result<SyncResult, SyncError> {
103
+ let syncOptions = options ?? config.defaultSyncOptions
104
+
105
+ notifyObservers { $0.syncDidStart() }
106
+
107
+ let result = await syncEngine.sync(options: syncOptions)
108
+
109
+ switch result {
110
+ case .success(let syncResult):
111
+ lastSyncDate = Date()
112
+ recordSyncHistory(success: true, result: syncResult)
113
+ notifyObservers { $0.syncDidComplete(result: syncResult) }
114
+
115
+ case .failure(let error):
116
+ recordSyncHistory(success: false, error: error)
117
+ notifyObservers { $0.syncDidFail(error: error) }
118
+ }
119
+
120
+ return result
121
+ }
122
+
123
+ /// Adds an observer for sync events
124
+ ///
125
+ /// - Parameter observer: The observer to add
126
+ public func addObserver(_ observer: SyncObserver) {
127
+ let weakObserver = WeakSyncObserver(observer)
128
+ observers.append(weakObserver)
129
+ }
130
+
131
+ /// Removes an observer
132
+ ///
133
+ /// - Parameter observer: The observer to remove
134
+ public func removeObserver(_ observer: SyncObserver) {
135
+ observers.removeAll { $0.observer === observer }
136
+ }
137
+
138
+ /// Returns sync statistics
139
+ public var statistics: SyncStatistics {
140
+ SyncStatistics(
141
+ lastSyncDate: lastSyncDate,
142
+ totalSyncs: syncHistory.count,
143
+ successfulSyncs: syncHistory.filter { $0.success }.count,
144
+ failedSyncs: syncHistory.filter { !$0.success }.count,
145
+ averageDuration: calculateAverageDuration(),
146
+ syncHistory: Array(syncHistory.suffix(10))
147
+ )
148
+ }
149
+
150
+ /// Updates sync configuration
151
+ ///
152
+ /// - Parameter config: New configuration to apply
153
+ public func updateConfiguration(_ config: SyncConfiguration) async {
154
+ // Note: Configuration updates require recreating the manager
155
+ // This is a placeholder - in practice, you'd want to make config mutable
156
+ if isEnabled {
157
+ stop()
158
+ // Caller should create a new BackgroundSyncManager with new config
159
+ }
160
+ }
161
+
162
+ // MARK: - Private Methods
163
+
164
+ /// Schedules the next automatic sync
165
+ private func scheduleNextSync() {
166
+ guard isEnabled else { return }
167
+
168
+ let interval = determineNextInterval()
169
+
170
+ Task { @MainActor [weak self] in
171
+ guard let self = self else { return }
172
+ self.syncTimer?.invalidate()
173
+ self.syncTimer = Timer.scheduledTimer(
174
+ withTimeInterval: interval,
175
+ repeats: false
176
+ ) { [weak self] _ in
177
+ Task {
178
+ await self?.performScheduledSync()
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ /// Determines the appropriate sync interval based on app state
185
+ private func determineNextInterval() -> TimeInterval {
186
+ #if canImport(UIKit)
187
+ let isActive = UIApplication.shared.applicationState == .active
188
+ return isActive ? config.activeSyncInterval : config.backgroundSyncInterval
189
+ #else
190
+ return config.activeSyncInterval
191
+ #endif
192
+ }
193
+
194
+ /// Performs a scheduled background sync
195
+ private func performScheduledSync() async {
196
+ #if canImport(UIKit)
197
+ // Begin background task
198
+ backgroundTaskID = UIApplication.shared.beginBackgroundTask { [weak self] in
199
+ self?.endBackgroundTask()
200
+ }
201
+ #endif
202
+
203
+ await syncNow()
204
+
205
+ #if canImport(UIKit)
206
+ endBackgroundTask()
207
+ #endif
208
+
209
+ // Schedule next sync
210
+ scheduleNextSync()
211
+ }
212
+
213
+ #if canImport(UIKit)
214
+ /// Ends the background task
215
+ private func endBackgroundTask() {
216
+ guard backgroundTaskID != .invalid else { return }
217
+ UIApplication.shared.endBackgroundTask(backgroundTaskID)
218
+ backgroundTaskID = .invalid
219
+ }
220
+ #endif
221
+
222
+ /// Registers observers for app lifecycle events
223
+ private func registerLifecycleObservers() {
224
+ #if canImport(UIKit)
225
+ NotificationCenter.default.addObserver(
226
+ forName: UIApplication.didBecomeActiveNotification,
227
+ object: nil,
228
+ queue: .main
229
+ ) { [weak self] _ in
230
+ Task { await self?.handleAppBecameActive() }
231
+ }
232
+
233
+ NotificationCenter.default.addObserver(
234
+ forName: UIApplication.didEnterBackgroundNotification,
235
+ object: nil,
236
+ queue: .main
237
+ ) { [weak self] _ in
238
+ Task { await self?.handleAppEnteredBackground() }
239
+ }
240
+ #endif
241
+
242
+ #if canImport(AppKit)
243
+ NotificationCenter.default.addObserver(
244
+ forName: NSApplication.didBecomeActiveNotification,
245
+ object: nil,
246
+ queue: .main
247
+ ) { [weak self] _ in
248
+ Task { await self?.handleAppBecameActive() }
249
+ }
250
+
251
+ NotificationCenter.default.addObserver(
252
+ forName: NSApplication.didResignActiveNotification,
253
+ object: nil,
254
+ queue: .main
255
+ ) { [weak self] _ in
256
+ Task { await self?.handleAppEnteredBackground() }
257
+ }
258
+ #endif
259
+ }
260
+
261
+ /// Handles app becoming active
262
+ private func handleAppBecameActive() async {
263
+ guard config.syncOnForeground else { return }
264
+
265
+ // Sync if last sync was more than interval ago
266
+ if shouldSyncOnForeground() {
267
+ await syncNow()
268
+ }
269
+ }
270
+
271
+ /// Handles app entering background
272
+ private func handleAppEnteredBackground() async {
273
+ guard config.syncOnBackground else { return }
274
+ await syncNow()
275
+ }
276
+
277
+ /// Determines if sync should occur on foreground
278
+ private func shouldSyncOnForeground() -> Bool {
279
+ guard let lastSync = lastSyncDate else { return true }
280
+
281
+ let elapsed = Date().timeIntervalSince(lastSync)
282
+ return elapsed >= config.activeSyncInterval
283
+ }
284
+
285
+ /// Records sync operation in history
286
+ private func recordSyncHistory(
287
+ success: Bool,
288
+ result: SyncResult? = nil,
289
+ error: SyncError? = nil
290
+ ) {
291
+ let entry = SyncHistoryEntry(
292
+ timestamp: Date(),
293
+ success: success,
294
+ duration: result?.duration ?? 0,
295
+ pushedCount: result?.pushedCount ?? 0,
296
+ fetchedCount: result?.fetchedCount ?? 0,
297
+ error: error?.localizedDescription
298
+ )
299
+
300
+ syncHistory.append(entry)
301
+
302
+ // Trim history if needed
303
+ if syncHistory.count > maxHistoryEntries {
304
+ syncHistory.removeFirst(syncHistory.count - maxHistoryEntries)
305
+ }
306
+ }
307
+
308
+ /// Calculates average sync duration
309
+ private func calculateAverageDuration() -> TimeInterval {
310
+ let successfulSyncs = syncHistory.filter { $0.success && $0.duration > 0 }
311
+ guard !successfulSyncs.isEmpty else { return 0 }
312
+
313
+ let totalDuration = successfulSyncs.reduce(0.0) { $0 + $1.duration }
314
+ return totalDuration / Double(successfulSyncs.count)
315
+ }
316
+
317
+ /// Notifies all observers
318
+ private func notifyObservers(_ action: (SyncObserver) -> Void) {
319
+ // Clean up deallocated observers
320
+ observers.removeAll { $0.observer == nil }
321
+
322
+ // Notify active observers
323
+ for weakObserver in observers {
324
+ if let observer = weakObserver.observer {
325
+ action(observer)
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ // MARK: - Supporting Types
332
+
333
+ /// Configuration for background sync behavior
334
+ public struct SyncConfiguration: Sendable {
335
+ public let activeSyncInterval: TimeInterval
336
+ public let backgroundSyncInterval: TimeInterval
337
+ public let syncOnForeground: Bool
338
+ public let syncOnBackground: Bool
339
+ public let defaultSyncOptions: SyncOptions
340
+
341
+ public init(
342
+ activeSyncInterval: TimeInterval = 300,
343
+ backgroundSyncInterval: TimeInterval = 3600,
344
+ syncOnForeground: Bool = true,
345
+ syncOnBackground: Bool = true,
346
+ defaultSyncOptions: SyncOptions = SyncOptions()
347
+ ) {
348
+ self.activeSyncInterval = activeSyncInterval
349
+ self.backgroundSyncInterval = backgroundSyncInterval
350
+ self.syncOnForeground = syncOnForeground
351
+ self.syncOnBackground = syncOnBackground
352
+ self.defaultSyncOptions = defaultSyncOptions
353
+ }
354
+
355
+ public static let `default` = SyncConfiguration()
356
+ }
357
+
358
+ /// Statistics about sync operations
359
+ public struct SyncStatistics: Sendable {
360
+ public let lastSyncDate: Date?
361
+ public let totalSyncs: Int
362
+ public let successfulSyncs: Int
363
+ public let failedSyncs: Int
364
+ public let averageDuration: TimeInterval
365
+ public let syncHistory: [SyncHistoryEntry]
366
+
367
+ public var successRate: Double {
368
+ guard totalSyncs > 0 else { return 0 }
369
+ return Double(successfulSyncs) / Double(totalSyncs) * 100
370
+ }
371
+ }
372
+
373
+ public struct SyncHistoryEntry: Sendable, Codable {
374
+ public let timestamp: Date
375
+ public let success: Bool
376
+ public let duration: TimeInterval
377
+ public let pushedCount: Int
378
+ public let fetchedCount: Int
379
+ public let error: String?
380
+ }
381
+
382
+ private struct WeakSyncObserver {
383
+ weak var observer: SyncObserver?
384
+ init(_ observer: SyncObserver) { self.observer = observer }
385
+ }
@@ -0,0 +1,310 @@
1
+ import Foundation
2
+
3
+ // MARK: - Sync Conflict Resolver
4
+
5
+ /// Default implementation of conflict resolution for synchronized records
6
+ ///
7
+ /// This resolver implements multiple conflict resolution strategies:
8
+ /// - Last-write-wins (default): Most recently modified record wins
9
+ /// - Prefer local: Always keep local version
10
+ /// - Prefer remote: Always keep remote version
11
+ /// - Highest version: Keep record with highest version number
12
+ /// - Merge: Attempt to merge both versions (custom logic required)
13
+ public actor SyncConflictResolver: SyncConflictResolverProtocol {
14
+
15
+ // MARK: - Properties
16
+
17
+ /// Statistics about resolved conflicts
18
+ public private(set) var conflictStats: ConflictStatistics
19
+
20
+ /// Custom merge handlers for specific record types
21
+ private var mergeHandlers: [String: MergeHandler] = [:]
22
+
23
+ // MARK: - Initialization
24
+
25
+ public init() {
26
+ self.conflictStats = ConflictStatistics()
27
+ }
28
+
29
+ // MARK: - Public Methods
30
+
31
+ /// Resolves a conflict between local and remote versions
32
+ ///
33
+ /// - Parameters:
34
+ /// - local: The local version of the record
35
+ /// - remote: The remote version of the record
36
+ /// - strategy: The resolution strategy to apply
37
+ /// - Returns: The resolved record that should be kept
38
+ public func resolve(
39
+ local: SyncableRecord,
40
+ remote: SyncableRecord,
41
+ strategy: ConflictResolutionStrategy
42
+ ) async -> SyncableRecord {
43
+ // Validate that records are the same type
44
+ guard local.recordID == remote.recordID else {
45
+ return local // Fallback to local if IDs don't match
46
+ }
47
+
48
+ let resolved: SyncableRecord
49
+
50
+ switch strategy {
51
+ case .lastWriteWins:
52
+ resolved = resolveLastWriteWins(local: local, remote: remote)
53
+
54
+ case .preferLocal:
55
+ resolved = local
56
+
57
+ case .preferRemote:
58
+ resolved = remote
59
+
60
+ case .highestVersion:
61
+ resolved = resolveHighestVersion(local: local, remote: remote)
62
+
63
+ case .merge:
64
+ resolved = resolveMerge(local: local, remote: remote)
65
+ }
66
+
67
+ // Update statistics
68
+ conflictStats.recordConflict(
69
+ recordType: local.recordType,
70
+ strategy: strategy,
71
+ winner: resolved.recordID == local.recordID ? .local : .remote
72
+ )
73
+
74
+ return resolved
75
+ }
76
+
77
+ /// Registers a custom merge handler for a specific record type
78
+ ///
79
+ /// - Parameters:
80
+ /// - recordType: The CloudKit record type name
81
+ /// - handler: Closure that merges two records of this type
82
+ public func registerMergeHandler(
83
+ for recordType: String,
84
+ handler: @escaping MergeHandler
85
+ ) {
86
+ mergeHandlers[recordType] = handler
87
+ }
88
+
89
+ /// Resets conflict statistics
90
+ public func resetStatistics() {
91
+ conflictStats = ConflictStatistics()
92
+ }
93
+
94
+ // MARK: - Private Resolution Methods
95
+
96
+ /// Resolves conflict by keeping the most recently modified record
97
+ private func resolveLastWriteWins(
98
+ local: SyncableRecord,
99
+ remote: SyncableRecord
100
+ ) -> SyncableRecord {
101
+ if local.modifiedAt > remote.modifiedAt {
102
+ return local
103
+ } else if remote.modifiedAt > local.modifiedAt {
104
+ return remote
105
+ } else {
106
+ // Same timestamp, use version as tiebreaker
107
+ return local.version >= remote.version ? local : remote
108
+ }
109
+ }
110
+
111
+ /// Resolves conflict by keeping the record with highest version number
112
+ private func resolveHighestVersion(
113
+ local: SyncableRecord,
114
+ remote: SyncableRecord
115
+ ) -> SyncableRecord {
116
+ if local.version > remote.version {
117
+ return local
118
+ } else if remote.version > local.version {
119
+ return remote
120
+ } else {
121
+ // Same version, use timestamp as tiebreaker
122
+ return local.modifiedAt >= remote.modifiedAt ? local : remote
123
+ }
124
+ }
125
+
126
+ /// Attempts to merge both versions using custom merge logic
127
+ private func resolveMerge(
128
+ local: SyncableRecord,
129
+ remote: SyncableRecord
130
+ ) -> SyncableRecord {
131
+ // Check for registered merge handler
132
+ if let handler = mergeHandlers[local.recordType] {
133
+ return handler(local, remote)
134
+ }
135
+
136
+ // No custom merge handler, fall back to last-write-wins
137
+ return resolveLastWriteWins(local: local, remote: remote)
138
+ }
139
+ }
140
+
141
+ // MARK: - Supporting Types
142
+
143
+ /// Closure type for custom merge handlers
144
+ public typealias MergeHandler = @Sendable (SyncableRecord, SyncableRecord) -> SyncableRecord
145
+
146
+ /// Indicates which version won during conflict resolution
147
+ public enum ConflictWinner: String, Codable, Sendable {
148
+ case local
149
+ case remote
150
+ }
151
+
152
+ /// Statistics about conflict resolution operations
153
+ public struct ConflictStatistics: Sendable {
154
+ /// Total number of conflicts resolved
155
+ public private(set) var totalConflicts: Int = 0
156
+
157
+ /// Number of conflicts per record type
158
+ public private(set) var conflictsByType: [String: Int] = [:]
159
+
160
+ /// Number of conflicts per strategy
161
+ public private(set) var conflictsByStrategy: [String: Int] = [:]
162
+
163
+ /// Number of wins for local vs remote
164
+ public private(set) var localWins: Int = 0
165
+ public private(set) var remoteWins: Int = 0
166
+
167
+ /// Timestamp of last conflict
168
+ public private(set) var lastConflictAt: Date?
169
+
170
+ public init() {}
171
+
172
+ /// Records a conflict resolution event
173
+ mutating func recordConflict(
174
+ recordType: String,
175
+ strategy: ConflictResolutionStrategy,
176
+ winner: ConflictWinner
177
+ ) {
178
+ totalConflicts += 1
179
+ conflictsByType[recordType, default: 0] += 1
180
+ conflictsByStrategy[strategy.rawValue, default: 0] += 1
181
+
182
+ switch winner {
183
+ case .local:
184
+ localWins += 1
185
+ case .remote:
186
+ remoteWins += 1
187
+ }
188
+
189
+ lastConflictAt = Date()
190
+ }
191
+
192
+ /// Percentage of conflicts won by local version
193
+ public var localWinRate: Double {
194
+ guard totalConflicts > 0 else { return 0 }
195
+ return Double(localWins) / Double(totalConflicts)
196
+ }
197
+
198
+ /// Percentage of conflicts won by remote version
199
+ public var remoteWinRate: Double {
200
+ guard totalConflicts > 0 else { return 0 }
201
+ return Double(remoteWins) / Double(totalConflicts)
202
+ }
203
+
204
+ /// Human-readable summary of conflict statistics
205
+ public var summary: String {
206
+ var lines = [
207
+ "Conflict Resolution Statistics:",
208
+ " Total conflicts: \(totalConflicts)",
209
+ " Local wins: \(localWins) (\(String(format: "%.1f%%", localWinRate * 100)))",
210
+ " Remote wins: \(remoteWins) (\(String(format: "%.1f%%", remoteWinRate * 100)))"
211
+ ]
212
+
213
+ if !conflictsByType.isEmpty {
214
+ lines.append(" By record type:")
215
+ for (type, count) in conflictsByType.sorted(by: { $0.value > $1.value }) {
216
+ lines.append(" \(type): \(count)")
217
+ }
218
+ }
219
+
220
+ if !conflictsByStrategy.isEmpty {
221
+ lines.append(" By strategy:")
222
+ for (strategy, count) in conflictsByStrategy.sorted(by: { $0.value > $1.value }) {
223
+ lines.append(" \(strategy): \(count)")
224
+ }
225
+ }
226
+
227
+ if let lastConflict = lastConflictAt {
228
+ let formatter = RelativeDateTimeFormatter()
229
+ let timeAgo = formatter.localizedString(for: lastConflict, relativeTo: Date())
230
+ lines.append(" Last conflict: \(timeAgo)")
231
+ }
232
+
233
+ return lines.joined(separator: "\n")
234
+ }
235
+ }
236
+
237
+ // MARK: - Default Merge Handlers
238
+
239
+ extension SyncConflictResolver {
240
+ /// Registers default merge handlers for Runic record types
241
+ public func registerDefaultMergeHandlers() {
242
+ // Usage Snapshot merge: prefer record with most recent data
243
+ registerMergeHandler(for: CloudKitRecordType.usageSnapshot) { local, remote in
244
+ guard let localUsage = local as? UsageSnapshotSyncRecord,
245
+ let remoteUsage = remote as? UsageSnapshotSyncRecord else {
246
+ return local
247
+ }
248
+
249
+ // Prefer the record with more recent usage data
250
+ return localUsage.updatedAt > remoteUsage.updatedAt ? local : remote
251
+ }
252
+
253
+ // User Preferences merge: combine enabled providers
254
+ registerMergeHandler(for: CloudKitRecordType.userPreferences) { local, remote in
255
+ guard let localPrefs = local as? UserPreferencesSyncRecord,
256
+ let remotePrefs = remote as? UserPreferencesSyncRecord else {
257
+ return local
258
+ }
259
+
260
+ // Merge enabled providers (union of both sets)
261
+ let mergedProviders = Array(Set(localPrefs.enabledProviders + remotePrefs.enabledProviders))
262
+
263
+ // Use most recent settings for other fields
264
+ let source = localPrefs.modifiedAt > remotePrefs.modifiedAt ? localPrefs : remotePrefs
265
+
266
+ return UserPreferencesSyncRecord(
267
+ recordID: localPrefs.recordID,
268
+ version: max(localPrefs.version, remotePrefs.version) + 1,
269
+ modifiedAt: Date(),
270
+ lastModifiedDeviceID: source.lastModifiedDeviceID,
271
+ refreshInterval: source.refreshInterval,
272
+ enabledProviders: mergedProviders,
273
+ notificationsEnabled: source.notificationsEnabled,
274
+ autoRefreshEnabled: source.autoRefreshEnabled,
275
+ theme: source.theme,
276
+ displayFormat: source.displayFormat
277
+ )
278
+ }
279
+
280
+ // Alert Configuration merge: prefer most restrictive thresholds
281
+ registerMergeHandler(for: CloudKitRecordType.alertConfiguration) { local, remote in
282
+ guard let localAlert = local as? AlertConfigurationSyncRecord,
283
+ let remoteAlert = remote as? AlertConfigurationSyncRecord else {
284
+ return local
285
+ }
286
+
287
+ // Use lower (more conservative) thresholds
288
+ let warningThreshold = min(localAlert.warningThreshold, remoteAlert.warningThreshold)
289
+ let criticalThreshold = min(localAlert.criticalThreshold, remoteAlert.criticalThreshold)
290
+
291
+ // Merge notification channels (union)
292
+ let mergedChannels = Array(Set(localAlert.notificationChannels + remoteAlert.notificationChannels))
293
+
294
+ // Enabled if either version has it enabled
295
+ let enabled = localAlert.enabled || remoteAlert.enabled
296
+
297
+ return AlertConfigurationSyncRecord(
298
+ recordID: localAlert.recordID,
299
+ version: max(localAlert.version, remoteAlert.version) + 1,
300
+ modifiedAt: Date(),
301
+ lastModifiedDeviceID: localAlert.lastModifiedDeviceID,
302
+ providerID: localAlert.providerID,
303
+ warningThreshold: warningThreshold,
304
+ criticalThreshold: criticalThreshold,
305
+ notificationChannels: mergedChannels,
306
+ enabled: enabled
307
+ )
308
+ }
309
+ }
310
+ }