@sriinnu/runic 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +1 -1
  3. package/README.md +47 -29
  4. package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
  5. package/RuniciOS/Views/ProviderDetailView.swift +9 -9
  6. package/RuniciOS/Views/ProviderListView.swift +10 -10
  7. package/RuniciOS/Widgets/LargeWidget.swift +14 -14
  8. package/RuniciOS/Widgets/LiveActivity.swift +19 -19
  9. package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
  10. package/RuniciOS/Widgets/MediumWidget.swift +10 -10
  11. package/RuniciOS/Widgets/RunicWidget.swift +5 -5
  12. package/RuniciOS/Widgets/SmallWidget.swift +9 -9
  13. package/Scripts/package_app.sh +2 -2
  14. package/Sources/Runic/About.swift +3 -3
  15. package/Sources/Runic/AppNotifications.swift +5 -3
  16. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +17 -4
  17. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
  18. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
  19. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
  20. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +47 -14
  21. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
  22. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
  23. package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
  24. package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
  25. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
  26. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
  27. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
  28. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +3 -6
  29. package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
  30. package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
  31. package/Sources/Runic/Core/RunicColors.swift +6 -6
  32. package/Sources/Runic/Core/RunicSpacing.swift +7 -7
  33. package/Sources/Runic/Core/RunicTypography.swift +206 -0
  34. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +10 -36
  35. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +10 -36
  36. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +10 -36
  37. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +10 -36
  38. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +10 -36
  39. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
  40. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +10 -36
  41. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +10 -36
  42. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +10 -36
  43. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +10 -36
  44. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +10 -36
  45. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +10 -36
  46. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +10 -36
  47. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +10 -36
  48. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +10 -36
  49. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +10 -36
  50. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
  51. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +10 -36
  52. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +10 -36
  53. package/Sources/Runic/Core/Stores/SettingsStore.swift +99 -52
  54. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +10 -36
  55. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
  56. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
  57. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
  58. package/Sources/Runic/Core/Stores/UsageStore.swift +234 -167
  59. package/Sources/Runic/Core/Stores/XAITokenStore.swift +10 -36
  60. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +10 -36
  61. package/Sources/Runic/Core/UsageExporter.swift +8 -4
  62. package/Sources/Runic/ProviderRegistry.swift +1 -1
  63. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
  64. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
  65. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
  66. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
  67. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
  68. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
  69. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
  70. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
  71. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
  72. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
  73. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
  74. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
  75. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
  76. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
  77. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
  78. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
  79. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
  80. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
  81. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
  82. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
  83. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
  84. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
  85. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
  86. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
  87. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
  88. package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
  89. package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
  90. package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
  91. package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
  92. package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
  93. package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
  94. package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
  95. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
  96. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
  97. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
  98. package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
  99. package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
  100. package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
  101. package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
  102. package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
  103. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
  104. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
  105. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
  106. package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
  107. package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
  108. package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
  109. package/Sources/Runic/Resources/provider-context-windows.json +20 -0
  110. package/Sources/Runic/RunicApp.swift +46 -36
  111. package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
  112. package/Sources/Runic/UsagePaceText.swift +1 -1
  113. package/Sources/Runic/UsageProgressBar.swift +13 -9
  114. package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
  115. package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
  116. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
  117. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
  118. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
  119. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
  120. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
  121. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
  122. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
  123. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +15 -20
  124. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
  125. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
  126. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
  127. package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
  128. package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
  129. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
  130. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
  131. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
  132. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +198 -58
  133. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
  134. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
  135. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
  136. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
  137. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
  138. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
  139. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +28 -76
  140. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
  141. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
  142. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
  143. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
  144. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
  145. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
  146. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
  147. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
  148. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
  149. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
  150. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
  151. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
  152. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
  153. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
  154. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +108 -33
  155. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
  156. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
  157. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
  158. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
  159. package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
  160. package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
  161. package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
  162. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
  163. package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
  164. package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
  165. package/Sources/RunicCLI/CLIEntry.swift +83 -42
  166. package/Sources/RunicCLI/CLIRenderer.swift +1 -1
  167. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
  168. package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
  169. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
  170. package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
  171. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
  172. package/Sources/RunicClaudeWebProbe/main.swift +1 -1
  173. package/Sources/RunicCore/AlertRuleStore.swift +20 -20
  174. package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
  175. package/Sources/RunicCore/CostUsageModels.swift +1 -1
  176. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
  177. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +80 -71
  178. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
  179. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
  180. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
  181. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
  182. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
  183. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
  184. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
  185. package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
  186. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
  187. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
  188. package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
  189. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
  190. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
  191. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
  192. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
  193. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
  194. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +5 -3
  195. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
  196. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
  197. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
  198. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
  199. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
  200. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
  201. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
  202. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
  203. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
  204. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
  205. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
  206. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
  207. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
  208. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
  209. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
  210. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
  211. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
  212. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
  213. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
  214. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
  215. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
  216. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
  217. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
  218. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
  219. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
  220. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
  221. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +3 -2
  222. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
  223. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
  224. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
  225. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +6 -2
  226. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
  227. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
  228. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
  229. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
  230. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
  231. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +8 -11
  232. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
  233. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
  234. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
  235. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
  236. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +25 -16
  237. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
  238. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
  239. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
  240. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
  241. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
  242. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +27 -16
  243. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
  244. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
  245. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
  246. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
  247. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
  248. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
  249. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
  250. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
  251. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
  252. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
  253. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +51 -30
  254. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
  255. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
  256. package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
  257. package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
  258. package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
  259. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
  260. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
  261. package/Sources/RunicCore/Team/TeamModels.swift +111 -110
  262. package/Sources/RunicCore/Team/TeamStore.swift +126 -109
  263. package/Sources/RunicCore/UsageFetcher.swift +2 -2
  264. package/Sources/RunicCore/UsageFormatter.swift +25 -25
  265. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +141 -29
  266. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +59 -5
  267. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +161 -0
  268. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
  269. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
  270. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
  271. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
  272. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
  273. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
  274. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
  275. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
  276. package/Tests/RunicTests/AppDelegateTests.swift +8 -3
  277. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
  278. package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
  279. package/Tests/RunicTests/CLICostTests.swift +6 -7
  280. package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
  281. package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
  282. package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
  283. package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
  284. package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
  285. package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
  286. package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
  287. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
  288. package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
  289. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
  290. package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
  291. package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
  292. package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
  293. package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
  294. package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
  295. package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
  296. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
  297. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
  298. package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
  299. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
  300. package/Tests/RunicTests/InstallOriginTests.swift +1 -2
  301. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
  302. package/Tests/RunicTests/LiveAccountTests.swift +3 -3
  303. package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
  304. package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
  305. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
  306. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
  307. package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
  308. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
  309. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
  310. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
  311. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
  312. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
  313. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
  314. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
  315. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
  316. package/Tests/RunicTests/PathBuilderTests.swift +14 -15
  317. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
  318. package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
  319. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
  320. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
  321. package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
  322. package/Tests/RunicTests/RunicTests.swift +6 -7
  323. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
  324. package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
  325. package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
  326. package/Tests/RunicTests/StatusMenuTests.swift +63 -22
  327. package/Tests/RunicTests/StatusProbeTests.swift +28 -29
  328. package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
  329. package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
  330. package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
  331. package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
  332. package/Tests/RunicTests/TextParsingTests.swift +1 -2
  333. package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
  334. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
  335. package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
  336. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
  337. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
  338. package/Tests/RunicTests/UsagePaceTests.swift +5 -6
  339. package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
  340. package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
  341. package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
  342. package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
  343. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
  344. package/appcast.xml +36 -37
  345. package/package.json +1 -1
  346. package/runic.png +0 -0
  347. package/runic.svg +15 -0
  348. package/version.env +2 -2
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol CerebrasTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainCerebrasTokenStore: CerebrasTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainCerebrasTokenStore: CerebrasTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainCerebrasTokenStore: CerebrasTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol CohereTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainCohereTokenStore: CohereTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainCohereTokenStore: CohereTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainCohereTokenStore: CohereTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol CopilotTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainCopilotTokenStore: CopilotTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainCopilotTokenStore: CopilotTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainCopilotTokenStore: CopilotTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -12,20 +12,20 @@ enum CustomProviderStoreError: LocalizedError {
12
12
  var errorDescription: String? {
13
13
  switch self {
14
14
  case let .providerNotFound(id):
15
- return "Provider with ID '\(id)' not found."
15
+ "Provider with ID '\(id)' not found."
16
16
  case let .duplicateProvider(id):
17
- return "Provider with ID '\(id)' already exists."
17
+ "Provider with ID '\(id)' already exists."
18
18
  case .invalidData:
19
- return "Invalid provider data."
19
+ "Invalid provider data."
20
20
  case let .fileSystemError(message):
21
- return "File system error: \(message)"
21
+ "File system error: \(message)"
22
22
  }
23
23
  }
24
24
  }
