@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,1702 @@
1
+ import AppKit
2
+ import RunicCore
3
+ import Observation
4
+ import ServiceManagement
5
+
6
+ enum RefreshFrequency: String, CaseIterable, Identifiable {
7
+ case manual
8
+ case oneMinute
9
+ case twoMinutes
10
+ case fiveMinutes
11
+ case fifteenMinutes
12
+
13
+ var id: String { self.rawValue }
14
+
15
+ var seconds: TimeInterval? {
16
+ switch self {
17
+ case .manual: nil
18
+ case .oneMinute: 60
19
+ case .twoMinutes: 120
20
+ case .fiveMinutes: 300
21
+ case .fifteenMinutes: 900
22
+ }
23
+ }
24
+
25
+ var label: String {
26
+ switch self {
27
+ case .manual: "Manual"
28
+ case .oneMinute: "1 min"
29
+ case .twoMinutes: "2 min"
30
+ case .fiveMinutes: "5 min"
31
+ case .fifteenMinutes: "15 min"
32
+ }
33
+ }
34
+ }
35
+
36
+ enum UsageMetricDisplayMode: String, CaseIterable, Identifiable {
37
+ case barsAndPercent
38
+ case barsOnly
39
+ case percentOnly
40
+
41
+ var id: String { self.rawValue }
42
+
43
+ var label: String {
44
+ switch self {
45
+ case .barsAndPercent: "Bars + %"
46
+ case .barsOnly: "Bars"
47
+ case .percentOnly: "%"
48
+ }
49
+ }
50
+
51
+ var showsBars: Bool {
52
+ switch self {
53
+ case .barsAndPercent, .barsOnly:
54
+ true
55
+ case .percentOnly:
56
+ false
57
+ }
58
+ }
59
+
60
+ var showsPercent: Bool {
61
+ switch self {
62
+ case .barsAndPercent, .percentOnly:
63
+ true
64
+ case .barsOnly:
65
+ false
66
+ }
67
+ }
68
+ }
69
+
70
+ enum MenuMode: String, CaseIterable, Identifiable {
71
+ case glance
72
+ case analyst
73
+ case `operator`
74
+
75
+ var id: String { self.rawValue }
76
+
77
+ var label: String {
78
+ switch self {
79
+ case .glance:
80
+ "Glance"
81
+ case .analyst:
82
+ "Analyst"
83
+ case .`operator`:
84
+ "Operator"
85
+ }
86
+ }
87
+ }
88
+
89
+ enum ChartStyle: String, CaseIterable, Identifiable {
90
+ case line
91
+ case area
92
+ case bar
93
+
94
+ var id: String { self.rawValue }
95
+
96
+ var label: String {
97
+ switch self {
98
+ case .line: return "Line"
99
+ case .area: return "Area"
100
+ case .bar: return "Bar"
101
+ }
102
+ }
103
+ }
104
+
105
+ enum NumberFormat: String, CaseIterable, Identifiable {
106
+ case abbreviated
107
+ case full
108
+
109
+ var id: String { self.rawValue }
110
+
111
+ var label: String {
112
+ switch self {
113
+ case .abbreviated: return "Abbreviated"
114
+ case .full: return "Full"
115
+ }
116
+ }
117
+ }
118
+
119
+ enum DateFormat: String, CaseIterable, Identifiable {
120
+ case relative
121
+ case absolute
122
+
123
+ var id: String { self.rawValue }
124
+
125
+ var label: String {
126
+ switch self {
127
+ case .relative: return "Relative"
128
+ case .absolute: return "Absolute"
129
+ }
130
+ }
131
+ }
132
+
133
+ enum Theme: String, CaseIterable, Identifiable {
134
+ case system
135
+ case light
136
+ case dark
137
+
138
+ var id: String { self.rawValue }
139
+
140
+ var label: String {
141
+ switch self {
142
+ case .system: return "System"
143
+ case .light: return "Light"
144
+ case .dark: return "Dark"
145
+ }
146
+ }
147
+ }
148
+
149
+ enum ProviderSwitcherLayout: String, CaseIterable, Identifiable {
150
+ case top
151
+ case sidebar
152
+
153
+ var id: String { self.rawValue }
154
+
155
+ var label: String {
156
+ switch self {
157
+ case .top: return "Top"
158
+ case .sidebar: return "Sidebar"
159
+ }
160
+ }
161
+ }
162
+
163
+ enum ProviderSwitcherIconSize: String, CaseIterable, Identifiable {
164
+ case small
165
+ case medium
166
+
167
+ var id: String { self.rawValue }
168
+
169
+ var label: String {
170
+ switch self {
171
+ case .small: return "Small"
172
+ case .medium: return "Medium"
173
+ }
174
+ }
175
+ }
176
+
177
+ @MainActor
178
+ @Observable
179
+ final class SettingsStore {
180
+ /// Persisted provider display order.
181
+ ///
182
+ /// Stored as raw `UsageProvider` strings so new providers can be appended automatically without breaking.
183
+ private var providerOrderRaw: [String] {
184
+ didSet { self.userDefaults.set(self.providerOrderRaw, forKey: "providerOrder") }
185
+ }
186
+
187
+ var refreshFrequency: RefreshFrequency {
188
+ didSet { self.userDefaults.set(self.refreshFrequency.rawValue, forKey: "refreshFrequency") }
189
+ }
190
+
191
+ var autoDisableRefreshWhenIdleEnabled: Bool {
192
+ didSet { self.userDefaults.set(self.autoDisableRefreshWhenIdleEnabled, forKey: "autoDisableRefreshWhenIdleEnabled") }
193
+ }
194
+
195
+ var autoDisableRefreshWhenIdleMinutes: Int {
196
+ didSet { self.userDefaults.set(self.autoDisableRefreshWhenIdleMinutes, forKey: "autoDisableRefreshWhenIdleMinutes") }
197
+ }
198
+
199
+ var autoDisableRefreshOnSleepEnabled: Bool {
200
+ didSet { self.userDefaults.set(self.autoDisableRefreshOnSleepEnabled, forKey: "autoDisableRefreshOnSleepEnabled") }
201
+ }
202
+
203
+ var autoRefreshWarningEnabled: Bool {
204
+ didSet { self.userDefaults.set(self.autoRefreshWarningEnabled, forKey: "autoRefreshWarningEnabled") }
205
+ }
206
+
207
+ var autoRefreshWarningThreshold: Int {
208
+ didSet { self.userDefaults.set(self.autoRefreshWarningThreshold, forKey: "autoRefreshWarningThreshold") }
209
+ }
210
+
211
+ var autoSuspendInactiveProvidersEnabled: Bool {
212
+ didSet {
213
+ self.userDefaults.set(self.autoSuspendInactiveProvidersEnabled, forKey: "autoSuspendInactiveProvidersEnabled")
214
+ }
215
+ }
216
+
217
+ var autoSuspendInactiveProvidersMinutes: Int {
218
+ didSet {
219
+ self.userDefaults.set(self.autoSuspendInactiveProvidersMinutes, forKey: "autoSuspendInactiveProvidersMinutes")
220
+ }
221
+ }
222
+
223
+ var launchAtLogin: Bool {
224
+ didSet {
225
+ self.userDefaults.set(self.launchAtLogin, forKey: "launchAtLogin")
226
+ LaunchAtLoginManager.setEnabled(self.launchAtLogin)
227
+ }
228
+ }
229
+
230
+ /// Hidden toggle to reveal debug-only menu items (enable via defaults write com.sriinnu.athena.Runic debugMenuEnabled
231
+ /// -bool YES).
232
+ var debugMenuEnabled: Bool {
233
+ didSet { self.userDefaults.set(self.debugMenuEnabled, forKey: "debugMenuEnabled") }
234
+ }
235
+
236
+ private var debugLoadingPatternRaw: String? {
237
+ didSet {
238
+ if let raw = self.debugLoadingPatternRaw {
239
+ self.userDefaults.set(raw, forKey: "debugLoadingPattern")
240
+ } else {
241
+ self.userDefaults.removeObject(forKey: "debugLoadingPattern")
242
+ }
243
+ }
244
+ }
245
+
246
+ var statusChecksEnabled: Bool {
247
+ didSet { self.userDefaults.set(self.statusChecksEnabled, forKey: "statusChecksEnabled") }
248
+ }
249
+
250
+ var sessionQuotaNotificationsEnabled: Bool {
251
+ didSet {
252
+ self.userDefaults.set(self.sessionQuotaNotificationsEnabled, forKey: "sessionQuotaNotificationsEnabled")
253
+ }
254
+ }
255
+
256
+ /// When enabled, post macOS notifications when spend forecasts breach project budgets.
257
+ var budgetNotificationsEnabled: Bool {
258
+ didSet {
259
+ self.userDefaults.set(self.budgetNotificationsEnabled, forKey: "budgetNotificationsEnabled")
260
+ }
261
+ }
262
+
263
+ /// When enabled, progress bars show "percent used" instead of "percent left".
264
+ var usageBarsShowUsed: Bool {
265
+ didSet { self.userDefaults.set(self.usageBarsShowUsed, forKey: "usageBarsShowUsed") }
266
+ }
267
+
268
+ var usageMetricDisplayMode: UsageMetricDisplayMode {
269
+ didSet {
270
+ self.userDefaults.set(self.usageMetricDisplayMode.rawValue, forKey: "usageMetricDisplayMode")
271
+ }
272
+ }
273
+
274
+ var menuMode: MenuMode {
275
+ didSet {
276
+ self.userDefaults.set(self.menuMode.rawValue, forKey: "menuMode")
277
+ }
278
+ }
279
+
280
+ var chartStyle: ChartStyle {
281
+ didSet { self.userDefaults.set(self.chartStyle.rawValue, forKey: "chartStyle") }
282
+ }
283
+
284
+ var numberFormat: NumberFormat {
285
+ didSet { self.userDefaults.set(self.numberFormat.rawValue, forKey: "numberFormat") }
286
+ }
287
+
288
+ var dateFormat: DateFormat {
289
+ didSet { self.userDefaults.set(self.dateFormat.rawValue, forKey: "dateFormat") }
290
+ }
291
+
292
+ var theme: Theme {
293
+ didSet {
294
+ self.userDefaults.set(self.theme.rawValue, forKey: "theme")
295
+ RunicApp.applyTheme(self.theme)
296
+ }
297
+ }
298
+
299
+ /// Optional: use provider branding icons with a percentage in the menu bar.
300
+ var menuBarShowsBrandIconWithPercent: Bool {
301
+ didSet {
302
+ self.userDefaults.set(self.menuBarShowsBrandIconWithPercent, forKey: "menuBarShowsBrandIconWithPercent")
303
+ }
304
+ }
305
+
306
+ /// Optional: render the menu bar icon with a vibrant, data-reactive color.
307
+ var menuBarVibrantIconEnabled: Bool {
308
+ didSet {
309
+ self.userDefaults.set(self.menuBarVibrantIconEnabled, forKey: "menuBarVibrantIconEnabled")
310
+ }
311
+ }
312
+
313
+ /// Optional: show provider cost summary from local usage logs (Codex + Claude).
314
+ var costUsageEnabled: Bool {
315
+ didSet { self.userDefaults.set(self.costUsageEnabled, forKey: "tokenCostUsageEnabled") }
316
+ }
317
+
318
+ /// Optional: limit how many insight rows appear in the menu before "More…".
319
+ var insightsMenuMaxItems: Int {
320
+ didSet { self.userDefaults.set(self.insightsMenuMaxItems, forKey: "insightsMenuMaxItems") }
321
+ }
322
+
323
+ /// Optional: how many days to include in the insights report.
324
+ var insightsReportDays: Int {
325
+ didSet { self.userDefaults.set(self.insightsReportDays, forKey: "insightsReportDays") }
326
+ }
327
+
328
+ var randomBlinkEnabled: Bool {
329
+ didSet { self.userDefaults.set(self.randomBlinkEnabled, forKey: "randomBlinkEnabled") }
330
+ }
331
+
332
+ /// Optional: augment Claude usage with claude.ai web API (via browser cookies),
333
+ /// incl. "Extra usage" spend.
334
+ var claudeWebExtrasEnabled: Bool {
335
+ didSet { self.userDefaults.set(self.claudeWebExtrasEnabled, forKey: "claudeWebExtrasEnabled") }
336
+ }
337
+
338
+ /// Optional: show Codex credits + Claude extra usage sections in the menu UI.
339
+ var showOptionalCreditsAndExtraUsage: Bool {
340
+ didSet {
341
+ self.userDefaults.set(self.showOptionalCreditsAndExtraUsage, forKey: "showOptionalCreditsAndExtraUsage")
342
+ }
343
+ }
344
+
345
+ /// Optional: fetch OpenAI web dashboard extras for Codex (browser cookies).
346
+ var openAIWebAccessEnabled: Bool {
347
+ didSet { self.userDefaults.set(self.openAIWebAccessEnabled, forKey: "openAIWebAccessEnabled") }
348
+ }
349
+
350
+ private var codexUsageDataSourceRaw: String? {
351
+ didSet {
352
+ if let raw = self.codexUsageDataSourceRaw {
353
+ self.userDefaults.set(raw, forKey: "codexUsageDataSource")
354
+ } else {
355
+ self.userDefaults.removeObject(forKey: "codexUsageDataSource")
356
+ }
357
+ }
358
+ }
359
+
360
+ private var claudeUsageDataSourceRaw: String? {
361
+ didSet {
362
+ if let raw = self.claudeUsageDataSourceRaw {
363
+ self.userDefaults.set(raw, forKey: "claudeUsageDataSource")
364
+ } else {
365
+ self.userDefaults.removeObject(forKey: "claudeUsageDataSource")
366
+ }
367
+ }
368
+ }
369
+
370
+ /// Optional: collapse provider icons into a single menu bar item with an in-menu switcher.
371
+ var mergeIcons: Bool {
372
+ didSet { self.userDefaults.set(self.mergeIcons, forKey: "mergeIcons") }
373
+ }
374
+
375
+ /// Optional: show provider icons in the in-menu switcher.
376
+ var switcherShowsIcons: Bool {
377
+ didSet { self.userDefaults.set(self.switcherShowsIcons, forKey: "switcherShowsIcons") }
378
+ }
379
+
380
+ /// Optional: position the provider switcher either on top or in a left sidebar.
381
+ var providerSwitcherLayout: ProviderSwitcherLayout {
382
+ didSet {
383
+ self.userDefaults.set(self.providerSwitcherLayout.rawValue, forKey: "providerSwitcherLayout")
384
+ }
385
+ }
386
+
387
+ /// Optional: size of provider icons in the switcher.
388
+ var providerSwitcherIconSize: ProviderSwitcherIconSize {
389
+ didSet {
390
+ self.userDefaults.set(self.providerSwitcherIconSize.rawValue, forKey: "providerSwitcherIconSize")
391
+ }
392
+ }
393
+
394
+ /// Show the built-in providers pane as a sidebar (true) or flat list (false).
395
+ var providersPaneSidebar: Bool {
396
+ didSet { self.userDefaults.set(self.providersPaneSidebar, forKey: "providersPaneSidebar") }
397
+ }
398
+
399
+ /// z.ai API token (stored in Keychain).
400
+ var zaiAPIToken: String {
401
+ didSet {
402
+ self.schedulePersistZaiAPIToken()
403
+ if !zaiAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
404
+ self.autoEnableProviderIfNeeded(cliName: "zai")
405
+ }
406
+ }
407
+ }
408
+
409
+ /// MiniMax API token (stored in Keychain).
410
+ var minimaxAPIToken: String {
411
+ didSet {
412
+ self.schedulePersistMiniMaxAPIToken()
413
+ if !minimaxAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
414
+ self.autoEnableProviderIfNeeded(cliName: "minimax")
415
+ }
416
+ }
417
+ }
418
+
419
+ /// MiniMax manual Cookie header (stored in Keychain).
420
+ var minimaxCookieHeader: String {
421
+ didSet {
422
+ self.schedulePersistMiniMaxCookieHeader()
423
+ if !minimaxCookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
424
+ self.autoEnableProviderIfNeeded(cliName: "minimax")
425
+ }
426
+ }
427
+ }
428
+
429
+ /// MiniMax Group ID (stored in Keychain).
430
+ var minimaxGroupID: String {
431
+ didSet { self.schedulePersistMiniMaxGroupID() }
432
+ }
433
+
434
+ /// Copilot API token (stored in Keychain).
435
+ var copilotAPIToken: String {
436
+ didSet {
437
+ self.schedulePersistCopilotAPIToken()
438
+ if !copilotAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
439
+ self.autoEnableProviderIfNeeded(cliName: "copilot")
440
+ }
441
+ }
442
+ }
443
+
444
+ /// OpenRouter API key (stored in Keychain).
445
+ var openRouterAPIToken: String {
446
+ didSet {
447
+ self.schedulePersistOpenRouterAPIToken()
448
+ if !openRouterAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
449
+ self.autoEnableProviderIfNeeded(cliName: "openrouter")
450
+ }
451
+ }
452
+ }
453
+
454
+ /// Groq API key (stored in Keychain).
455
+ var groqAPIToken: String {
456
+ didSet {
457
+ self.schedulePersistGroqAPIToken()
458
+ if !groqAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
459
+ self.autoEnableProviderIfNeeded(cliName: "groq")
460
+ }
461
+ }
462
+ }
463
+
464
+ /// DeepSeek API key (stored in Keychain).
465
+ var deepSeekAPIToken: String {
466
+ didSet {
467
+ self.schedulePersistDeepSeekAPIToken()
468
+ if !deepSeekAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
469
+ self.autoEnableProviderIfNeeded(cliName: "deepseek")
470
+ }
471
+ }
472
+ }
473
+
474
+ /// Fireworks API key (stored in Keychain).
475
+ var fireworksAPIToken: String {
476
+ didSet {
477
+ self.schedulePersistFireworksAPIToken()
478
+ if !fireworksAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
479
+ self.autoEnableProviderIfNeeded(cliName: "fireworks")
480
+ }
481
+ }
482
+ }
483
+
484
+ /// Mistral API key (stored in Keychain).
485
+ var mistralAPIToken: String {
486
+ didSet {
487
+ self.schedulePersistMistralAPIToken()
488
+ if !mistralAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
489
+ self.autoEnableProviderIfNeeded(cliName: "mistral")
490
+ }
491
+ }
492
+ }
493
+
494
+ /// Perplexity API key (stored in Keychain).
495
+ var perplexityAPIToken: String {
496
+ didSet {
497
+ self.schedulePersistPerplexityAPIToken()
498
+ if !perplexityAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
499
+ self.autoEnableProviderIfNeeded(cliName: "perplexity")
500
+ }
501
+ }
502
+ }
503
+
504
+ /// Kimi API key (stored in Keychain).
505
+ var kimiAPIToken: String {
506
+ didSet {
507
+ self.schedulePersistKimiAPIToken()
508
+ if !kimiAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
509
+ self.autoEnableProviderIfNeeded(cliName: "kimi")
510
+ }
511
+ }
512
+ }
513
+
514
+ /// Auggie API token (stored in Keychain).
515
+ var auggieAPIToken: String {
516
+ didSet {
517
+ self.schedulePersistAuggieAPIToken()
518
+ if !auggieAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
519
+ self.autoEnableProviderIfNeeded(cliName: "auggie")
520
+ }
521
+ }
522
+ }
523
+
524
+ /// Together API key (stored in Keychain).
525
+ var togetherAPIToken: String {
526
+ didSet {
527
+ self.schedulePersistTogetherAPIToken()
528
+ if !togetherAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
529
+ self.autoEnableProviderIfNeeded(cliName: "together")
530
+ }
531
+ }
532
+ }
533
+
534
+ /// Cohere API key (stored in Keychain).
535
+ var cohereAPIToken: String {
536
+ didSet {
537
+ self.schedulePersistCohereAPIToken()
538
+ if !cohereAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
539
+ self.autoEnableProviderIfNeeded(cliName: "cohere")
540
+ }
541
+ }
542
+ }
543
+
544
+ /// xAI API key (stored in Keychain).
545
+ var xaiAPIToken: String {
546
+ didSet {
547
+ self.schedulePersistXAiAPIToken()
548
+ if !xaiAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
549
+ self.autoEnableProviderIfNeeded(cliName: "xai")
550
+ }
551
+ }
552
+ }
553
+
554
+ /// Cerebras API key (stored in Keychain).
555
+ var cerebrasAPIToken: String {
556
+ didSet {
557
+ self.schedulePersistCerebrasAPIToken()
558
+ if !cerebrasAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
559
+ self.autoEnableProviderIfNeeded(cliName: "cerebras")
560
+ }
561
+ }
562
+ }
563
+
564
+ /// Qwen DashScope API key (stored in Keychain).
565
+ var qwenAPIToken: String {
566
+ didSet {
567
+ self.schedulePersistQwenAPIToken()
568
+ if !qwenAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
569
+ self.autoEnableProviderIfNeeded(cliName: "qwen")
570
+ }
571
+ }
572
+ }
573
+
574
+ /// SambaNova API key (stored in Keychain).
575
+ var sambaNovaAPIToken: String {
576
+ didSet {
577
+ self.schedulePersistSambaNovaAPIToken()
578
+ if !sambaNovaAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
579
+ self.autoEnableProviderIfNeeded(cliName: "sambanova")
580
+ }
581
+ }
582
+ }
583
+
584
+ /// Azure OpenAI endpoint URL (stored in UserDefaults).
585
+ var azureOpenAIEndpoint: String {
586
+ didSet { self.userDefaults.set(self.azureOpenAIEndpoint, forKey: "azureOpenAIEndpoint") }
587
+ }
588
+
589
+ /// Azure OpenAI deployment name (stored in UserDefaults).
590
+ var azureOpenAIDeployment: String {
591
+ didSet { self.userDefaults.set(self.azureOpenAIDeployment, forKey: "azureOpenAIDeployment") }
592
+ }
593
+
594
+ /// Azure OpenAI API version (stored in UserDefaults).
595
+ var azureOpenAIAPIVersion: String {
596
+ didSet { self.userDefaults.set(self.azureOpenAIAPIVersion, forKey: "azureOpenAIAPIVersion") }
597
+ }
598
+
599
+ /// Azure OpenAI API key (stored in Keychain).
600
+ var azureOpenAIAPIToken: String {
601
+ didSet {
602
+ self.schedulePersistAzureOpenAIAPIToken()
603
+ if !azureOpenAIAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
604
+ self.autoEnableProviderIfNeeded(cliName: "azure")
605
+ }
606
+ }
607
+ }
608
+
609
+ /// Amazon Bedrock region (stored in UserDefaults).
610
+ var bedrockRegion: String {
611
+ didSet { self.userDefaults.set(self.bedrockRegion, forKey: "bedrockRegion") }
612
+ }
613
+
614
+ /// Optional AWS profile for Amazon Bedrock (stored in UserDefaults).
615
+ var bedrockAWSProfile: String {
616
+ didSet { self.userDefaults.set(self.bedrockAWSProfile, forKey: "bedrockAWSProfile") }
617
+ }
618
+
619
+ /// Optional model filter for Amazon Bedrock (stored in UserDefaults).
620
+ var bedrockModelID: String {
621
+ didSet { self.userDefaults.set(self.bedrockModelID, forKey: "bedrockModelID") }
622
+ }
623
+
624
+ /// Google Cloud project for Vertex AI (stored in UserDefaults).
625
+ var vertexaiProject: String {
626
+ didSet { self.userDefaults.set(self.vertexaiProject, forKey: "vertexaiProject") }
627
+ }
628
+
629
+ /// Google Cloud location/region for Vertex AI (stored in UserDefaults).
630
+ var vertexaiLocation: String {
631
+ didSet { self.userDefaults.set(self.vertexaiLocation, forKey: "vertexaiLocation") }
632
+ }
633
+
634
+ private var selectedMenuProviderRaw: String? {
635
+ didSet {
636
+ if let raw = self.selectedMenuProviderRaw {
637
+ self.userDefaults.set(raw, forKey: "selectedMenuProvider")
638
+ } else {
639
+ self.userDefaults.removeObject(forKey: "selectedMenuProvider")
640
+ }
641
+ }
642
+ }
643
+
644
+ /// Optional override for the loading animation pattern, exposed via the Debug tab.
645
+ var debugLoadingPattern: LoadingPattern? {
646
+ get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) }
647
+ set {
648
+ self.debugLoadingPatternRaw = newValue?.rawValue
649
+ }
650
+ }
651
+
652
+ var selectedMenuProvider: UsageProvider? {
653
+ get { self.selectedMenuProviderRaw.flatMap(UsageProvider.init(rawValue:)) }
654
+ set {
655
+ self.selectedMenuProviderRaw = newValue?.rawValue
656
+ }
657
+ }
658
+
659
+ var codexUsageDataSource: CodexUsageDataSource {
660
+ get { CodexUsageDataSource(rawValue: self.codexUsageDataSourceRaw ?? "") ?? .oauth }
661
+ set {
662
+ self.codexUsageDataSourceRaw = newValue.rawValue
663
+ }
664
+ }
665
+
666
+ var claudeUsageDataSource: ClaudeUsageDataSource {
667
+ get { ClaudeUsageDataSource(rawValue: self.claudeUsageDataSourceRaw ?? "") ?? .oauth }
668
+ set {
669
+ self.claudeUsageDataSourceRaw = newValue.rawValue
670
+ if newValue != .cli {
671
+ self.claudeWebExtrasEnabled = false
672
+ }
673
+ }
674
+ }
675
+
676
+ var menuObservationToken: Int {
677
+ _ = self.providerOrderRaw
678
+ _ = self.refreshFrequency
679
+ _ = self.autoDisableRefreshWhenIdleEnabled
680
+ _ = self.autoDisableRefreshWhenIdleMinutes
681
+ _ = self.autoDisableRefreshOnSleepEnabled
682
+ _ = self.autoRefreshWarningEnabled
683
+ _ = self.autoRefreshWarningThreshold
684
+ _ = self.autoSuspendInactiveProvidersEnabled
685
+ _ = self.autoSuspendInactiveProvidersMinutes
686
+ _ = self.launchAtLogin
687
+ _ = self.debugMenuEnabled
688
+ _ = self.statusChecksEnabled
689
+ _ = self.sessionQuotaNotificationsEnabled
690
+ _ = self.usageBarsShowUsed
691
+ _ = self.usageMetricDisplayMode
692
+ _ = self.menuMode
693
+ _ = self.chartStyle
694
+ _ = self.numberFormat
695
+ _ = self.dateFormat
696
+ _ = self.theme
697
+ _ = self.menuBarShowsBrandIconWithPercent
698
+ _ = self.menuBarVibrantIconEnabled
699
+ _ = self.costUsageEnabled
700
+ _ = self.insightsMenuMaxItems
701
+ _ = self.insightsReportDays
702
+ _ = self.randomBlinkEnabled
703
+ _ = self.claudeWebExtrasEnabled
704
+ _ = self.showOptionalCreditsAndExtraUsage
705
+ _ = self.openAIWebAccessEnabled
706
+ _ = self.codexUsageDataSource
707
+ _ = self.claudeUsageDataSource
708
+ _ = self.mergeIcons
709
+ _ = self.switcherShowsIcons
710
+ _ = self.providerSwitcherLayout
711
+ _ = self.providerSwitcherIconSize
712
+ _ = self.zaiAPIToken
713
+ _ = self.minimaxAPIToken
714
+ _ = self.minimaxCookieHeader
715
+ _ = self.minimaxGroupID
716
+ _ = self.copilotAPIToken
717
+ _ = self.openRouterAPIToken
718
+ _ = self.groqAPIToken
719
+ _ = self.deepSeekAPIToken
720
+ _ = self.fireworksAPIToken
721
+ _ = self.mistralAPIToken
722
+ _ = self.perplexityAPIToken
723
+ _ = self.kimiAPIToken
724
+ _ = self.auggieAPIToken
725
+ _ = self.togetherAPIToken
726
+ _ = self.cohereAPIToken
727
+ _ = self.xaiAPIToken
728
+ _ = self.cerebrasAPIToken
729
+ _ = self.qwenAPIToken
730
+ _ = self.sambaNovaAPIToken
731
+ _ = self.azureOpenAIEndpoint
732
+ _ = self.azureOpenAIDeployment
733
+ _ = self.azureOpenAIAPIVersion
734
+ _ = self.azureOpenAIAPIToken
735
+ _ = self.bedrockRegion
736
+ _ = self.bedrockAWSProfile
737
+ _ = self.bedrockModelID
738
+ _ = self.vertexaiProject
739
+ _ = self.vertexaiLocation
740
+ _ = self.debugLoadingPattern
741
+ _ = self.selectedMenuProvider
742
+ _ = self.providerToggleRevision
743
+ return 0
744
+ }
745
+
746
+ private var providerDetectionCompleted: Bool {
747
+ didSet { self.userDefaults.set(self.providerDetectionCompleted, forKey: "providerDetectionCompleted") }
748
+ }
749
+
750
+ @ObservationIgnored private let userDefaults: UserDefaults
751
+ @ObservationIgnored private let toggleStore: ProviderToggleStore
752
+ @ObservationIgnored private let zaiTokenStore: any ZaiTokenStoring
753
+ @ObservationIgnored private var zaiTokenPersistTask: Task<Void, Never>?
754
+ @ObservationIgnored private let minimaxTokenStore: any MiniMaxTokenStoring
755
+ @ObservationIgnored private var minimaxTokenPersistTask: Task<Void, Never>?
756
+ @ObservationIgnored private let minimaxCookieHeaderStore: any MiniMaxCookieHeaderStoring
757
+ @ObservationIgnored private var minimaxCookieHeaderPersistTask: Task<Void, Never>?
758
+ @ObservationIgnored private let minimaxGroupIDStore: any MiniMaxGroupIDStoring
759
+ @ObservationIgnored private var minimaxGroupIDPersistTask: Task<Void, Never>?
760
+ @ObservationIgnored private let copilotTokenStore: any CopilotTokenStoring
761
+ @ObservationIgnored private var copilotTokenPersistTask: Task<Void, Never>?
762
+ @ObservationIgnored private let openRouterTokenStore: any OpenRouterTokenStoring
763
+ @ObservationIgnored private var openRouterTokenPersistTask: Task<Void, Never>?
764
+ @ObservationIgnored private let groqTokenStore: any GroqTokenStoring
765
+ @ObservationIgnored private var groqTokenPersistTask: Task<Void, Never>?
766
+ @ObservationIgnored private let deepSeekTokenStore: any DeepSeekTokenStoring
767
+ @ObservationIgnored private var deepSeekTokenPersistTask: Task<Void, Never>?
768
+ @ObservationIgnored private let fireworksTokenStore: any FireworksTokenStoring
769
+ @ObservationIgnored private var fireworksTokenPersistTask: Task<Void, Never>?
770
+ @ObservationIgnored private let mistralTokenStore: any MistralTokenStoring
771
+ @ObservationIgnored private var mistralTokenPersistTask: Task<Void, Never>?
772
+ @ObservationIgnored private let perplexityTokenStore: any PerplexityTokenStoring
773
+ @ObservationIgnored private var perplexityTokenPersistTask: Task<Void, Never>?
774
+ @ObservationIgnored private let kimiTokenStore: any KimiTokenStoring
775
+ @ObservationIgnored private var kimiTokenPersistTask: Task<Void, Never>?
776
+ @ObservationIgnored private let auggieTokenStore: any AuggieTokenStoring
777
+ @ObservationIgnored private var auggieTokenPersistTask: Task<Void, Never>?
778
+ @ObservationIgnored private let togetherTokenStore: any TogetherTokenStoring
779
+ @ObservationIgnored private var togetherTokenPersistTask: Task<Void, Never>?
780
+ @ObservationIgnored private let cohereTokenStore: any CohereTokenStoring
781
+ @ObservationIgnored private var cohereTokenPersistTask: Task<Void, Never>?
782
+ @ObservationIgnored private let xaiTokenStore: any XAITokenStoring
783
+ @ObservationIgnored private var xaiTokenPersistTask: Task<Void, Never>?
784
+ @ObservationIgnored private let cerebrasTokenStore: any CerebrasTokenStoring
785
+ @ObservationIgnored private var cerebrasTokenPersistTask: Task<Void, Never>?
786
+ @ObservationIgnored private let sambaNovaTokenStore: any SambaNovaTokenStoring
787
+ @ObservationIgnored private var sambaNovaTokenPersistTask: Task<Void, Never>?
788
+ @ObservationIgnored private let qwenTokenStore: any QwenTokenStoring
789
+ @ObservationIgnored private var qwenTokenPersistTask: Task<Void, Never>?
790
+ @ObservationIgnored private let azureOpenAITokenStore: any AzureOpenAITokenStoring
791
+ @ObservationIgnored private var azureOpenAITokenPersistTask: Task<Void, Never>?
792
+ // Cache enablement so tight UI loops (menu bar animations) don't hit UserDefaults each tick.
793
+ @ObservationIgnored private var cachedProviderEnablement: [UsageProvider: Bool] = [:]
794
+ @ObservationIgnored private var cachedProviderEnablementRevision: Int = -1
795
+ @ObservationIgnored private var cachedEnabledProviders: [UsageProvider] = []
796
+ @ObservationIgnored private var cachedEnabledProvidersRevision: Int = -1
797
+ @ObservationIgnored private var cachedEnabledProvidersOrderRaw: [String] = []
798
+ // Cache order to avoid re-building sets/arrays every animation tick.
799
+ @ObservationIgnored private var cachedProviderOrder: [UsageProvider] = []
800
+ @ObservationIgnored private var cachedProviderOrderRaw: [String] = []
801
+ private var providerToggleRevision: Int = 0
802
+
803
+ init(
804
+ userDefaults: UserDefaults = .standard,
805
+ zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore(),
806
+ minimaxTokenStore: any MiniMaxTokenStoring = KeychainMiniMaxTokenStore(),
807
+ minimaxCookieHeaderStore: any MiniMaxCookieHeaderStoring = KeychainMiniMaxCookieHeaderStore(),
808
+ minimaxGroupIDStore: any MiniMaxGroupIDStoring = KeychainMiniMaxGroupIDStore(),
809
+ copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(),
810
+ openRouterTokenStore: any OpenRouterTokenStoring = KeychainOpenRouterTokenStore(),
811
+ groqTokenStore: any GroqTokenStoring = KeychainGroqTokenStore(),
812
+ deepSeekTokenStore: any DeepSeekTokenStoring = KeychainDeepSeekTokenStore(),
813
+ fireworksTokenStore: any FireworksTokenStoring = KeychainFireworksTokenStore(),
814
+ mistralTokenStore: any MistralTokenStoring = KeychainMistralTokenStore(),
815
+ perplexityTokenStore: any PerplexityTokenStoring = KeychainPerplexityTokenStore(),
816
+ kimiTokenStore: any KimiTokenStoring = KeychainKimiTokenStore(),
817
+ auggieTokenStore: any AuggieTokenStoring = KeychainAuggieTokenStore(),
818
+ togetherTokenStore: any TogetherTokenStoring = KeychainTogetherTokenStore(),
819
+ cohereTokenStore: any CohereTokenStoring = KeychainCohereTokenStore(),
820
+ xaiTokenStore: any XAITokenStoring = KeychainXAITokenStore(),
821
+ cerebrasTokenStore: any CerebrasTokenStoring = KeychainCerebrasTokenStore(),
822
+ sambaNovaTokenStore: any SambaNovaTokenStoring = KeychainSambaNovaTokenStore(),
823
+ qwenTokenStore: any QwenTokenStoring = KeychainQwenTokenStore(),
824
+ azureOpenAITokenStore: any AzureOpenAITokenStoring = KeychainAzureOpenAITokenStore())
825
+ {
826
+ self.userDefaults = userDefaults
827
+ self.zaiTokenStore = zaiTokenStore
828
+ self.minimaxTokenStore = minimaxTokenStore
829
+ self.minimaxCookieHeaderStore = minimaxCookieHeaderStore
830
+ self.minimaxGroupIDStore = minimaxGroupIDStore
831
+ self.copilotTokenStore = copilotTokenStore
832
+ self.openRouterTokenStore = openRouterTokenStore
833
+ self.groqTokenStore = groqTokenStore
834
+ self.deepSeekTokenStore = deepSeekTokenStore
835
+ self.fireworksTokenStore = fireworksTokenStore
836
+ self.mistralTokenStore = mistralTokenStore
837
+ self.perplexityTokenStore = perplexityTokenStore
838
+ self.kimiTokenStore = kimiTokenStore
839
+ self.auggieTokenStore = auggieTokenStore
840
+ self.togetherTokenStore = togetherTokenStore
841
+ self.cohereTokenStore = cohereTokenStore
842
+ self.xaiTokenStore = xaiTokenStore
843
+ self.cerebrasTokenStore = cerebrasTokenStore
844
+ self.sambaNovaTokenStore = sambaNovaTokenStore
845
+ self.qwenTokenStore = qwenTokenStore
846
+ self.azureOpenAITokenStore = azureOpenAITokenStore
847
+ self.providerOrderRaw = userDefaults.stringArray(forKey: "providerOrder") ?? []
848
+ let raw = userDefaults.string(forKey: "refreshFrequency") ?? RefreshFrequency.manual.rawValue
849
+ self.refreshFrequency = RefreshFrequency(rawValue: raw) ?? .manual
850
+ self.autoDisableRefreshWhenIdleEnabled = userDefaults.object(
851
+ forKey: "autoDisableRefreshWhenIdleEnabled") as? Bool ?? true
852
+ self.autoDisableRefreshWhenIdleMinutes = userDefaults.object(
853
+ forKey: "autoDisableRefreshWhenIdleMinutes") as? Int ?? 5
854
+ self.autoDisableRefreshOnSleepEnabled = userDefaults.object(
855
+ forKey: "autoDisableRefreshOnSleepEnabled") as? Bool ?? true
856
+ self.autoRefreshWarningEnabled = userDefaults.object(
857
+ forKey: "autoRefreshWarningEnabled") as? Bool ?? true
858
+ self.autoRefreshWarningThreshold = userDefaults.object(
859
+ forKey: "autoRefreshWarningThreshold") as? Int ?? 10
860
+ self.autoSuspendInactiveProvidersEnabled = userDefaults.object(
861
+ forKey: "autoSuspendInactiveProvidersEnabled") as? Bool ?? true
862
+ self.autoSuspendInactiveProvidersMinutes = userDefaults.object(
863
+ forKey: "autoSuspendInactiveProvidersMinutes") as? Int ?? 720
864
+ self.launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false
865
+ self.debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false
866
+ self.debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern")
867
+ self.statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true
868
+ let sessionQuotaNotificationsDefault = userDefaults.object(
869
+ forKey: "sessionQuotaNotificationsEnabled") as? Bool
870
+ self.sessionQuotaNotificationsEnabled = sessionQuotaNotificationsDefault ?? true
871
+ if sessionQuotaNotificationsDefault == nil {
872
+ self.userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled")
873
+ }
874
+ self.budgetNotificationsEnabled = userDefaults.object(
875
+ forKey: "budgetNotificationsEnabled") as? Bool ?? false
876
+ self.usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? true
877
+ let metricDisplayRaw = userDefaults.string(forKey: "usageMetricDisplayMode")
878
+ self.usageMetricDisplayMode = UsageMetricDisplayMode(rawValue: metricDisplayRaw ?? "") ?? .barsAndPercent
879
+ let menuModeRaw = userDefaults.string(forKey: "menuMode")
880
+ self.menuMode = MenuMode(rawValue: menuModeRaw ?? "") ?? .`operator`
881
+ let chartStyleRaw = userDefaults.string(forKey: "chartStyle")
882
+ self.chartStyle = ChartStyle(rawValue: chartStyleRaw ?? "") ?? .line
883
+ let numberFormatRaw = userDefaults.string(forKey: "numberFormat")
884
+ self.numberFormat = NumberFormat(rawValue: numberFormatRaw ?? "") ?? .abbreviated
885
+ let dateFormatRaw = userDefaults.string(forKey: "dateFormat")
886
+ self.dateFormat = DateFormat(rawValue: dateFormatRaw ?? "") ?? .relative
887
+ let themeRaw = userDefaults.string(forKey: "theme")
888
+ self.theme = Theme(rawValue: themeRaw ?? "") ?? .system
889
+ self.menuBarShowsBrandIconWithPercent = userDefaults.object(
890
+ forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false
891
+ self.menuBarVibrantIconEnabled = userDefaults.object(
892
+ forKey: "menuBarVibrantIconEnabled") as? Bool ?? true
893
+ self.costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? true
894
+ self.insightsMenuMaxItems = userDefaults.object(forKey: "insightsMenuMaxItems") as? Int ?? 4
895
+ self.insightsReportDays = userDefaults.object(forKey: "insightsReportDays") as? Int ?? 7
896
+ self.randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false
897
+ self.claudeWebExtrasEnabled = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false
898
+ let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool
899
+ self.showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true
900
+ if creditsExtrasDefault == nil {
901
+ self.userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage")
902
+ }
903
+ let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool
904
+ self.openAIWebAccessEnabled = openAIWebAccessDefault ?? true
905
+ if openAIWebAccessDefault == nil {
906
+ self.userDefaults.set(true, forKey: "openAIWebAccessEnabled")
907
+ }
908
+ let codexSourceRaw = userDefaults.string(forKey: "codexUsageDataSource")
909
+ self.codexUsageDataSourceRaw = codexSourceRaw ?? CodexUsageDataSource.oauth.rawValue
910
+ let claudeSourceRaw = userDefaults.string(forKey: "claudeUsageDataSource")
911
+ self.claudeUsageDataSourceRaw = claudeSourceRaw ?? ClaudeUsageDataSource.oauth.rawValue
912
+ self.mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true
913
+ self.switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true
914
+ let layoutRaw = userDefaults.string(forKey: "providerSwitcherLayout")
915
+ self.providerSwitcherLayout = ProviderSwitcherLayout(rawValue: layoutRaw ?? "") ?? .top
916
+ let iconSizeRaw = userDefaults.string(forKey: "providerSwitcherIconSize")
917
+ self.providerSwitcherIconSize = ProviderSwitcherIconSize(rawValue: iconSizeRaw ?? "") ?? .medium
918
+ self.providersPaneSidebar = userDefaults.object(forKey: "providersPaneSidebar") as? Bool ?? false
919
+ self.azureOpenAIEndpoint = userDefaults.string(forKey: "azureOpenAIEndpoint") ?? ""
920
+ self.azureOpenAIDeployment = userDefaults.string(forKey: "azureOpenAIDeployment") ?? ""
921
+ self.azureOpenAIAPIVersion = userDefaults.string(forKey: "azureOpenAIAPIVersion")
922
+ ?? "2024-10-21"
923
+ self.bedrockRegion = userDefaults.string(forKey: "bedrockRegion") ?? ""
924
+ self.bedrockAWSProfile = userDefaults.string(forKey: "bedrockAWSProfile") ?? ""
925
+ self.bedrockModelID = userDefaults.string(forKey: "bedrockModelID") ?? ""
926
+ self.vertexaiProject = userDefaults.string(forKey: "vertexaiProject") ?? ""
927
+ self.vertexaiLocation = userDefaults.string(forKey: "vertexaiLocation") ?? ""
928
+ self.zaiAPIToken = (try? zaiTokenStore.loadToken()) ?? ""
929
+ self.minimaxAPIToken = (try? minimaxTokenStore.loadToken()) ?? ""
930
+ self.minimaxCookieHeader = (try? minimaxCookieHeaderStore.loadHeader()) ?? ""
931
+ self.minimaxGroupID = (try? minimaxGroupIDStore.loadGroupID()) ?? ""
932
+ self.copilotAPIToken = (try? copilotTokenStore.loadToken()) ?? ""
933
+ self.openRouterAPIToken = (try? openRouterTokenStore.loadToken()) ?? ""
934
+ self.groqAPIToken = (try? groqTokenStore.loadToken()) ?? ""
935
+ self.deepSeekAPIToken = (try? deepSeekTokenStore.loadToken()) ?? ""
936
+ self.fireworksAPIToken = (try? fireworksTokenStore.loadToken()) ?? ""
937
+ self.mistralAPIToken = (try? mistralTokenStore.loadToken()) ?? ""
938
+ self.perplexityAPIToken = (try? perplexityTokenStore.loadToken()) ?? ""
939
+ self.kimiAPIToken = (try? kimiTokenStore.loadToken()) ?? ""
940
+ self.auggieAPIToken = (try? auggieTokenStore.loadToken()) ?? ""
941
+ self.togetherAPIToken = (try? togetherTokenStore.loadToken()) ?? ""
942
+ self.cohereAPIToken = (try? cohereTokenStore.loadToken()) ?? ""
943
+ self.xaiAPIToken = (try? xaiTokenStore.loadToken()) ?? ""
944
+ self.cerebrasAPIToken = (try? cerebrasTokenStore.loadToken()) ?? ""
945
+ self.sambaNovaAPIToken = (try? sambaNovaTokenStore.loadToken()) ?? ""
946
+ self.qwenAPIToken = (try? qwenTokenStore.loadToken()) ?? ""
947
+ self.azureOpenAIAPIToken = (try? azureOpenAITokenStore.loadToken()) ?? ""
948
+ self.selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider")
949
+ self.providerDetectionCompleted = userDefaults.object(
950
+ forKey: "providerDetectionCompleted") as? Bool ?? false
951
+ self.toggleStore = ProviderToggleStore(userDefaults: userDefaults)
952
+ self.toggleStore.purgeLegacyKeys()
953
+ LaunchAtLoginManager.setEnabled(self.launchAtLogin)
954
+ self.runInitialProviderDetectionIfNeeded()
955
+ self.applyTokenCostDefaultIfNeeded()
956
+ if self.claudeUsageDataSource != .cli {
957
+ self.claudeWebExtrasEnabled = false
958
+ }
959
+ }
960
+
961
+ func orderedProviders() -> [UsageProvider] {
962
+ let raw = self.providerOrderRaw
963
+ if raw == self.cachedProviderOrderRaw, !self.cachedProviderOrder.isEmpty {
964
+ return self.cachedProviderOrder
965
+ }
966
+ let ordered = Self.effectiveProviderOrder(raw: raw)
967
+ self.cachedProviderOrderRaw = raw
968
+ self.cachedProviderOrder = ordered
969
+ return ordered
970
+ }
971
+
972
+ func moveProvider(fromOffsets: IndexSet, toOffset: Int) {
973
+ var order = self.orderedProviders()
974
+ order.move(fromOffsets: fromOffsets, toOffset: toOffset)
975
+ self.providerOrderRaw = order.map(\.rawValue)
976
+ }
977
+
978
+ func isProviderEnabled(provider: UsageProvider, metadata: ProviderMetadata) -> Bool {
979
+ _ = self.providerToggleRevision
980
+ return self.toggleStore.isEnabled(metadata: metadata)
981
+ }
982
+
983
+ func isProviderEnabledCached(
984
+ provider: UsageProvider,
985
+ metadataByProvider: [UsageProvider: ProviderMetadata]) -> Bool
986
+ {
987
+ self.refreshProviderEnablementCacheIfNeeded(metadataByProvider: metadataByProvider)
988
+ return self.cachedProviderEnablement[provider] ?? false
989
+ }
990
+
991
+ func enabledProvidersOrdered(metadataByProvider: [UsageProvider: ProviderMetadata]) -> [UsageProvider] {
992
+ self.refreshProviderEnablementCacheIfNeeded(metadataByProvider: metadataByProvider)
993
+ let orderRaw = self.providerOrderRaw
994
+ let revision = self.cachedProviderEnablementRevision
995
+ if revision == self.cachedEnabledProvidersRevision,
996
+ orderRaw == self.cachedEnabledProvidersOrderRaw,
997
+ !self.cachedEnabledProviders.isEmpty
998
+ {
999
+ return self.cachedEnabledProviders
1000
+ }
1001
+ let enabled = self.orderedProviders().filter { self.cachedProviderEnablement[$0] ?? false }
1002
+ self.cachedEnabledProviders = enabled
1003
+ self.cachedEnabledProvidersRevision = revision
1004
+ self.cachedEnabledProvidersOrderRaw = orderRaw
1005
+ return enabled
1006
+ }
1007
+
1008
+ func setProviderEnabled(provider: UsageProvider, metadata: ProviderMetadata, enabled: Bool) {
1009
+ self.providerToggleRevision &+= 1
1010
+ self.toggleStore.setEnabled(enabled, metadata: metadata)
1011
+ }
1012
+
1013
+ func rerunProviderDetection() {
1014
+ self.runInitialProviderDetectionIfNeeded(force: true)
1015
+ }
1016
+
1017
+ /// Auto-enable a provider when the user enters a non-empty API token.
1018
+ private func autoEnableProviderIfNeeded(cliName: String) {
1019
+ let toggles = (self.userDefaults.dictionary(forKey: "providerToggles") as? [String: Bool]) ?? [:]
1020
+ guard toggles[cliName] == nil else { return }
1021
+ var updated = toggles
1022
+ updated[cliName] = true
1023
+ self.userDefaults.set(updated, forKey: "providerToggles")
1024
+ self.providerToggleRevision &+= 1
1025
+ }
1026
+
1027
+ // MARK: - Private
1028
+
1029
+ func isCostUsageEffectivelyEnabled(for provider: UsageProvider) -> Bool {
1030
+ self.costUsageEnabled
1031
+ && ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.supportsTokenCost
1032
+ }
1033
+
1034
+ private static func effectiveProviderOrder(raw: [String]) -> [UsageProvider] {
1035
+ var seen: Set<UsageProvider> = []
1036
+ var ordered: [UsageProvider] = []
1037
+
1038
+ for rawValue in raw {
1039
+ guard let provider = UsageProvider(rawValue: rawValue) else { continue }
1040
+ guard !seen.contains(provider) else { continue }
1041
+ seen.insert(provider)
1042
+ ordered.append(provider)
1043
+ }
1044
+
1045
+ if ordered.isEmpty {
1046
+ ordered = UsageProvider.allCases
1047
+ seen = Set(ordered)
1048
+ }
1049
+
1050
+ if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {
1051
+ ordered.insert(.factory, at: zaiIndex)
1052
+ seen.insert(.factory)
1053
+ }
1054
+
1055
+ for provider in UsageProvider.allCases where !seen.contains(provider) {
1056
+ ordered.append(provider)
1057
+ }
1058
+
1059
+ return ordered
1060
+ }
1061
+
1062
+ private func refreshProviderEnablementCacheIfNeeded(
1063
+ metadataByProvider: [UsageProvider: ProviderMetadata])
1064
+ {
1065
+ let revision = self.providerToggleRevision
1066
+ guard revision != self.cachedProviderEnablementRevision else { return }
1067
+ var cache: [UsageProvider: Bool] = [:]
1068
+ for (provider, metadata) in metadataByProvider {
1069
+ cache[provider] = self.toggleStore.isEnabled(metadata: metadata)
1070
+ }
1071
+ self.cachedProviderEnablement = cache
1072
+ self.cachedProviderEnablementRevision = revision
1073
+ }
1074
+
1075
+ private func runInitialProviderDetectionIfNeeded(force: Bool = false) {
1076
+ guard force || !self.providerDetectionCompleted else { return }
1077
+ guard let codexMeta = ProviderRegistry.shared.metadata[.codex],
1078
+ let claudeMeta = ProviderRegistry.shared.metadata[.claude],
1079
+ let geminiMeta = ProviderRegistry.shared.metadata[.gemini],
1080
+ let antigravityMeta = ProviderRegistry.shared.metadata[.antigravity] else { return }
1081
+
1082
+ LoginShellPathCache.shared.captureOnce { [weak self] _ in
1083
+ Task { @MainActor in
1084
+ await self?.applyProviderDetection(
1085
+ codexMeta: codexMeta,
1086
+ claudeMeta: claudeMeta,
1087
+ geminiMeta: geminiMeta,
1088
+ antigravityMeta: antigravityMeta)
1089
+ }
1090
+ }
1091
+ }
1092
+
1093
+ private func applyProviderDetection(
1094
+ codexMeta: ProviderMetadata,
1095
+ claudeMeta: ProviderMetadata,
1096
+ geminiMeta: ProviderMetadata,
1097
+ antigravityMeta: ProviderMetadata) async
1098
+ {
1099
+ guard !self.providerDetectionCompleted else { return }
1100
+ let codexInstalled = BinaryLocator.resolveCodexBinary() != nil
1101
+ let claudeInstalled = BinaryLocator.resolveClaudeBinary() != nil
1102
+ let geminiInstalled = BinaryLocator.resolveGeminiBinary() != nil
1103
+ let antigravityRunning = await AntigravityStatusProbe.isRunning()
1104
+
1105
+ // If none installed, keep Codex enabled to match previous behavior.
1106
+ let noneInstalled = !codexInstalled && !claudeInstalled && !geminiInstalled && !antigravityRunning
1107
+ let enableCodex = codexInstalled || noneInstalled
1108
+ let enableClaude = claudeInstalled
1109
+ let enableGemini = geminiInstalled
1110
+ let enableAntigravity = antigravityRunning
1111
+
1112
+ self.providerToggleRevision &+= 1
1113
+ self.toggleStore.setEnabled(enableCodex, metadata: codexMeta)
1114
+ self.toggleStore.setEnabled(enableClaude, metadata: claudeMeta)
1115
+ self.toggleStore.setEnabled(enableGemini, metadata: geminiMeta)
1116
+ self.toggleStore.setEnabled(enableAntigravity, metadata: antigravityMeta)
1117
+ self.providerDetectionCompleted = true
1118
+ }
1119
+
1120
+ private func applyTokenCostDefaultIfNeeded() {
1121
+ // Settings are persisted in UserDefaults.standard.
1122
+ guard UserDefaults.standard.object(forKey: "tokenCostUsageEnabled") == nil else { return }
1123
+
1124
+ Task { @MainActor [weak self] in
1125
+ guard let self else { return }
1126
+ let hasSources = await Task.detached(priority: .utility) {
1127
+ Self.hasAnyTokenCostUsageSources()
1128
+ }.value
1129
+ guard hasSources else { return }
1130
+ guard UserDefaults.standard.object(forKey: "tokenCostUsageEnabled") == nil else { return }
1131
+ self.costUsageEnabled = true
1132
+ }
1133
+ }
1134
+
1135
+ nonisolated static func hasAnyTokenCostUsageSources(
1136
+ env: [String: String] = ProcessInfo.processInfo.environment,
1137
+ fileManager: FileManager = .default) -> Bool
1138
+ {
1139
+ func hasAnyJsonl(in root: URL) -> Bool {
1140
+ guard fileManager.fileExists(atPath: root.path) else { return false }
1141
+ guard let enumerator = fileManager.enumerator(
1142
+ at: root,
1143
+ includingPropertiesForKeys: [.isRegularFileKey],
1144
+ options: [.skipsHiddenFiles, .skipsPackageDescendants])
1145
+ else { return false }
1146
+
1147
+ for case let url as URL in enumerator where url.pathExtension.lowercased() == "jsonl" {
1148
+ return true
1149
+ }
1150
+ return false
1151
+ }
1152
+
1153
+ let codexRoot: URL = {
1154
+ let raw = env["CODEX_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines)
1155
+ if let raw, !raw.isEmpty {
1156
+ return URL(fileURLWithPath: raw).appendingPathComponent("sessions", isDirectory: true)
1157
+ }
1158
+ return fileManager.homeDirectoryForCurrentUser
1159
+ .appendingPathComponent(".codex", isDirectory: true)
1160
+ .appendingPathComponent("sessions", isDirectory: true)
1161
+ }()
1162
+ if hasAnyJsonl(in: codexRoot) { return true }
1163
+
1164
+ let claudeRoots: [URL] = {
1165
+ if let env = env["CLAUDE_CONFIG_DIR"]?.trimmingCharacters(in: .whitespacesAndNewlines),
1166
+ !env.isEmpty
1167
+ {
1168
+ return env.split(separator: ",").map { part in
1169
+ let raw = String(part).trimmingCharacters(in: .whitespacesAndNewlines)
1170
+ let url = URL(fileURLWithPath: raw)
1171
+ if url.lastPathComponent == "projects" {
1172
+ return url
1173
+ }
1174
+ return url.appendingPathComponent("projects", isDirectory: true)
1175
+ }
1176
+ }
1177
+
1178
+ let home = fileManager.homeDirectoryForCurrentUser
1179
+ return [
1180
+ home.appendingPathComponent(".config/claude/projects", isDirectory: true),
1181
+ home.appendingPathComponent(".claude/projects", isDirectory: true),
1182
+ ]
1183
+ }()
1184
+
1185
+ return claudeRoots.contains(where: hasAnyJsonl(in:))
1186
+ }
1187
+
1188
+ private func schedulePersistZaiAPIToken() {
1189
+ self.zaiTokenPersistTask?.cancel()
1190
+ let token = self.zaiAPIToken
1191
+ let tokenStore = self.zaiTokenStore
1192
+ self.zaiTokenPersistTask = Task { @MainActor in
1193
+ do {
1194
+ try await Task.sleep(nanoseconds: 350_000_000)
1195
+ } catch {
1196
+ return
1197
+ }
1198
+ guard !Task.isCancelled else { return }
1199
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1200
+ do {
1201
+ try tokenStore.storeToken(token)
1202
+ return nil
1203
+ } catch {
1204
+ return error
1205
+ }
1206
+ }.value
1207
+ if let error {
1208
+ // Keep value in memory; persist best-effort.
1209
+ RunicLog.logger("zai-token-store").error("Failed to persist z.ai token: \(error)")
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ private func schedulePersistMiniMaxAPIToken() {
1215
+ self.minimaxTokenPersistTask?.cancel()
1216
+ let token = self.minimaxAPIToken
1217
+ let tokenStore = self.minimaxTokenStore
1218
+ self.minimaxTokenPersistTask = Task { @MainActor in
1219
+ do {
1220
+ try await Task.sleep(nanoseconds: 350_000_000)
1221
+ } catch {
1222
+ return
1223
+ }
1224
+ guard !Task.isCancelled else { return }
1225
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1226
+ do {
1227
+ try tokenStore.storeToken(token)
1228
+ return nil
1229
+ } catch {
1230
+ return error
1231
+ }
1232
+ }.value
1233
+ if let error {
1234
+ RunicLog.logger("minimax-token-store").error("Failed to persist MiniMax token: \(error)")
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ private func schedulePersistMiniMaxCookieHeader() {
1240
+ self.minimaxCookieHeaderPersistTask?.cancel()
1241
+ let header = self.minimaxCookieHeader
1242
+ let store = self.minimaxCookieHeaderStore
1243
+ self.minimaxCookieHeaderPersistTask = Task { @MainActor in
1244
+ do {
1245
+ try await Task.sleep(nanoseconds: 350_000_000)
1246
+ } catch {
1247
+ return
1248
+ }
1249
+ guard !Task.isCancelled else { return }
1250
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1251
+ do {
1252
+ try store.storeHeader(header)
1253
+ return nil
1254
+ } catch {
1255
+ return error
1256
+ }
1257
+ }.value
1258
+ if let error {
1259
+ RunicLog.logger("minimax-cookie-store").error("Failed to persist MiniMax cookie header: \(error)")
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ private func schedulePersistMiniMaxGroupID() {
1265
+ self.minimaxGroupIDPersistTask?.cancel()
1266
+ let groupID = self.minimaxGroupID
1267
+ let groupStore = self.minimaxGroupIDStore
1268
+ self.minimaxGroupIDPersistTask = Task { @MainActor in
1269
+ do {
1270
+ try await Task.sleep(nanoseconds: 350_000_000)
1271
+ } catch {
1272
+ return
1273
+ }
1274
+ guard !Task.isCancelled else { return }
1275
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1276
+ do {
1277
+ try groupStore.storeGroupID(groupID)
1278
+ return nil
1279
+ } catch {
1280
+ return error
1281
+ }
1282
+ }.value
1283
+ if let error {
1284
+ RunicLog.logger("minimax-groupid-store").error("Failed to persist MiniMax Group ID: \(error)")
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ private func schedulePersistCopilotAPIToken() {
1290
+ self.copilotTokenPersistTask?.cancel()
1291
+ let token = self.copilotAPIToken
1292
+ let tokenStore = self.copilotTokenStore
1293
+ self.copilotTokenPersistTask = Task { @MainActor in
1294
+ do {
1295
+ try await Task.sleep(nanoseconds: 350_000_000)
1296
+ } catch {
1297
+ return
1298
+ }
1299
+ guard !Task.isCancelled else { return }
1300
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1301
+ do {
1302
+ try tokenStore.storeToken(token)
1303
+ return nil
1304
+ } catch {
1305
+ return error
1306
+ }
1307
+ }.value
1308
+ if let error {
1309
+ RunicLog.logger("copilot-token-store").error("Failed to persist Copilot token: \(error)")
1310
+ }
1311
+ }
1312
+ }
1313
+
1314
+ private func schedulePersistOpenRouterAPIToken() {
1315
+ self.openRouterTokenPersistTask?.cancel()
1316
+ let token = self.openRouterAPIToken
1317
+ let tokenStore = self.openRouterTokenStore
1318
+ self.openRouterTokenPersistTask = Task { @MainActor in
1319
+ do {
1320
+ try await Task.sleep(nanoseconds: 350_000_000)
1321
+ } catch {
1322
+ return
1323
+ }
1324
+ guard !Task.isCancelled else { return }
1325
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1326
+ do {
1327
+ try tokenStore.storeToken(token)
1328
+ return nil
1329
+ } catch {
1330
+ return error
1331
+ }
1332
+ }.value
1333
+ if let error {
1334
+ RunicLog.logger("openrouter-token-store").error("Failed to persist OpenRouter token: \(error)")
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ private func schedulePersistGroqAPIToken() {
1340
+ self.groqTokenPersistTask?.cancel()
1341
+ let token = self.groqAPIToken
1342
+ let tokenStore = self.groqTokenStore
1343
+ self.groqTokenPersistTask = Task { @MainActor in
1344
+ do {
1345
+ try await Task.sleep(nanoseconds: 350_000_000)
1346
+ } catch {
1347
+ return
1348
+ }
1349
+ guard !Task.isCancelled else { return }
1350
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1351
+ do {
1352
+ try tokenStore.storeToken(token)
1353
+ return nil
1354
+ } catch {
1355
+ return error
1356
+ }
1357
+ }.value
1358
+ if let error {
1359
+ RunicLog.logger("groq-token-store").error("Failed to persist Groq token: \(error)")
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ private func schedulePersistDeepSeekAPIToken() {
1365
+ self.deepSeekTokenPersistTask?.cancel()
1366
+ let token = self.deepSeekAPIToken
1367
+ let tokenStore = self.deepSeekTokenStore
1368
+ self.deepSeekTokenPersistTask = Task { @MainActor in
1369
+ do {
1370
+ try await Task.sleep(nanoseconds: 350_000_000)
1371
+ } catch {
1372
+ return
1373
+ }
1374
+ guard !Task.isCancelled else { return }
1375
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1376
+ do {
1377
+ try tokenStore.storeToken(token)
1378
+ return nil
1379
+ } catch {
1380
+ return error
1381
+ }
1382
+ }.value
1383
+ if let error {
1384
+ RunicLog.logger("deepseek-token-store").error("Failed to persist DeepSeek token: \(error)")
1385
+ }
1386
+ }
1387
+ }
1388
+
1389
+ private func schedulePersistFireworksAPIToken() {
1390
+ self.fireworksTokenPersistTask?.cancel()
1391
+ let token = self.fireworksAPIToken
1392
+ let tokenStore = self.fireworksTokenStore
1393
+ self.fireworksTokenPersistTask = Task { @MainActor in
1394
+ do {
1395
+ try await Task.sleep(nanoseconds: 350_000_000)
1396
+ } catch {
1397
+ return
1398
+ }
1399
+ guard !Task.isCancelled else { return }
1400
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1401
+ do {
1402
+ try tokenStore.storeToken(token)
1403
+ return nil
1404
+ } catch {
1405
+ return error
1406
+ }
1407
+ }.value
1408
+ if let error {
1409
+ RunicLog.logger("fireworks-token-store").error("Failed to persist Fireworks token: \(error)")
1410
+ }
1411
+ }
1412
+ }
1413
+
1414
+ private func schedulePersistMistralAPIToken() {
1415
+ self.mistralTokenPersistTask?.cancel()
1416
+ let token = self.mistralAPIToken
1417
+ let tokenStore = self.mistralTokenStore
1418
+ self.mistralTokenPersistTask = Task { @MainActor in
1419
+ do {
1420
+ try await Task.sleep(nanoseconds: 350_000_000)
1421
+ } catch {
1422
+ return
1423
+ }
1424
+ guard !Task.isCancelled else { return }
1425
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1426
+ do {
1427
+ try tokenStore.storeToken(token)
1428
+ return nil
1429
+ } catch {
1430
+ return error
1431
+ }
1432
+ }.value
1433
+ if let error {
1434
+ RunicLog.logger("mistral-token-store").error("Failed to persist Mistral token: \(error)")
1435
+ }
1436
+ }
1437
+ }
1438
+
1439
+ private func schedulePersistPerplexityAPIToken() {
1440
+ self.perplexityTokenPersistTask?.cancel()
1441
+ let token = self.perplexityAPIToken
1442
+ let tokenStore = self.perplexityTokenStore
1443
+ self.perplexityTokenPersistTask = Task { @MainActor in
1444
+ do {
1445
+ try await Task.sleep(nanoseconds: 350_000_000)
1446
+ } catch {
1447
+ return
1448
+ }
1449
+ guard !Task.isCancelled else { return }
1450
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1451
+ do {
1452
+ try tokenStore.storeToken(token)
1453
+ return nil
1454
+ } catch {
1455
+ return error
1456
+ }
1457
+ }.value
1458
+ if let error {
1459
+ RunicLog.logger("perplexity-token-store").error("Failed to persist Perplexity token: \(error)")
1460
+ }
1461
+ }
1462
+ }
1463
+
1464
+ private func schedulePersistKimiAPIToken() {
1465
+ self.kimiTokenPersistTask?.cancel()
1466
+ let token = self.kimiAPIToken
1467
+ let tokenStore = self.kimiTokenStore
1468
+ self.kimiTokenPersistTask = Task { @MainActor in
1469
+ do {
1470
+ try await Task.sleep(nanoseconds: 350_000_000)
1471
+ } catch {
1472
+ return
1473
+ }
1474
+ guard !Task.isCancelled else { return }
1475
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1476
+ do {
1477
+ try tokenStore.storeToken(token)
1478
+ return nil
1479
+ } catch {
1480
+ return error
1481
+ }
1482
+ }.value
1483
+ if let error {
1484
+ RunicLog.logger("kimi-token-store").error("Failed to persist Kimi token: \(error)")
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ private func schedulePersistAuggieAPIToken() {
1490
+ self.auggieTokenPersistTask?.cancel()
1491
+ let token = self.auggieAPIToken
1492
+ let tokenStore = self.auggieTokenStore
1493
+ self.auggieTokenPersistTask = Task { @MainActor in
1494
+ do {
1495
+ try await Task.sleep(nanoseconds: 350_000_000)
1496
+ } catch {
1497
+ return
1498
+ }
1499
+ guard !Task.isCancelled else { return }
1500
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1501
+ do {
1502
+ try tokenStore.storeToken(token)
1503
+ return nil
1504
+ } catch {
1505
+ return error
1506
+ }
1507
+ }.value
1508
+ if let error {
1509
+ RunicLog.logger("auggie-token-store").error("Failed to persist Auggie token: \(error)")
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ private func schedulePersistTogetherAPIToken() {
1515
+ self.togetherTokenPersistTask?.cancel()
1516
+ let token = self.togetherAPIToken
1517
+ let tokenStore = self.togetherTokenStore
1518
+ self.togetherTokenPersistTask = Task { @MainActor in
1519
+ do {
1520
+ try await Task.sleep(nanoseconds: 350_000_000)
1521
+ } catch {
1522
+ return
1523
+ }
1524
+ guard !Task.isCancelled else { return }
1525
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1526
+ do {
1527
+ try tokenStore.storeToken(token)
1528
+ return nil
1529
+ } catch {
1530
+ return error
1531
+ }
1532
+ }.value
1533
+ if let error {
1534
+ RunicLog.logger("together-token-store").error("Failed to persist Together token: \(error)")
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ private func schedulePersistCohereAPIToken() {
1540
+ self.cohereTokenPersistTask?.cancel()
1541
+ let token = self.cohereAPIToken
1542
+ let tokenStore = self.cohereTokenStore
1543
+ self.cohereTokenPersistTask = Task { @MainActor in
1544
+ do {
1545
+ try await Task.sleep(nanoseconds: 350_000_000)
1546
+ } catch {
1547
+ return
1548
+ }
1549
+ guard !Task.isCancelled else { return }
1550
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1551
+ do {
1552
+ try tokenStore.storeToken(token)
1553
+ return nil
1554
+ } catch {
1555
+ return error
1556
+ }
1557
+ }.value
1558
+ if let error {
1559
+ RunicLog.logger("cohere-token-store").error("Failed to persist Cohere token: \(error)")
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ private func schedulePersistXAiAPIToken() {
1565
+ self.xaiTokenPersistTask?.cancel()
1566
+ let token = self.xaiAPIToken
1567
+ let tokenStore = self.xaiTokenStore
1568
+ self.xaiTokenPersistTask = Task { @MainActor in
1569
+ do {
1570
+ try await Task.sleep(nanoseconds: 350_000_000)
1571
+ } catch {
1572
+ return
1573
+ }
1574
+ guard !Task.isCancelled else { return }
1575
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1576
+ do {
1577
+ try tokenStore.storeToken(token)
1578
+ return nil
1579
+ } catch {
1580
+ return error
1581
+ }
1582
+ }.value
1583
+ if let error {
1584
+ RunicLog.logger("xai-token-store").error("Failed to persist xAI token: \(error)")
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ private func schedulePersistCerebrasAPIToken() {
1590
+ self.cerebrasTokenPersistTask?.cancel()
1591
+ let token = self.cerebrasAPIToken
1592
+ let tokenStore = self.cerebrasTokenStore
1593
+ self.cerebrasTokenPersistTask = Task { @MainActor in
1594
+ do {
1595
+ try await Task.sleep(nanoseconds: 350_000_000)
1596
+ } catch {
1597
+ return
1598
+ }
1599
+ guard !Task.isCancelled else { return }
1600
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1601
+ do {
1602
+ try tokenStore.storeToken(token)
1603
+ return nil
1604
+ } catch {
1605
+ return error
1606
+ }
1607
+ }.value
1608
+ if let error {
1609
+ RunicLog.logger("cerebras-token-store").error("Failed to persist Cerebras token: \(error)")
1610
+ }
1611
+ }
1612
+ }
1613
+
1614
+ private func schedulePersistSambaNovaAPIToken() {
1615
+ self.sambaNovaTokenPersistTask?.cancel()
1616
+ let token = self.sambaNovaAPIToken
1617
+ let tokenStore = self.sambaNovaTokenStore
1618
+ self.sambaNovaTokenPersistTask = Task { @MainActor in
1619
+ do {
1620
+ try await Task.sleep(nanoseconds: 350_000_000)
1621
+ } catch {
1622
+ return
1623
+ }
1624
+ guard !Task.isCancelled else { return }
1625
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1626
+ do {
1627
+ try tokenStore.storeToken(token)
1628
+ return nil
1629
+ } catch {
1630
+ return error
1631
+ }
1632
+ }.value
1633
+ if let error {
1634
+ RunicLog.logger("sambanova-token-store").error("Failed to persist SambaNova token: \(error)")
1635
+ }
1636
+ }
1637
+ }
1638
+
1639
+ private func schedulePersistAzureOpenAIAPIToken() {
1640
+ self.azureOpenAITokenPersistTask?.cancel()
1641
+ let token = self.azureOpenAIAPIToken
1642
+ let tokenStore = self.azureOpenAITokenStore
1643
+ self.azureOpenAITokenPersistTask = Task { @MainActor in
1644
+ do {
1645
+ try await Task.sleep(nanoseconds: 350_000_000)
1646
+ } catch {
1647
+ return
1648
+ }
1649
+ guard !Task.isCancelled else { return }
1650
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1651
+ do {
1652
+ try tokenStore.storeToken(token)
1653
+ return nil
1654
+ } catch {
1655
+ return error
1656
+ }
1657
+ }.value
1658
+ if let error {
1659
+ RunicLog.logger("azure-openai-token-store")
1660
+ .error("Failed to persist Azure OpenAI token: \(error)")
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ private func schedulePersistQwenAPIToken() {
1666
+ self.qwenTokenPersistTask?.cancel()
1667
+ let token = self.qwenAPIToken
1668
+ let tokenStore = self.qwenTokenStore
1669
+ self.qwenTokenPersistTask = Task { @MainActor in
1670
+ do {
1671
+ try await Task.sleep(nanoseconds: 350_000_000)
1672
+ } catch {
1673
+ return
1674
+ }
1675
+ guard !Task.isCancelled else { return }
1676
+ let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in
1677
+ do {
1678
+ try tokenStore.storeToken(token)
1679
+ return nil
1680
+ } catch {
1681
+ return error
1682
+ }
1683
+ }.value
1684
+ if let error {
1685
+ RunicLog.logger("qwen-token-store").error("Failed to persist Qwen token: \(error)")
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+
1691
+ enum LaunchAtLoginManager {
1692
+ @MainActor
1693
+ static func setEnabled(_ enabled: Bool) {
1694
+ guard #available(macOS 13, *) else { return }
1695
+ let service = SMAppService.mainApp
1696
+ if enabled {
1697
+ try? service.register()
1698
+ } else {
1699
+ try? service.unregister()
1700
+ }
1701
+ }
1702
+ }