@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,288 @@
1
+ import Foundation
2
+ import CloudKit
3
+
4
+ // MARK: - Sync Protocol Definitions
5
+
6
+ /// Protocol defining the core synchronization engine interface
7
+ ///
8
+ /// This protocol abstracts the synchronization logic to allow for different
9
+ /// implementations (CloudKit, custom server, etc.) while maintaining a
10
+ /// consistent API for syncing data across devices.
11
+ public protocol SyncEngine: Sendable {
12
+ /// Synchronizes local data with remote storage
13
+ ///
14
+ /// - Parameter options: Configuration options for this sync operation
15
+ /// - Returns: Result indicating success or failure with error details
16
+ func sync(options: SyncOptions) async -> Result<SyncResult, SyncError>
17
+
18
+ /// Pushes local changes to remote storage
19
+ ///
20
+ /// - Parameter records: Array of records to push
21
+ /// - Returns: Result containing pushed record identifiers or error
22
+ func push(records: [SyncableRecord]) async -> Result<[String], SyncError>
23
+
24
+ /// Fetches changes from remote storage
25
+ ///
26
+ /// - Parameter recordTypes: Array of record type names to fetch
27
+ /// - Returns: Result containing fetched records or error
28
+ func fetch(recordTypes: [String]) async -> Result<[SyncableRecord], SyncError>
29
+
30
+ /// Deletes records from remote storage
31
+ ///
32
+ /// - Parameter recordIDs: Array of record identifiers to delete
33
+ /// - Returns: Result containing deleted record identifiers or error
34
+ func delete(recordIDs: [String]) async -> Result<[String], SyncError>
35
+
36
+ /// Resets sync state and clears all remote data
37
+ ///
38
+ /// - Warning: This operation is destructive and cannot be undone
39
+ /// - Returns: Result indicating success or failure
40
+ func resetSync() async -> Result<Void, SyncError>
41
+ }
42
+
43
+ // MARK: - Syncable Record Protocol
44
+
45
+ /// Protocol for objects that can be synchronized across devices
46
+ ///
47
+ /// Conform to this protocol to enable CloudKit synchronization for your data types.
48
+ public protocol SyncableRecord: Sendable, Codable {
49
+ /// Unique identifier for the record
50
+ var recordID: String { get }
51
+
52
+ /// Type name used for CloudKit record type
53
+ var recordType: String { get }
54
+
55
+ /// Version number for conflict resolution
56
+ var version: Int { get }
57
+
58
+ /// Last modification timestamp
59
+ var modifiedAt: Date { get }
60
+
61
+ /// Device identifier that last modified this record
62
+ var lastModifiedDeviceID: String? { get }
63
+
64
+ /// Converts the record to a CloudKit CKRecord
65
+ ///
66
+ /// - Returns: CloudKit record representation
67
+ func toCKRecord() throws -> CKRecord
68
+
69
+ /// Creates a record from a CloudKit CKRecord
70
+ ///
71
+ /// - Parameter ckRecord: CloudKit record to convert
72
+ /// - Returns: Initialized syncable record
73
+ static func fromCKRecord(_ ckRecord: CKRecord) throws -> Self
74
+ }
75
+
76
+ // MARK: - Sync Conflict Resolver Protocol
77
+
78
+ /// Protocol for resolving conflicts when multiple devices modify the same record
79
+ ///
80
+ /// Implement this protocol to define custom conflict resolution strategies
81
+ /// beyond the default last-write-wins behavior.
82
+ public protocol SyncConflictResolverProtocol: Sendable {
83
+ /// Resolves a conflict between local and remote versions of a record
84
+ ///
85
+ /// - Parameters:
86
+ /// - local: The local version of the conflicting record
87
+ /// - remote: The remote version of the conflicting record
88
+ /// - strategy: The conflict resolution strategy to apply
89
+ /// - Returns: The resolved record that should be kept
90
+ func resolve(
91
+ local: SyncableRecord,
92
+ remote: SyncableRecord,
93
+ strategy: ConflictResolutionStrategy
94
+ ) async -> SyncableRecord
95
+ }
96
+
97
+ // MARK: - Sync Observer Protocol
98
+
99
+ /// Protocol for observing synchronization events
100
+ ///
101
+ /// Implement this protocol to receive notifications about sync state changes,
102
+ /// progress updates, and completion events.
103
+ public protocol SyncObserver: AnyObject, Sendable {
104
+ /// Called when synchronization begins
105
+ func syncDidStart()
106
+
107
+ /// Called when synchronization progress updates
108
+ ///
109
+ /// - Parameters:
110
+ /// - current: Number of records processed
111
+ /// - total: Total number of records to process
112
+ func syncDidProgress(current: Int, total: Int)
113
+
114
+ /// Called when synchronization completes successfully
115
+ ///
116
+ /// - Parameter result: The sync result containing statistics
117
+ func syncDidComplete(result: SyncResult)
118
+
119
+ /// Called when synchronization fails
120
+ ///
121
+ /// - Parameter error: The error that caused the failure
122
+ func syncDidFail(error: SyncError)
123
+ }
124
+
125
+ // MARK: - Supporting Types
126
+
127
+ /// Configuration options for sync operations
128
+ public struct SyncOptions: Sendable {
129
+ /// Whether to force a full sync, ignoring change tracking
130
+ public let forceFullSync: Bool
131
+
132
+ /// Maximum number of records to sync in a single batch
133
+ public let batchSize: Int
134
+
135
+ /// Conflict resolution strategy to use
136
+ public let conflictStrategy: ConflictResolutionStrategy
137
+
138
+ /// Whether to sync in background mode
139
+ public let backgroundMode: Bool
140
+
141
+ /// Timeout interval for sync operations
142
+ public let timeout: TimeInterval
143
+
144
+ /// Whether to encrypt sensitive data
145
+ public let encryptSensitiveData: Bool
146
+
147
+ public init(
148
+ forceFullSync: Bool = false,
149
+ batchSize: Int = 100,
150
+ conflictStrategy: ConflictResolutionStrategy = .lastWriteWins,
151
+ backgroundMode: Bool = false,
152
+ timeout: TimeInterval = 60.0,
153
+ encryptSensitiveData: Bool = true
154
+ ) {
155
+ self.forceFullSync = forceFullSync
156
+ self.batchSize = batchSize
157
+ self.conflictStrategy = conflictStrategy
158
+ self.backgroundMode = backgroundMode
159
+ self.timeout = timeout
160
+ self.encryptSensitiveData = encryptSensitiveData
161
+ }
162
+ }
163
+
164
+ /// Result of a sync operation with statistics
165
+ public struct SyncResult: Sendable {
166
+ /// Number of records successfully pushed
167
+ public let pushedCount: Int
168
+
169
+ /// Number of records successfully fetched
170
+ public let fetchedCount: Int
171
+
172
+ /// Number of conflicts resolved
173
+ public let conflictsResolved: Int
174
+
175
+ /// Number of records deleted
176
+ public let deletedCount: Int
177
+
178
+ /// Timestamp when sync completed
179
+ public let completedAt: Date
180
+
181
+ /// Duration of the sync operation
182
+ public let duration: TimeInterval
183
+
184
+ /// Any non-fatal warnings encountered
185
+ public let warnings: [String]
186
+
187
+ public init(
188
+ pushedCount: Int = 0,
189
+ fetchedCount: Int = 0,
190
+ conflictsResolved: Int = 0,
191
+ deletedCount: Int = 0,
192
+ completedAt: Date = Date(),
193
+ duration: TimeInterval = 0,
194
+ warnings: [String] = []
195
+ ) {
196
+ self.pushedCount = pushedCount
197
+ self.fetchedCount = fetchedCount
198
+ self.conflictsResolved = conflictsResolved
199
+ self.deletedCount = deletedCount
200
+ self.completedAt = completedAt
201
+ self.duration = duration
202
+ self.warnings = warnings
203
+ }
204
+ }
205
+
206
+ /// Conflict resolution strategies
207
+ public enum ConflictResolutionStrategy: String, Codable, Sendable {
208
+ /// Keep the most recently modified record (default)
209
+ case lastWriteWins
210
+
211
+ /// Keep the local version
212
+ case preferLocal
213
+
214
+ /// Keep the remote version
215
+ case preferRemote
216
+
217
+ /// Keep the version with higher version number
218
+ case highestVersion
219
+
220
+ /// Merge both versions (requires custom merge logic)
221
+ case merge
222
+ }
223
+
224
+ /// Errors that can occur during synchronization
225
+ public enum SyncError: Error, Sendable {
226
+ /// iCloud account is not available
227
+ case iCloudAccountUnavailable
228
+
229
+ /// Network connection is not available
230
+ case networkUnavailable
231
+
232
+ /// CloudKit quota exceeded
233
+ case quotaExceeded
234
+
235
+ /// Record format is invalid
236
+ case invalidRecordFormat(String)
237
+
238
+ /// Sync operation timed out
239
+ case timeout
240
+
241
+ /// Authentication failed
242
+ case authenticationFailed
243
+
244
+ /// Conflict could not be resolved
245
+ case conflictResolutionFailed(String)
246
+
247
+ /// Generic CloudKit error
248
+ case cloudKitError(Error)
249
+
250
+ /// Encryption/decryption failed
251
+ case encryptionFailed(String)
252
+
253
+ /// Unknown error occurred
254
+ case unknown(Error)
255
+
256
+ public var localizedDescription: String {
257
+ switch self {
258
+ case .iCloudAccountUnavailable:
259
+ return "iCloud account is not available. Please sign in to iCloud in Settings."
260
+ case .networkUnavailable:
261
+ return "Network connection is not available. Sync will retry when online."
262
+ case .quotaExceeded:
263
+ return "iCloud storage quota exceeded. Please free up space or upgrade your plan."
264
+ case .invalidRecordFormat(let details):
265
+ return "Invalid record format: \(details)"
266
+ case .timeout:
267
+ return "Sync operation timed out. Please try again."
268
+ case .authenticationFailed:
269
+ return "Authentication failed. Please sign in again."
270
+ case .conflictResolutionFailed(let details):
271
+ return "Failed to resolve conflict: \(details)"
272
+ case .cloudKitError(let error):
273
+ return "CloudKit error: \(error.localizedDescription)"
274
+ case .encryptionFailed(let details):
275
+ return "Encryption failed: \(details)"
276
+ case .unknown(let error):
277
+ return "Unknown error: \(error.localizedDescription)"
278
+ }
279
+ }
280
+ }
281
+
282
+ /// Priority levels for sync operations
283
+ public enum SyncPriority: Int, Sendable {
284
+ case low = 0
285
+ case normal = 1
286
+ case high = 2
287
+ case critical = 3
288
+ }
@@ -0,0 +1,405 @@
1
+ import Foundation
2
+ import CloudKit
3
+ import CryptoKit
4
+ import LocalAuthentication
5
+
6
+ // MARK: - CloudKit Record Type Constants
7
+
8
+ /// CloudKit record type identifiers
9
+ public enum CloudKitRecordType {
10
+ public static let usageSnapshot = "UsageSnapshot"
11
+ public static let userPreferences = "UserPreferences"
12
+ public static let alertConfiguration = "AlertConfiguration"
13
+ public static let syncMetadata = "SyncMetadata"
14
+ }
15
+
16
+ // MARK: - Usage Snapshot Sync Record
17
+
18
+ /// Syncable usage snapshot record for cross-device synchronization
19
+ ///
20
+ /// This record encapsulates usage data that can be synchronized across
21
+ /// devices using CloudKit, with support for encryption and versioning.
22
+ public struct UsageSnapshotSyncRecord: SyncableRecord {
23
+ public let recordID: String
24
+ public let recordType: String = CloudKitRecordType.usageSnapshot
25
+ public let version: Int
26
+ public let modifiedAt: Date
27
+ public let lastModifiedDeviceID: String?
28
+
29
+ // Usage data
30
+ public let providerID: String
31
+ public let primaryUsed: Int
32
+ public let primaryLimit: Int?
33
+ public let secondaryUsed: Int?
34
+ public let secondaryLimit: Int?
35
+ public let costUSD: Double?
36
+ public let accountEmail: String?
37
+ public let updatedAt: Date
38
+
39
+ // Metadata
40
+ public let deviceName: String
41
+ public let platform: String
42
+
43
+ public init(
44
+ recordID: String = UUID().uuidString,
45
+ version: Int = 1,
46
+ modifiedAt: Date = Date(),
47
+ lastModifiedDeviceID: String? = nil,
48
+ providerID: String,
49
+ primaryUsed: Int,
50
+ primaryLimit: Int?,
51
+ secondaryUsed: Int? = nil,
52
+ secondaryLimit: Int? = nil,
53
+ costUSD: Double? = nil,
54
+ accountEmail: String? = nil,
55
+ updatedAt: Date = Date(),
56
+ deviceName: String,
57
+ platform: String
58
+ ) {
59
+ self.recordID = recordID
60
+ self.version = version
61
+ self.modifiedAt = modifiedAt
62
+ self.lastModifiedDeviceID = lastModifiedDeviceID
63
+ self.providerID = providerID
64
+ self.primaryUsed = primaryUsed
65
+ self.primaryLimit = primaryLimit
66
+ self.secondaryUsed = secondaryUsed
67
+ self.secondaryLimit = secondaryLimit
68
+ self.costUSD = costUSD
69
+ self.accountEmail = accountEmail
70
+ self.updatedAt = updatedAt
71
+ self.deviceName = deviceName
72
+ self.platform = platform
73
+ }
74
+
75
+ public func toCKRecord() throws -> CKRecord {
76
+ let record = CKRecord(recordType: recordType, recordID: CKRecord.ID(recordName: recordID))
77
+
78
+ record["version"] = version as CKRecordValue
79
+ record["modifiedAt"] = modifiedAt as CKRecordValue
80
+ record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
81
+ record["providerID"] = providerID as CKRecordValue
82
+ record["primaryUsed"] = primaryUsed as CKRecordValue
83
+ record["primaryLimit"] = primaryLimit as? CKRecordValue
84
+ record["secondaryUsed"] = secondaryUsed as? CKRecordValue
85
+ record["secondaryLimit"] = secondaryLimit as? CKRecordValue
86
+ record["costUSD"] = costUSD as? CKRecordValue
87
+ record["updatedAt"] = updatedAt as CKRecordValue
88
+ record["deviceName"] = deviceName as CKRecordValue
89
+ record["platform"] = platform as CKRecordValue
90
+
91
+ // Encrypt sensitive data
92
+ if let email = accountEmail {
93
+ let encrypted = try encryptString(email)
94
+ record["accountEmail"] = encrypted as CKRecordValue
95
+ }
96
+
97
+ return record
98
+ }
99
+
100
+ public static func fromCKRecord(_ ckRecord: CKRecord) throws -> UsageSnapshotSyncRecord {
101
+ guard let version = ckRecord["version"] as? Int,
102
+ let modifiedAt = ckRecord["modifiedAt"] as? Date,
103
+ let providerID = ckRecord["providerID"] as? String,
104
+ let primaryUsed = ckRecord["primaryUsed"] as? Int,
105
+ let updatedAt = ckRecord["updatedAt"] as? Date,
106
+ let deviceName = ckRecord["deviceName"] as? String,
107
+ let platform = ckRecord["platform"] as? String
108
+ else {
109
+ throw SyncError.invalidRecordFormat("Missing required fields in UsageSnapshot")
110
+ }
111
+
112
+ var accountEmail: String?
113
+ if let encryptedEmail = ckRecord["accountEmail"] as? String {
114
+ accountEmail = try? decryptString(encryptedEmail)
115
+ }
116
+
117
+ return UsageSnapshotSyncRecord(
118
+ recordID: ckRecord.recordID.recordName,
119
+ version: version,
120
+ modifiedAt: modifiedAt,
121
+ lastModifiedDeviceID: ckRecord["lastModifiedDeviceID"] as? String,
122
+ providerID: providerID,
123
+ primaryUsed: primaryUsed,
124
+ primaryLimit: ckRecord["primaryLimit"] as? Int,
125
+ secondaryUsed: ckRecord["secondaryUsed"] as? Int,
126
+ secondaryLimit: ckRecord["secondaryLimit"] as? Int,
127
+ costUSD: ckRecord["costUSD"] as? Double,
128
+ accountEmail: accountEmail,
129
+ updatedAt: updatedAt,
130
+ deviceName: deviceName,
131
+ platform: platform
132
+ )
133
+ }
134
+ }
135
+
136
+ // MARK: - User Preferences Sync Record
137
+
138
+ /// Syncable user preferences record
139
+ ///
140
+ /// Stores user preferences and settings that should be synchronized
141
+ /// across all devices where the user is signed in.
142
+ public struct UserPreferencesSyncRecord: SyncableRecord {
143
+ public let recordID: String
144
+ public let recordType: String = CloudKitRecordType.userPreferences
145
+ public let version: Int
146
+ public let modifiedAt: Date
147
+ public let lastModifiedDeviceID: String?
148
+
149
+ // Preferences
150
+ public let refreshInterval: TimeInterval
151
+ public let enabledProviders: [String]
152
+ public let notificationsEnabled: Bool
153
+ public let autoRefreshEnabled: Bool
154
+ public let theme: String
155
+ public let displayFormat: String
156
+
157
+ public init(
158
+ recordID: String = "user-preferences",
159
+ version: Int = 1,
160
+ modifiedAt: Date = Date(),
161
+ lastModifiedDeviceID: String? = nil,
162
+ refreshInterval: TimeInterval = 300,
163
+ enabledProviders: [String] = [],
164
+ notificationsEnabled: Bool = true,
165
+ autoRefreshEnabled: Bool = true,
166
+ theme: String = "system",
167
+ displayFormat: String = "compact"
168
+ ) {
169
+ self.recordID = recordID
170
+ self.version = version
171
+ self.modifiedAt = modifiedAt
172
+ self.lastModifiedDeviceID = lastModifiedDeviceID
173
+ self.refreshInterval = refreshInterval
174
+ self.enabledProviders = enabledProviders
175
+ self.notificationsEnabled = notificationsEnabled
176
+ self.autoRefreshEnabled = autoRefreshEnabled
177
+ self.theme = theme
178
+ self.displayFormat = displayFormat
179
+ }
180
+
181
+ public func toCKRecord() throws -> CKRecord {
182
+ let record = CKRecord(recordType: recordType, recordID: CKRecord.ID(recordName: recordID))
183
+
184
+ record["version"] = version as CKRecordValue
185
+ record["modifiedAt"] = modifiedAt as CKRecordValue
186
+ record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
187
+ record["refreshInterval"] = refreshInterval as CKRecordValue
188
+ record["enabledProviders"] = enabledProviders as CKRecordValue
189
+ record["notificationsEnabled"] = (notificationsEnabled ? 1 : 0) as CKRecordValue
190
+ record["autoRefreshEnabled"] = (autoRefreshEnabled ? 1 : 0) as CKRecordValue
191
+ record["theme"] = theme as CKRecordValue
192
+ record["displayFormat"] = displayFormat as CKRecordValue
193
+
194
+ return record
195
+ }
196
+
197
+ public static func fromCKRecord(_ ckRecord: CKRecord) throws -> UserPreferencesSyncRecord {
198
+ guard let version = ckRecord["version"] as? Int,
199
+ let modifiedAt = ckRecord["modifiedAt"] as? Date,
200
+ let refreshInterval = ckRecord["refreshInterval"] as? Double,
201
+ let enabledProviders = ckRecord["enabledProviders"] as? [String],
202
+ let notificationsEnabled = ckRecord["notificationsEnabled"] as? Int,
203
+ let autoRefreshEnabled = ckRecord["autoRefreshEnabled"] as? Int,
204
+ let theme = ckRecord["theme"] as? String,
205
+ let displayFormat = ckRecord["displayFormat"] as? String
206
+ else {
207
+ throw SyncError.invalidRecordFormat("Missing required fields in UserPreferences")
208
+ }
209
+
210
+ return UserPreferencesSyncRecord(
211
+ recordID: ckRecord.recordID.recordName,
212
+ version: version,
213
+ modifiedAt: modifiedAt,
214
+ lastModifiedDeviceID: ckRecord["lastModifiedDeviceID"] as? String,
215
+ refreshInterval: refreshInterval,
216
+ enabledProviders: enabledProviders,
217
+ notificationsEnabled: notificationsEnabled == 1,
218
+ autoRefreshEnabled: autoRefreshEnabled == 1,
219
+ theme: theme,
220
+ displayFormat: displayFormat
221
+ )
222
+ }
223
+ }
224
+
225
+ // MARK: - Alert Configuration Sync Record
226
+
227
+ /// Syncable alert configuration record
228
+ ///
229
+ /// Stores alert thresholds and notification settings that should be
230
+ /// consistent across all user devices.
231
+ public struct AlertConfigurationSyncRecord: SyncableRecord {
232
+ public let recordID: String
233
+ public let recordType: String = CloudKitRecordType.alertConfiguration
234
+ public let version: Int
235
+ public let modifiedAt: Date
236
+ public let lastModifiedDeviceID: String?
237
+
238
+ // Alert settings
239
+ public let providerID: String
240
+ public let warningThreshold: Double
241
+ public let criticalThreshold: Double
242
+ public let notificationChannels: [String]
243
+ public let enabled: Bool
244
+
245
+ public init(
246
+ recordID: String = UUID().uuidString,
247
+ version: Int = 1,
248
+ modifiedAt: Date = Date(),
249
+ lastModifiedDeviceID: String? = nil,
250
+ providerID: String,
251
+ warningThreshold: Double = 0.75,
252
+ criticalThreshold: Double = 0.90,
253
+ notificationChannels: [String] = ["system"],
254
+ enabled: Bool = true
255
+ ) {
256
+ self.recordID = recordID
257
+ self.version = version
258
+ self.modifiedAt = modifiedAt
259
+ self.lastModifiedDeviceID = lastModifiedDeviceID
260
+ self.providerID = providerID
261
+ self.warningThreshold = warningThreshold
262
+ self.criticalThreshold = criticalThreshold
263
+ self.notificationChannels = notificationChannels
264
+ self.enabled = enabled
265
+ }
266
+
267
+ public func toCKRecord() throws -> CKRecord {
268
+ let record = CKRecord(recordType: recordType, recordID: CKRecord.ID(recordName: recordID))
269
+
270
+ record["version"] = version as CKRecordValue
271
+ record["modifiedAt"] = modifiedAt as CKRecordValue
272
+ record["lastModifiedDeviceID"] = lastModifiedDeviceID as? CKRecordValue
273
+ record["providerID"] = providerID as CKRecordValue
274
+ record["warningThreshold"] = warningThreshold as CKRecordValue
275
+ record["criticalThreshold"] = criticalThreshold as CKRecordValue
276
+ record["notificationChannels"] = notificationChannels as CKRecordValue
277
+ record["enabled"] = (enabled ? 1 : 0) as CKRecordValue
278
+
279
+ return record
280
+ }
281
+
282
+ public static func fromCKRecord(_ ckRecord: CKRecord) throws -> AlertConfigurationSyncRecord {
283
+ guard let version = ckRecord["version"] as? Int,
284
+ let modifiedAt = ckRecord["modifiedAt"] as? Date,
285
+ let providerID = ckRecord["providerID"] as? String,
286
+ let warningThreshold = ckRecord["warningThreshold"] as? Double,
287
+ let criticalThreshold = ckRecord["criticalThreshold"] as? Double,
288
+ let notificationChannels = ckRecord["notificationChannels"] as? [String],
289
+ let enabled = ckRecord["enabled"] as? Int
290
+ else {
291
+ throw SyncError.invalidRecordFormat("Missing required fields in AlertConfiguration")
292
+ }
293
+
294
+ return AlertConfigurationSyncRecord(
295
+ recordID: ckRecord.recordID.recordName,
296
+ version: version,
297
+ modifiedAt: modifiedAt,
298
+ lastModifiedDeviceID: ckRecord["lastModifiedDeviceID"] as? String,
299
+ providerID: providerID,
300
+ warningThreshold: warningThreshold,
301
+ criticalThreshold: criticalThreshold,
302
+ notificationChannels: notificationChannels,
303
+ enabled: enabled == 1
304
+ )
305
+ }
306
+ }
307
+
308
+ // MARK: - Encryption Helpers
309
+
310
+ /// Encrypts a string using AES-GCM encryption
311
+ ///
312
+ /// - Parameter plaintext: The string to encrypt
313
+ /// - Returns: Base64-encoded encrypted string with nonce prepended
314
+ /// - Throws: SyncError.encryptionFailed if encryption fails
315
+ private func encryptString(_ plaintext: String) throws -> String {
316
+ guard let data = plaintext.data(using: .utf8) else {
317
+ throw SyncError.encryptionFailed("Failed to convert string to data")
318
+ }
319
+
320
+ let key = getOrCreateEncryptionKey()
321
+ let sealed = try AES.GCM.seal(data, using: key)
322
+
323
+ guard let combined = sealed.combined else {
324
+ throw SyncError.encryptionFailed("Failed to create sealed box")
325
+ }
326
+
327
+ return combined.base64EncodedString()
328
+ }
329
+
330
+ /// Decrypts a string that was encrypted with encryptString
331
+ ///
332
+ /// - Parameter encrypted: Base64-encoded encrypted string
333
+ /// - Returns: Decrypted plaintext string
334
+ /// - Throws: SyncError.encryptionFailed if decryption fails
335
+ private func decryptString(_ encrypted: String) throws -> String {
336
+ guard let combined = Data(base64Encoded: encrypted) else {
337
+ throw SyncError.encryptionFailed("Failed to decode base64")
338
+ }
339
+
340
+ let key = getOrCreateEncryptionKey()
341
+ let sealedBox = try AES.GCM.SealedBox(combined: combined)
342
+ let decrypted = try AES.GCM.open(sealedBox, using: key)
343
+
344
+ guard let plaintext = String(data: decrypted, encoding: .utf8) else {
345
+ throw SyncError.encryptionFailed("Failed to convert decrypted data to string")
346
+ }
347
+
348
+ return plaintext
349
+ }
350
+
351
+ /// Retrieves or creates an encryption key for sensitive data
352
+ ///
353
+ /// The key is stored securely in the Keychain and persists across app launches.
354
+ ///
355
+ /// - Returns: SymmetricKey for AES-GCM encryption
356
+ private func getOrCreateEncryptionKey() -> SymmetricKey {
357
+ let keychainKey = "com.runic.sync.encryption.key"
358
+
359
+ // Try to load existing key from Keychain
360
+ if let keyData = loadFromKeychain(key: keychainKey) {
361
+ return SymmetricKey(data: keyData)
362
+ }
363
+
364
+ // Generate new key
365
+ let key = SymmetricKey(size: .bits256)
366
+ let keyData = key.withUnsafeBytes { Data($0) }
367
+
368
+ // Store in Keychain
369
+ saveToKeychain(key: keychainKey, data: keyData)
370
+
371
+ return key
372
+ }
373
+
374
+ private func saveToKeychain(key: String, data: Data) {
375
+ let query: [String: Any] = [
376
+ kSecClass as String: kSecClassGenericPassword,
377
+ kSecAttrService as String: "com.sriinnu.athena.Runic",
378
+ kSecAttrAccount as String: key,
379
+ kSecUseDataProtectionKeychain as String: true,
380
+ kSecValueData as String: data,
381
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
382
+ ]
383
+ SecItemDelete(query as CFDictionary)
384
+ SecItemAdd(query as CFDictionary, nil)
385
+ }
386
+
387
+ private func loadFromKeychain(key: String) -> Data? {
388
+ var query: [String: Any] = [
389
+ kSecClass as String: kSecClassGenericPassword,
390
+ kSecAttrService as String: "com.sriinnu.athena.Runic",
391
+ kSecAttrAccount as String: key,
392
+ kSecUseDataProtectionKeychain as String: true,
393
+ kSecUseAuthenticationUI as String: "kSecUseAuthenticationUIFail" as CFString,
394
+ kSecReturnData as String: true
395
+ ]
396
+ let authContext = LAContext()
397
+ authContext.interactionNotAllowed = true
398
+ query[kSecUseAuthenticationContext as String] = authContext
399
+ var result: AnyObject?
400
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
401
+ if status == errSecInteractionNotAllowed {
402
+ return nil
403
+ }
404
+ return status == errSecSuccess ? result as? Data : nil
405
+ }