25
25
 
26
26
  // MARK: - Store
27
27
 
28
- public struct CustomProviderStore {
28
+ public enum CustomProviderStore {
29
29
  private static let log = RunicLog.logger("custom-provider-store")
30
30
 
31
31
  // MARK: - Storage Location
@@ -41,8 +41,8 @@ public struct CustomProviderStore {
41
41
 
42
42
  /// Load all custom providers from disk.
43
43
  public static func load() -> CustomProvidersData {
44
- guard FileManager.default.fileExists(atPath: storageURL.path) else {
45
- Self.log.info("No custom providers file found, returning empty data.")
44
+ guard FileManager.default.fileExists(atPath: self.storageURL.path) else {
45
+ self.log.info("No custom providers file found, returning empty data.")
46
46
  return CustomProvidersData()
47
47
  }
48
48
 
@@ -54,7 +54,7 @@ public struct CustomProviderStore {
54
54
  Self.log.info("Loaded \(providersData.providers.count) custom provider(s).")
55
55
  return providersData
56
56
  } catch {
57
- Self.log.error("Failed to load custom providers: \(error)")
57
+ self.log.error("Failed to load custom providers: \(error)")
58
58
  return CustomProvidersData()
59
59
  }
60
60
  }
@@ -67,7 +67,7 @@ public struct CustomProviderStore {
67
67
 
68
68
  do {
69
69
  let data = try encoder.encode(providersData)
70
- try data.write(to: storageURL, options: .atomic)
70
+ try data.write(to: self.storageURL, options: .atomic)
71
71
  Self.log.info("Saved \(providersData.providers.count) custom provider(s).")
72
72
  } catch {
73
73
  Self.log.error("Failed to save custom providers: \(error)")
@@ -153,6 +153,6 @@ public struct CustomProviderStore {
153
153
  /// - Returns: Array of enabled custom provider configurations.
154
154
  public static func getEnabledProviders() -> [CustomProviderConfig] {
155
155
  let data = Self.load()
156
- return data.providers.filter { $0.enabled }
156
+ return data.providers.filter(\.enabled)
157
157
  }
158
158
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol DeepSeekTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainDeepSeekTokenStore: DeepSeekTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainDeepSeekTokenStore: DeepSeekTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainDeepSeekTokenStore: DeepSeekTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol FireworksTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainFireworksTokenStore: FireworksTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainFireworksTokenStore: FireworksTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainFireworksTokenStore: FireworksTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol GroqTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainGroqTokenStore: GroqTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainGroqTokenStore: GroqTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainGroqTokenStore: GroqTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol KimiTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainKimiTokenStore: KimiTokenStoring {
53
53
  }
54
54
 
55
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
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainKimiTokenStore: KimiTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainKimiTokenStore: KimiTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol MiniMaxCookieHeaderStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
53
53
  }
54
54
 
55
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.deleteHeader(dataProtection: false)
69
- }
70
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteHeader(dataProtection: false)
61
+ try? self.deleteHeader(dataProtection: true)
73
62
 
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
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
126
114
  }
127
115
  return q
128
116
  }
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
117
  }