@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,120 @@
1
+ import RunicCore
2
+ import SwiftUI
3
+
4
+ @MainActor
5
+ struct IconView: View {
6
+ let snapshot: UsageSnapshot?
7
+ let creditsRemaining: Double?
8
+ let isStale: Bool
9
+ let showLoadingAnimation: Bool
10
+ let style: IconStyle
11
+ @State private var phase: CGFloat = 0
12
+ @State private var displayLink = DisplayLinkDriver()
13
+ @State private var pattern: LoadingPattern = .knightRider
14
+ @State private var debugCycle = false
15
+ @State private var cycleIndex = 0
16
+ @State private var cycleCounter = 0
17
+ private let loadingFPS: Double = 12
18
+ // Advance to next pattern every N ticks when debug cycling.
19
+ private let cycleIntervalTicks = 20
20
+ private let patterns = LoadingPattern.allCases
21
+
22
+ private var isLoading: Bool { self.showLoadingAnimation && self.snapshot == nil }
23
+
24
+ var body: some View {
25
+ Group {
26
+ if let snapshot {
27
+ Image(nsImage: IconRenderer.makeIcon(
28
+ primaryRemaining: snapshot.primary.remainingPercent,
29
+ weeklyRemaining: snapshot.secondary?.remainingPercent,
30
+ creditsRemaining: self.creditsRemaining,
31
+ stale: self.isStale,
32
+ style: self.style))
33
+ .renderingMode(.original)
34
+ .interpolation(.none)
35
+ .frame(width: 20, height: 18, alignment: .center)
36
+ .padding(.horizontal, RunicSpacing.xxxs)
37
+ } else if self.showLoadingAnimation {
38
+ // Loading: animate bars with the current pattern until data arrives.
39
+ Image(nsImage: self.loadingImage)
40
+ .renderingMode(.original)
41
+ .interpolation(.none)
42
+ .frame(width: 20, height: 18, alignment: .center)
43
+ .padding(.horizontal, RunicSpacing.xxxs)
44
+ .onChange(of: self.displayLink.tick) { _, _ in
45
+ self.phase += 0.09 // half-speed animation
46
+ if self.debugCycle {
47
+ self.cycleCounter += 1
48
+ if self.cycleCounter >= self.cycleIntervalTicks {
49
+ self.cycleCounter = 0
50
+ self.cycleIndex = (self.cycleIndex + 1) % self.patterns.count
51
+ self.pattern = self.patterns[self.cycleIndex]
52
+ }
53
+ }
54
+ }
55
+ } else {
56
+ // No animation when usage/account is unavailable; show empty tracks.
57
+ Image(nsImage: IconRenderer.makeIcon(
58
+ primaryRemaining: nil,
59
+ weeklyRemaining: nil,
60
+ creditsRemaining: self.creditsRemaining,
61
+ stale: self.isStale,
62
+ style: self.style))
63
+ .renderingMode(.original)
64
+ .interpolation(.none)
65
+ .frame(width: 20, height: 18, alignment: .center)
66
+ .padding(.horizontal, RunicSpacing.xxxs)
67
+ }
68
+ }
69
+ .onChange(of: self.isLoading, initial: true) { _, isLoading in
70
+ if isLoading {
71
+ self.displayLink.start(fps: self.loadingFPS)
72
+ if !self.debugCycle {
73
+ self.pattern = self.patterns.randomElement() ?? .knightRider
74
+ }
75
+ } else {
76
+ self.displayLink.stop()
77
+ self.debugCycle = false
78
+ self.phase = 0
79
+ }
80
+ }
81
+ .onDisappear { self.displayLink.stop() }
82
+ .onReceive(NotificationCenter.default.publisher(for: .runicDebugReplayAllAnimations)) { notification in
83
+ if let raw = notification.userInfo?["pattern"] as? String,
84
+ let selected = LoadingPattern(rawValue: raw)
85
+ {
86
+ self.debugCycle = false
87
+ self.pattern = selected
88
+ self.cycleIndex = self.patterns.firstIndex(of: selected) ?? 0
89
+ } else {
90
+ self.debugCycle = true
91
+ self.cycleIndex = 0
92
+ self.pattern = self.patterns.first ?? .knightRider
93
+ }
94
+ self.cycleCounter = 0
95
+ self.phase = 0
96
+ }
97
+ }
98
+
99
+ private var loadingPrimary: Double {
100
+ self.pattern.value(phase: Double(self.phase))
101
+ }
102
+
103
+ private var loadingSecondary: Double {
104
+ self.pattern.value(phase: Double(self.phase + self.pattern.secondaryOffset))
105
+ }
106
+
107
+ private var loadingImage: NSImage {
108
+ if self.pattern == .unbraid {
109
+ let progress = self.loadingPrimary / 100
110
+ return IconRenderer.makeMorphIcon(progress: progress, style: self.style)
111
+ } else {
112
+ return IconRenderer.makeIcon(
113
+ primaryRemaining: self.loadingPrimary,
114
+ weeklyRemaining: self.loadingSecondary,
115
+ creditsRemaining: nil,
116
+ stale: false,
117
+ style: self.style)
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,71 @@
1
+ import AppKit
2
+ import RunicCore
3
+
4
+ @MainActor
5
+ enum ProviderBrandIcon {
6
+ private static let cache = NSCache<NSString, NSImage>()
7
+
8
+ static func image(for provider: UsageProvider, size: CGFloat = 16) -> NSImage? {
9
+ let cacheSize = Int(size.rounded())
10
+ let key = "\(provider.rawValue)-\(cacheSize)" as NSString
11
+ if let cached = self.cache.object(forKey: key) {
12
+ return cached
13
+ }
14
+
15
+ let baseName = ProviderDescriptorRegistry.descriptor(for: provider).branding.iconResourceName
16
+ guard let url = Bundle.main.url(forResource: baseName, withExtension: "svg"),
17
+ let image = NSImage(contentsOf: url)
18
+ else {
19
+ return nil
20
+ }
21
+
22
+ let targetSize = NSSize(width: size, height: size)
23
+ image.size = targetSize
24
+ image.isTemplate = true
25
+ let tinted = self.tintedImage(image, color: self.brandColor(for: provider), size: targetSize)
26
+ self.cache.setObject(tinted, forKey: key)
27
+ return tinted
28
+ }
29
+
30
+ static func templateImage(for provider: UsageProvider, size: CGFloat = 16) -> NSImage? {
31
+ let cacheSize = Int(size.rounded())
32
+ let key = "\(provider.rawValue)-\(cacheSize)-template" as NSString
33
+ if let cached = self.cache.object(forKey: key) {
34
+ return cached
35
+ }
36
+
37
+ let baseName = ProviderDescriptorRegistry.descriptor(for: provider).branding.iconResourceName
38
+ guard let url = Bundle.main.url(forResource: baseName, withExtension: "svg"),
39
+ let image = NSImage(contentsOf: url)
40
+ else {
41
+ return nil
42
+ }
43
+
44
+ image.size = NSSize(width: size, height: size)
45
+ image.isTemplate = true
46
+ self.cache.setObject(image, forKey: key)
47
+ return image
48
+ }
49
+
50
+ private static func brandColor(for provider: UsageProvider) -> NSColor {
51
+ let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
52
+ return NSColor(
53
+ calibratedRed: CGFloat(color.red),
54
+ green: CGFloat(color.green),
55
+ blue: CGFloat(color.blue),
56
+ alpha: 1)
57
+ }
58
+
59
+ private static func tintedImage(_ image: NSImage, color: NSColor, size: NSSize) -> NSImage {
60
+ let rect = NSRect(origin: .zero, size: size)
61
+ // Use drawingHandler instead of deprecated lockFocus/unlockFocus.
62
+ let tinted = NSImage(size: size, flipped: false) { _ in
63
+ color.setFill()
64
+ rect.fill()
65
+ image.draw(in: rect, from: .zero, operation: .destinationIn, fraction: 1.0)
66
+ return true
67
+ }
68
+ tinted.isTemplate = false
69
+ return tinted
70
+ }
71
+ }
@@ -0,0 +1,53 @@
1
+ import SwiftUI
2
+
3
+ /// Animation presets used across the Runic UI.
4
+ enum RunicAnimation {
5
+ // MARK: - Spring Presets
6
+
7
+ /// Standard appear animation for progress bars and chart elements.
8
+ static let barAppear: Animation = .spring(response: 0.6, dampingFraction: 0.75)
9
+
10
+ /// Value-change animation for progress bars.
11
+ static let barUpdate: Animation = .spring(response: 0.5, dampingFraction: 0.8)
12
+
13
+ /// Menu content fade when switching providers.
14
+ static let providerSwitch: Animation = .easeInOut(duration: 0.25)
15
+
16
+ /// Highlight state transitions on menu items.
17
+ static let highlight: Animation = .easeOut(duration: 0.12)
18
+
19
+ // MARK: - Transition Durations
20
+
21
+ /// Duration for sheen / gloss sweeps on progress bars.
22
+ static let sheenDuration: Double = 0.9
23
+
24
+ /// Delay before sheen resets after sweeping.
25
+ static let sheenResetDelay: Duration = .seconds(1.1)
26
+
27
+ /// Duration for copy-confirmation checkmark display.
28
+ static let copyFeedbackDuration: Duration = .seconds(0.9)
29
+
30
+ /// Fade-out duration for copy confirmation revert.
31
+ static let copyFeedbackFadeOut: Animation = .easeOut(duration: 0.2)
32
+
33
+ // MARK: - Skeleton / Loading
34
+
35
+ /// Continuous shimmer animation for loading skeletons.
36
+ static let shimmer: Animation = .easeInOut(duration: 1.5).repeatForever(autoreverses: false)
37
+
38
+ /// Subtle hover feedback on interactive elements.
39
+ static let hoverFeedback: Animation = .easeInOut(duration: 0.15)
40
+
41
+ // MARK: - Chart Interaction
42
+
43
+ /// Fade animation for chart interaction hints.
44
+ static let chartHintFade: Animation = .easeOut(duration: 0.4)
45
+
46
+ // MARK: - Menu Card Entrance
47
+
48
+ /// Staggered spring entrance for menu card sections.
49
+ static let cardEntrance: Animation = .spring(response: 0.45, dampingFraction: 0.82)
50
+
51
+ /// Base delay between staggered card section entrances.
52
+ static let cardEntranceStagger: Double = 0.06
53
+ }
@@ -0,0 +1,73 @@
1
+ import SwiftUI
2
+
3
+ /// Semantic color tokens for consistent theming across the Runic UI.
4
+ enum RunicColors {
5
+ // MARK: - Status Colors
6
+
7
+ static let success = Color(nsColor: .systemGreen)
8
+ static let warning = Color(nsColor: .systemOrange)
9
+ static let error = Color(nsColor: .systemRed)
10
+ static let info = Color(nsColor: .systemBlue)
11
+
12
+ // MARK: - Chart Palette (6 accessible colors)
13
+
14
+ /// Ordered palette for chart series. Colors chosen for distinguishability in
15
+ /// both light and dark mode and for common color-vision deficiencies.
16
+ static let chartPalette: [Color] = [
17
+ Color(red: 0.26, green: 0.55, blue: 0.96), // blue
18
+ Color(red: 0.94, green: 0.53, blue: 0.18), // orange
19
+ Color(red: 0.46, green: 0.75, blue: 0.36), // green
20
+ Color(red: 0.80, green: 0.45, blue: 0.92), // purple
21
+ Color(red: 0.26, green: 0.78, blue: 0.86), // teal
22
+ Color(red: 0.94, green: 0.74, blue: 0.26), // yellow
23
+ ]
24
+
25
+ /// Returns a chart color for the given index, wrapping around if needed.
26
+ static func chartColor(at index: Int) -> Color {
27
+ self.chartPalette[index % self.chartPalette.count]
28
+ }
29
+
30
+ // MARK: - Model Family Colors
31
+
32
+ /// Color for Claude/Anthropic model family.
33
+ static let modelFamilyClaude = Color(red: 0.80, green: 0.45, blue: 0.30)
34
+ /// Color for GPT/OpenAI model family.
35
+ static let modelFamilyGPT = Color(red: 0.30, green: 0.70, blue: 0.40)
36
+ /// Color for Gemini/Google model family.
37
+ static let modelFamilyGemini = Color(red: 0.26, green: 0.55, blue: 0.96)
38
+ /// Default color for unknown model families.
39
+ static let modelFamilyDefault = Color(red: 0.55, green: 0.55, blue: 0.60)
40
+
41
+ /// Returns a color appropriate for the given model name based on its family.
42
+ static func colorForModel(_ model: String) -> Color {
43
+ let lower = model.lowercased()
44
+ if lower.contains("claude") || lower.contains("sonnet") || lower.contains("opus") || lower.contains("haiku") {
45
+ return self.modelFamilyClaude
46
+ }
47
+ if lower.contains("gpt") || lower.contains("o1") || lower.contains("o3") || lower.contains("o4") {
48
+ return self.modelFamilyGPT
49
+ }
50
+ if lower.contains("gemini") {
51
+ return self.modelFamilyGemini
52
+ }
53
+ return self.modelFamilyDefault
54
+ }
55
+
56
+ // MARK: - Named Chart Colors
57
+
58
+ /// Credits history chart bar color
59
+ static let creditsChartBar = Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)
60
+
61
+ // MARK: - Opacity Tokens
62
+
63
+ enum Opacity {
64
+ static let nano: Double = 0.045
65
+ static let subtle: Double = 0.08
66
+ static let light: Double = 0.12
67
+ static let medium: Double = 0.22
68
+ static let strong: Double = 0.35
69
+ static let emphasis: Double = 0.55
70
+ static let prominent: Double = 0.70
71
+ static let vivid: Double = 0.90
72
+ }
73
+ }
@@ -0,0 +1,15 @@
1
+ import Foundation
2
+
3
+ /// Corner radius tokens replacing scattered literal values throughout the UI.
4
+ enum RunicCornerRadius {
5
+ /// 4pt — badges, small tags, inline controls
6
+ static let xs: CGFloat = 4
7
+ /// 6pt — list rows, compact cards, buttons
8
+ static let sm: CGFloat = 6
9
+ /// 8pt — standard cards, menu sections
10
+ static let md: CGFloat = 8
11
+ /// 12pt — prominent panels, avatar frames, provider cards
12
+ static let lg: CGFloat = 12
13
+ /// 16pt — large icons, about pane, hero elements
14
+ static let xl: CGFloat = 16
15
+ }
@@ -0,0 +1,68 @@
1
+ import Foundation
2
+
3
+ /// **Runic Spacing System**
4
+ /// Defines global spacing constants following a 4pt grid system.
5
+ /// All spacing values are multiples of 4 for consistent visual rhythm.
6
+ enum RunicSpacing {
7
+ // MARK: - Base Spacing Scale
8
+
9
+ /// Extra extra extra small spacing (2pt)
10
+ static let xxxs: CGFloat = 2
11
+
12
+ /// Extra extra small spacing (4pt)
13
+ static let xxs: CGFloat = 4
14
+
15
+ /// Extra small spacing (8pt)
16
+ static let xs: CGFloat = 8
17
+
18
+ /// Small spacing (12pt)
19
+ static let sm: CGFloat = 12
20
+
21
+ /// Medium spacing (16pt)
22
+ static let md: CGFloat = 16
23
+
24
+ /// Large spacing (24pt)
25
+ static let lg: CGFloat = 24
26
+
27
+ /// Extra large spacing (32pt)
28
+ static let xl: CGFloat = 32
29
+
30
+ /// Extra extra large spacing (48pt)
31
+ static let xxl: CGFloat = 48
32
+
33
+ // MARK: - Semantic Spacing
34
+
35
+ /// Horizontal padding for menu content
36
+ static let menuHorizontalPadding: CGFloat = sm // 12pt
37
+
38
+ /// Vertical padding for menu content
39
+ static let menuVerticalPadding: CGFloat = xs // 8pt
40
+
41
+ /// Spacing between cards in menu
42
+ static let cardSpacing: CGFloat = xs // 8pt
43
+
44
+ /// Spacing between major sections
45
+ static let sectionSpacing: CGFloat = lg // 24pt
46
+
47
+ // MARK: - Preferences Spacing
48
+
49
+ /// Compact spacing (6pt) - Used between sm and xs, common in preference toggles
50
+ static let compact: CGFloat = 6
51
+
52
+ /// Standardized spacing for toggle rows
53
+ static let toggleRowSpacing: CGFloat = xs // 8pt
54
+
55
+ /// Spacing after section headers
56
+ static let sectionHeaderSpacing: CGFloat = md // 16pt
57
+
58
+ /// Spacing within stepper controls
59
+ static let stepperControlSpacing: CGFloat = xs // 8pt
60
+
61
+ // MARK: - Chart Metrics
62
+
63
+ /// Standard height for chart views in menus
64
+ static let chartHeight: CGFloat = 130
65
+
66
+ /// Diameter for legend dot indicators
67
+ static let chartLegendDot: CGFloat = 7
68
+ }
@@ -0,0 +1,143 @@
1
+ import RunicCore
2
+ import Foundation
3
+ import Security
4
+
5
+ protocol AuggieTokenStoring: Sendable {
6
+ func loadToken() throws -> String?
7
+ func storeToken(_ token: String?) throws
8
+ }
9
+
10
+ enum AuggieTokenStoreError: LocalizedError {
11
+ case keychainStatus(OSStatus)
12
+ case invalidData
13
+
14
+ var errorDescription: String? {
15
+ switch self {
16
+ case let .keychainStatus(status):
17
+ "Keychain error: \(status)"
18
+ case .invalidData:
19
+ "Keychain returned invalid data."
20
+ }
21
+ }
22
+ }
23
+
24
+ struct KeychainAuggieTokenStore: AuggieTokenStoring {
25
+ private static let log = RunicLog.logger("auggie-token-store")
26
+
27
+ private let service = "com.sriinnu.athena.Runic"
28
+ private let account = "auggie-api-token"
29
+
30
+ func loadToken() throws -> String? {
31
+ // Try standard keychain first.
32
+ if let token = try self.readToken(dataProtection: false) {
33
+ return token
34
+ }
35
+ // Migrate from the old Data Protection keychain if present.
36
+ if let token = try self.readToken(dataProtection: true) {
37
+ Self.log.info("Migrating Auggie token from Data Protection keychain")
38
+ // Store first, delete old only on success — prevents data loss.
39
+ if (try? self.storeToken(token)) != nil {
40
+ try? self.deleteToken(dataProtection: true)
41
+ }
42
+ return token
43
+ }
44
+ return nil
45
+ }
46
+
47
+ func storeToken(_ token: String?) throws {
48
+ let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
49
+ if cleaned == nil || cleaned?.isEmpty == true {
50
+ try? self.deleteToken(dataProtection: false)
51
+ try? self.deleteToken(dataProtection: true)
52
+ return
53
+ }
54
+
55
+ let data = cleaned!.data(using: .utf8)!
56
+ let query = self.baseQuery(dataProtection: false)
57
+ let attributes: [String: Any] = [
58
+ kSecValueData as String: data,
59
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
+ ]
61
+
62
+ // Try update first.
63
+ let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
+ if updateStatus == errSecSuccess { return }
65
+
66
+ if updateStatus != errSecItemNotFound {
67
+ // Item exists but update failed — delete and re-add to reset ACL.
68
+ try? self.deleteToken(dataProtection: false)
69
+ }
70
+
71
+ var addQuery = query
72
+ for (key, value) in attributes { addQuery[key] = value }
73
+
74
+ // Grant the calling app permanent access so macOS never shows a
75
+ // Keychain password dialog for this item.
76
+ if let access = Self.createSelfAccess() {
77
+ addQuery[kSecAttrAccess as String] = access
78
+ }
79
+
80
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
+ guard addStatus == errSecSuccess else {
82
+ Self.log.error("Keychain add failed: \(addStatus)")
83
+ throw AuggieTokenStoreError.keychainStatus(addStatus)
84
+ }
85
+ }
86
+
87
+ // MARK: - Private
88
+
89
+ private func readToken(dataProtection: Bool) throws -> String? {
90
+ var result: CFTypeRef?
91
+ var query = self.baseQuery(dataProtection: dataProtection)
92
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
93
+ query[kSecReturnData as String] = true
94
+
95
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
96
+ if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
97
+ return nil
98
+ }
99
+ guard status == errSecSuccess else {
100
+ Self.log.error("Keychain read failed: \(status)")
101
+ throw AuggieTokenStoreError.keychainStatus(status)
102
+ }
103
+ guard let data = result as? Data else {
104
+ throw AuggieTokenStoreError.invalidData
105
+ }
106
+ let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
107
+ return (token?.isEmpty ?? true) ? nil : token
108
+ }
109
+
110
+ private func deleteToken(dataProtection: Bool) throws {
111
+ let query = self.baseQuery(dataProtection: dataProtection)
112
+ let status = SecItemDelete(query as CFDictionary)
113
+ if status == errSecSuccess || status == errSecItemNotFound { return }
114
+ Self.log.error("Keychain delete failed: \(status)")
115
+ throw AuggieTokenStoreError.keychainStatus(status)
116
+ }
117
+
118
+ private func baseQuery(dataProtection: Bool) -> [String: Any] {
119
+ var q: [String: Any] = [
120
+ kSecClass as String: kSecClassGenericPassword,
121
+ kSecAttrService as String: self.service,
122
+ kSecAttrAccount as String: self.account,
123
+ ]
124
+ if dataProtection {
125
+ q[kSecUseDataProtectionKeychain as String] = true
126
+ }
127
+ return q
128
+ }
129
+
130
+ /// Creates a SecAccess that grants the calling application permanent
131
+ /// read/write access without triggering the macOS Keychain password dialog.
132
+ private static func createSelfAccess() -> SecAccess? {
133
+ var trustedSelf: SecTrustedApplication?
134
+ guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
135
+ let trustedSelf else { return nil }
136
+ var access: SecAccess?
137
+ guard SecAccessCreate(
138
+ "Runic API Token" as CFString,
139
+ [trustedSelf] as CFArray,
140
+ &access) == errSecSuccess else { return nil }
141
+ return access
142
+ }
143
+ }
@@ -0,0 +1,143 @@
1
+ import RunicCore
2
+ import Foundation
3
+ import Security
4
+
5
+ protocol AzureOpenAITokenStoring: Sendable {
6
+ func loadToken() throws -> String?
7
+ func storeToken(_ token: String?) throws
8
+ }
9
+
10
+ enum AzureOpenAITokenStoreError: LocalizedError {
11
+ case keychainStatus(OSStatus)
12
+ case invalidData
13
+
14
+ var errorDescription: String? {
15
+ switch self {
16
+ case let .keychainStatus(status):
17
+ "Keychain error: \(status)"
18
+ case .invalidData:
19
+ "Keychain returned invalid data."
20
+ }
21
+ }
22
+ }
23
+
24
+ struct KeychainAzureOpenAITokenStore: AzureOpenAITokenStoring {
25
+ private static let log = RunicLog.logger("azure-openai-token-store")
26
+
27
+ private let service = "com.sriinnu.athena.Runic"
28
+ private let account = "azure-openai-api-token"
29
+
30
+ func loadToken() throws -> String? {
31
+ // Try standard keychain first.
32
+ if let token = try self.readToken(dataProtection: false) {
33
+ return token
34
+ }
35
+ // Migrate from the old Data Protection keychain if present.
36
+ if let token = try self.readToken(dataProtection: true) {
37
+ Self.log.info("Migrating Azure OpenAI token from Data Protection keychain")
38
+ // Store first, delete old only on success — prevents data loss.
39
+ if (try? self.storeToken(token)) != nil {
40
+ try? self.deleteToken(dataProtection: true)
41
+ }
42
+ return token
43
+ }
44
+ return nil
45
+ }
46
+
47
+ func storeToken(_ token: String?) throws {
48
+ let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
49
+ if cleaned == nil || cleaned?.isEmpty == true {
50
+ try? self.deleteToken(dataProtection: false)
51
+ try? self.deleteToken(dataProtection: true)
52
+ return
53
+ }
54
+
55
+ let data = cleaned!.data(using: .utf8)!
56
+ let query = self.baseQuery(dataProtection: false)
57
+ let attributes: [String: Any] = [
58
+ kSecValueData as String: data,
59
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
+ ]
61
+
62
+ // Try update first.
63
+ let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
+ if updateStatus == errSecSuccess { return }
65
+
66
+ if updateStatus != errSecItemNotFound {
67
+ // Item exists but update failed — delete and re-add to reset ACL.
68
+ try? self.deleteToken(dataProtection: false)
69
+ }
70
+
71
+ var addQuery = query
72
+ for (key, value) in attributes { addQuery[key] = value }
73
+
74
+ // Grant the calling app permanent access so macOS never shows a
75
+ // Keychain password dialog for this item.
76
+ if let access = Self.createSelfAccess() {
77
+ addQuery[kSecAttrAccess as String] = access
78
+ }
79
+
80
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
+ guard addStatus == errSecSuccess else {
82
+ Self.log.error("Keychain add failed: \(addStatus)")
83
+ throw AzureOpenAITokenStoreError.keychainStatus(addStatus)
84
+ }
85
+ }
86
+
87
+ // MARK: - Private
88
+
89
+ private func readToken(dataProtection: Bool) throws -> String? {
90
+ var result: CFTypeRef?
91
+ var query = self.baseQuery(dataProtection: dataProtection)
92
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
93
+ query[kSecReturnData as String] = true
94
+
95
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
96
+ if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
97
+ return nil
98
+ }
99
+ guard status == errSecSuccess else {
100
+ Self.log.error("Keychain read failed: \(status)")
101
+ throw AzureOpenAITokenStoreError.keychainStatus(status)
102
+ }
103
+ guard let data = result as? Data else {
104
+ throw AzureOpenAITokenStoreError.invalidData
105
+ }
106
+ let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
107
+ return (token?.isEmpty ?? true) ? nil : token
108
+ }
109
+
110
+ private func deleteToken(dataProtection: Bool) throws {
111
+ let query = self.baseQuery(dataProtection: dataProtection)
112
+ let status = SecItemDelete(query as CFDictionary)
113
+ if status == errSecSuccess || status == errSecItemNotFound { return }
114
+ Self.log.error("Keychain delete failed: \(status)")
115
+ throw AzureOpenAITokenStoreError.keychainStatus(status)
116
+ }
117
+
118
+ private func baseQuery(dataProtection: Bool) -> [String: Any] {
119
+ var q: [String: Any] = [
120
+ kSecClass as String: kSecClassGenericPassword,
121
+ kSecAttrService as String: self.service,
122
+ kSecAttrAccount as String: self.account,
123
+ ]
124
+ if dataProtection {
125
+ q[kSecUseDataProtectionKeychain as String] = true
126
+ }
127
+ return q
128
+ }
129
+
130
+ /// Creates a SecAccess that grants the calling application permanent
131
+ /// read/write access without triggering the macOS Keychain password dialog.
132
+ private static func createSelfAccess() -> SecAccess? {
133
+ var trustedSelf: SecTrustedApplication?
134
+ guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
135
+ let trustedSelf else { return nil }
136
+ var access: SecAccess?
137
+ guard SecAccessCreate(
138
+ "Runic API Token" as CFString,
139
+ [trustedSelf] as CFArray,
140
+ &access) == errSecSuccess else { return nil }
141
+ return access
142
+ }
143
+ }