@sriinnu/runic 1.2.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 (342) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +8 -1
  3. package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
  4. package/RuniciOS/Views/ProviderDetailView.swift +9 -9
  5. package/RuniciOS/Views/ProviderListView.swift +10 -10
  6. package/RuniciOS/Widgets/LargeWidget.swift +14 -14
  7. package/RuniciOS/Widgets/LiveActivity.swift +19 -19
  8. package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
  9. package/RuniciOS/Widgets/MediumWidget.swift +10 -10
  10. package/RuniciOS/Widgets/RunicWidget.swift +5 -5
  11. package/RuniciOS/Widgets/SmallWidget.swift +9 -9
  12. package/Sources/Runic/About.swift +3 -3
  13. package/Sources/Runic/AppNotifications.swift +5 -3
  14. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +7 -3
  15. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
  16. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
  17. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
  18. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +18 -13
  19. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
  20. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
  21. package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
  22. package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
  23. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
  24. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
  25. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
  26. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +1 -1
  27. package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
  28. package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
  29. package/Sources/Runic/Core/RunicColors.swift +6 -6
  30. package/Sources/Runic/Core/RunicSpacing.swift +7 -7
  31. package/Sources/Runic/Core/RunicTypography.swift +206 -0
  32. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +1 -3
  33. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +1 -3
  34. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +1 -3
  35. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +1 -3
  36. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +1 -3
  37. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
  38. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +1 -3
  39. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +1 -3
  40. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +1 -3
  41. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +1 -3
  42. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +1 -3
  43. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +1 -3
  44. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +1 -3
  45. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +1 -3
  46. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +1 -3
  47. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +1 -3
  48. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
  49. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +1 -3
  50. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +1 -3
  51. package/Sources/Runic/Core/Stores/SettingsStore.swift +89 -51
  52. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +1 -3
  53. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
  54. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
  55. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
  56. package/Sources/Runic/Core/Stores/UsageStore.swift +187 -161
  57. package/Sources/Runic/Core/Stores/XAITokenStore.swift +1 -3
  58. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +1 -2
  59. package/Sources/Runic/Core/UsageExporter.swift +8 -4
  60. package/Sources/Runic/ProviderRegistry.swift +1 -1
  61. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
  62. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
  63. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
  64. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
  65. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
  66. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
  67. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
  68. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
  69. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
  70. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
  71. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
  72. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
  73. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
  74. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
  75. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
  76. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
  77. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
  78. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
  79. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
  80. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
  81. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
  82. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
  83. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
  84. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
  85. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
  86. package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
  87. package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
  88. package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
  89. package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
  90. package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
  91. package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
  92. package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
  93. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
  94. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
  95. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
  96. package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
  97. package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
  98. package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
  99. package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
  100. package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
  101. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
  102. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
  103. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
  104. package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
  105. package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
  106. package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
  107. package/Sources/Runic/RunicApp.swift +46 -36
  108. package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
  109. package/Sources/Runic/UsagePaceText.swift +1 -1
  110. package/Sources/Runic/UsageProgressBar.swift +13 -9
  111. package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
  112. package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
  113. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
  114. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
  115. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
  116. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
  117. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
  118. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
  119. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
  120. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +12 -10
  121. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
  122. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
  123. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
  124. package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
  125. package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
  126. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
  127. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
  128. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
  129. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +18 -15
  130. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
  131. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
  132. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
  133. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
  134. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
  135. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
  136. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +22 -17
  137. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
  138. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
  139. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
  140. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
  141. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
  142. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
  143. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
  144. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
  145. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
  146. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
  147. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
  148. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
  149. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
  150. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
  151. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +44 -39
  152. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
  153. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
  154. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
  155. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
  156. package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
  157. package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
  158. package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
  159. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
  160. package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
  161. package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
  162. package/Sources/RunicCLI/CLIEntry.swift +83 -42
  163. package/Sources/RunicCLI/CLIRenderer.swift +1 -1
  164. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
  165. package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
  166. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
  167. package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
  168. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
  169. package/Sources/RunicClaudeWebProbe/main.swift +1 -1
  170. package/Sources/RunicCore/AlertRuleStore.swift +20 -20
  171. package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
  172. package/Sources/RunicCore/CostUsageModels.swift +1 -1
  173. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
  174. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +73 -75
  175. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
  176. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
  177. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
  178. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
  179. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
  180. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
  181. package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
  182. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
  183. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
  184. package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
  185. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
  186. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
  187. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
  188. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
  189. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
  190. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +3 -3
  191. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
  192. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
  193. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
  194. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
  195. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
  196. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
  197. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
  198. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
  199. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
  200. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +1 -1
  201. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
  202. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
  203. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
  204. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
  205. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
  206. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
  207. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
  208. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
  209. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
  210. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
  211. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
  212. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
  213. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
  214. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
  215. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +2 -2
  216. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
  217. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
  218. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
  219. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +5 -2
  220. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
  221. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
  222. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
  223. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
  224. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
  225. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +7 -7
  226. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
  227. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
  228. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
  229. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
  230. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +23 -10
  231. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
  232. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
  233. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
  234. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
  235. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
  236. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +26 -13
  237. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
  238. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
  239. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
  240. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
  241. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
  242. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
  243. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
  244. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
  245. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
  246. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
  247. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +48 -21
  248. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
  249. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
  250. package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
  251. package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
  252. package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
  253. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
  254. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
  255. package/Sources/RunicCore/Team/TeamModels.swift +111 -110
  256. package/Sources/RunicCore/Team/TeamStore.swift +126 -109
  257. package/Sources/RunicCore/UsageFetcher.swift +2 -2
  258. package/Sources/RunicCore/UsageFormatter.swift +25 -25
  259. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +27 -13
  260. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +11 -3
  261. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +5 -2
  262. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
  263. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
  264. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
  265. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
  266. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
  267. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
  268. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
  269. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
  270. package/Tests/RunicTests/AppDelegateTests.swift +8 -3
  271. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
  272. package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
  273. package/Tests/RunicTests/CLICostTests.swift +6 -7
  274. package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
  275. package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
  276. package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
  277. package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
  278. package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
  279. package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
  280. package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
  281. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
  282. package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
  283. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
  284. package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
  285. package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
  286. package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
  287. package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
  288. package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
  289. package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
  290. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
  291. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
  292. package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
  293. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
  294. package/Tests/RunicTests/InstallOriginTests.swift +1 -2
  295. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
  296. package/Tests/RunicTests/LiveAccountTests.swift +3 -3
  297. package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
  298. package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
  299. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
  300. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
  301. package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
  302. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
  303. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
  304. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
  305. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
  306. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
  307. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
  308. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
  309. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
  310. package/Tests/RunicTests/PathBuilderTests.swift +14 -15
  311. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
  312. package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
  313. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
  314. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
  315. package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
  316. package/Tests/RunicTests/RunicTests.swift +6 -7
  317. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
  318. package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
  319. package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
  320. package/Tests/RunicTests/StatusMenuTests.swift +63 -22
  321. package/Tests/RunicTests/StatusProbeTests.swift +28 -29
  322. package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
  323. package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
  324. package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
  325. package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
  326. package/Tests/RunicTests/TextParsingTests.swift +1 -2
  327. package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
  328. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
  329. package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
  330. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
  331. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
  332. package/Tests/RunicTests/UsagePaceTests.swift +5 -6
  333. package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
  334. package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
  335. package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
  336. package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
  337. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
  338. package/appcast.xml +36 -76
  339. package/package.json +1 -1
  340. package/runic.png +0 -0
  341. package/runic.svg +15 -0
  342. package/version.env +2 -2
@@ -97,7 +97,7 @@ public enum UsageFormatter {
97
97
  guard tokenCount > 0 else { return nil }
98
98
  let rate = costUSD / (Double(tokenCount) / 1000.0)
99
99
  guard rate.isFinite, rate >= 0 else { return nil }
100
- return "\(usdRateString(rate))/1K"
100
+ return "\(self.usdRateString(rate))/1K"
101
101
  }
102
102
 
103
103
  public static func usdPerRequestString(costUSD: Double?, requestCount: Int) -> String? {
@@ -105,7 +105,7 @@ public enum UsageFormatter {
105
105
  guard requestCount > 0 else { return nil }
106
106
  let rate = costUSD / Double(requestCount)
107
107
  guard rate.isFinite, rate >= 0 else { return nil }
108
- return "\(usdRateString(rate))/req"
108
+ return "\(self.usdRateString(rate))/req"
109
109
  }
110
110
 
111
111
  public static func usdPerHourFromTokensString(
@@ -120,7 +120,7 @@ public enum UsageFormatter {
120
120
  guard per1KValue.isFinite, per1KValue >= 0 else { return nil }
121
121
  let hourly = per1KValue * (tokensPerMinute * 60.0 / 1000.0)
122
122
  guard hourly.isFinite, hourly >= 0 else { return nil }
123
- return "\(usdRateString(hourly))/hr"
123
+ return "\(self.usdRateString(hourly))/hr"
124
124
  }
125
125
 
126
126
  public static func currencyString(_ value: Double, currencyCode: String) -> String {
@@ -164,21 +164,21 @@ public enum UsageFormatter {
164
164
  }
165
165
 
166
166
  public static func tokenSummaryString(_ totals: UsageLedgerTotals, includeBreakdown: Bool = true) -> String {
167
- let total = tokenCountString(totals.totalTokens)
167
+ let total = self.tokenCountString(totals.totalTokens)
168
168
  guard includeBreakdown else {
169
169
  return "\(total) tok"
170
170
  }
171
171
 
172
172
  var parts: [String] = []
173
173
  if totals.inputTokens > 0 {
174
- parts.append("in \(tokenCountString(totals.inputTokens))")
174
+ parts.append("in \(self.tokenCountString(totals.inputTokens))")
175
175
  }
176
176
  if totals.outputTokens > 0 {
177
- parts.append("out \(tokenCountString(totals.outputTokens))")
177
+ parts.append("out \(self.tokenCountString(totals.outputTokens))")
178
178
  }
179
179
  let cached = totals.cacheCreationTokens + totals.cacheReadTokens
180
180
  if cached > 0 {
181
- parts.append("cache \(tokenCountString(cached))")
181
+ parts.append("cache \(self.tokenCountString(cached))")
182
182
  }
183
183
  guard !parts.isEmpty else { return "\(total) tok" }
184
184
  return "\(total) tok (\(parts.joined(separator: ", ")))"
@@ -248,8 +248,8 @@ public enum UsageFormatter {
248
248
  /// Returns a human-friendly context window label for a model when known.
249
249
  /// Examples: "ctx 128k", "ctx 1M".
250
250
  public static func modelContextLabel(for model: String) -> String? {
251
- guard let context = Self.modelContextWindow(for: model) else { return nil }
252
- return "ctx \(tokenCountString(context))"
251
+ guard let context = modelContextWindow(for: model) else { return nil }
252
+ return "ctx \(self.tokenCountString(context))"
253
253
  }
254
254
 
255
255
  /// Resolves an approximate context window (in tokens) for common model families.
@@ -285,7 +285,7 @@ public enum UsageFormatter {
285
285
  "gpt-4o-mini": 128_000,
286
286
  "gpt-4-turbo": 128_000,
287
287
  "gpt-4": 128_000,
288
- "gpt-3.5-turbo": 16_385,
288
+ "gpt-3.5-turbo": 16385,
289
289
  "gpt-4.1": 1_000_000,
290
290
  "gpt-4.1-mini": 1_000_000,
291
291
  "gpt-4.1-nano": 1_000_000,
@@ -315,33 +315,33 @@ public enum UsageFormatter {
315
315
  "llama-3-1-8b": 128_000,
316
316
  "llama-3-3-70b": 128_000,
317
317
  "mistral-large": 128_000,
318
- "mistral-small": 32_000,
318
+ "mistral-small": 32000,
319
319
  "mistral-medium": 131_072,
320
- "deepseek-chat": 64_000,
321
- "deepseek-coder": 32_000,
320
+ "deepseek-chat": 64000,
321
+ "deepseek-coder": 32000,
322
322
  "cohere-command": 128_000,
323
323
  "cohere-command-r": 128_000,
324
324
  "cohere-command-r-plus": 128_000,
325
325
  "qwen-2-5-72b": 128_000,
326
326
  "qwen-2-5-32b": 128_000,
327
- "qwen1-5-32b": 8_192,
328
- "qwen1-5-14b": 8_192,
327
+ "qwen1-5-32b": 8192,
328
+ "qwen1-5-14b": 8192,
329
329
  "grok-2": 128_000,
330
330
  "grok-2-mini": 131_072,
331
331
  "grok-2-vision": 131_072,
332
- "mixtral-8x7b": 32_768,
333
- "mixtral-8x22b": 64_000,
334
- "llama-3-8b": 8_192,
335
- "llama-3-70b": 8_192,
336
- "llama-2-70b": 4_096,
337
- "llama-2-13b": 4_096,
332
+ "mixtral-8x7b": 32768,
333
+ "mixtral-8x22b": 64000,
334
+ "llama-3-8b": 8192,
335
+ "llama-3-70b": 8192,
336
+ "llama-2-70b": 4096,
337
+ "llama-2-13b": 4096,
338
338
  ]
339
339
 
340
340
  private static let modelContextPrefixes: [(String, Int)] = [
341
341
  ("gpt-4.1", 1_000_000),
342
342
  ("gpt-4o", 128_000),
343
343
  ("gpt-4-", 128_000),
344
- ("gpt-3.5", 16_385),
344
+ ("gpt-3.5", 16385),
345
345
  ("gpt-5", 400_000),
346
346
  ("gpt-5-", 400_000),
347
347
  ("claude-3", 200_000),
@@ -351,9 +351,9 @@ public enum UsageFormatter {
351
351
  ("gemini-1-5", 1_000_000),
352
352
  ("gemini-2-0", 1_000_000),
353
353
  ("gemini-2-5", 1_000_000),
354
- ("llama-3", 8_192),
354
+ ("llama-3", 8192),
355
355
  ("qwen-2-5", 128_000),
356
- ("qwen1-5", 8_192),
356
+ ("qwen1-5", 8192),
357
357
  ]
358
358
 
359
359
  private static func normalizedModelIdentifier(for model: String) -> String {
@@ -370,7 +370,7 @@ public enum UsageFormatter {
370
370
  for token in tokens.reversed() {
371
371
  let lower = token.lowercased()
372
372
  if lower.hasSuffix("k"), let numeric = Int(lower.dropLast()), numeric > 0 {
373
- return numeric * 1_000
373
+ return numeric * 1000
374
374
  }
375
375
  if lower.hasSuffix("m"), let numeric = Int(lower.dropLast()), numeric > 0 {
376
376
  return numeric * 1_000_000
@@ -56,7 +56,9 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
56
56
  // Filter to only files modified since last scan (plus today's files which may have grown).
57
57
  let todayStart = Calendar.current.startOfDay(for: self.now)
58
58
  let filesToScan = allFiles.filter { file in
59
- guard let modDate = (try? self.fileManager.attributesOfItem(atPath: file.url.path))?[.modificationDate] as? Date else {
59
+ guard let modDate = (try? self.fileManager
60
+ .attributesOfItem(atPath: file.url.path))?[.modificationDate] as? Date
61
+ else {
60
62
  return true
61
63
  }
62
64
  return modDate >= incrementalCutoff || modDate >= todayStart
@@ -66,7 +68,9 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
66
68
  return []
67
69
  }
68
70
 
69
- self.log.info("Scanning \(filesToScan.count) of \(allFiles.count) JSONL files (incremental since \(incrementalCutoff))")
71
+ self.log
72
+ .info(
73
+ "Scanning \(filesToScan.count) of \(allFiles.count) JSONL files (incremental since \(incrementalCutoff))")
70
74
 
71
75
  // Parse files concurrently (max 16 at a time).
72
76
  let maxConcurrency = 16
@@ -118,7 +122,14 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
118
122
  dailyFormatter.dateFormat = "yyyy-MM-dd"
119
123
  dailyFormatter.timeZone = .current
120
124
 
121
- var dailyBuckets: [String: (input: Int, output: Int, cacheCreate: Int, cacheRead: Int, cost: Double, requests: Int, models: Set<String>)] = [:]
125
+ var dailyBuckets: [String: (
126
+ input: Int,
127
+ output: Int,
128
+ cacheCreate: Int,
129
+ cacheRead: Int,
130
+ cost: Double,
131
+ requests: Int,
132
+ models: Set<String>)] = [:]
122
133
  for entry in entries {
123
134
  let key = dailyFormatter.string(from: entry.timestamp)
124
135
  var bucket = dailyBuckets[key] ?? (0, 0, 0, 0, 0, 0, [])
@@ -188,24 +199,22 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
188
199
 
189
200
  var candidates: [URL] = []
190
201
  if !envPaths.isEmpty {
191
- candidates = envPaths.map { expandTilde(path: String($0)) }
202
+ candidates = envPaths.map { self.expandTilde(path: String($0)) }
192
203
  } else {
193
- let home = fileManager.homeDirectoryForCurrentUser
204
+ let home = self.fileManager.homeDirectoryForCurrentUser
194
205
  candidates = [
195
206
  home.appendingPathComponent(".config/claude", isDirectory: true),
196
207
  home.appendingPathComponent(".claude", isDirectory: true),
197
208
  ]
198
209
  }
199
210
 
200
- let projectsDirs = candidates
211
+ return candidates
201
212
  .map { $0.appendingPathComponent("projects", isDirectory: true) }
202
213
  .filter { self.fileManager.directoryExists(at: $0) }
203
-
204
- return projectsDirs
205
214
  }
206
215
 
207
216
  private func normalizePaths(_ urls: [URL]) -> [URL] {
208
- urls.map { $0.standardizedFileURL }
217
+ urls.map(\.standardizedFileURL)
209
218
  .filter { self.fileManager.directoryExists(at: $0) }
210
219
  .map { $0.appendingPathComponent("projects", isDirectory: true) }
211
220
  .filter { self.fileManager.directoryExists(at: $0) }
@@ -236,7 +245,11 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
236
245
  let projectID = pathComponents[projectsIndex + 1]
237
246
  let projectName = self.derivedProjectName(from: projectID)
238
247
  let sessionID = pathComponents[projectsIndex + 2]
239
- results.append(UsageFile(url: fileURL, projectID: projectID, projectName: projectName, sessionID: sessionID))
248
+ results.append(UsageFile(
249
+ url: fileURL,
250
+ projectID: projectID,
251
+ projectName: projectName,
252
+ sessionID: sessionID))
240
253
  }
241
254
  }
242
255
 
@@ -355,7 +368,7 @@ public struct ClaudeUsageLogSource: UsageLedgerSource, @unchecked Sendable {
355
368
 
356
369
  private func expandTilde(path: String) -> URL {
357
370
  if path.hasPrefix("~/") {
358
- let home = fileManager.homeDirectoryForCurrentUser
371
+ let home = self.fileManager.homeDirectoryForCurrentUser
359
372
  let trimmed = String(path.dropFirst(2))
360
373
  return home.appendingPathComponent(trimmed, isDirectory: true)
361
374
  }
@@ -410,6 +423,7 @@ private final class ClaudeISO8601FormatterBox: @unchecked Sendable {
410
423
  formatter.formatOptions = [.withInternetDateTime]
411
424
  return formatter
412
425
  }()
426
+
413
427
  let fractional: ISO8601DateFormatter = {
414
428
  let formatter = ISO8601DateFormatter()
415
429
  formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -419,8 +433,8 @@ private final class ClaudeISO8601FormatterBox: @unchecked Sendable {
419
433
 
420
434
  private let claudeISOFormatterBox = ClaudeISO8601FormatterBox()
421
435
 
422
- private extension FileManager {
423
- func directoryExists(at url: URL) -> Bool {
436
+ extension FileManager {
437
+ fileprivate func directoryExists(at url: URL) -> Bool {
424
438
  var isDir: ObjCBool = false
425
439
  return self.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue
426
440
  }
@@ -123,7 +123,14 @@ public struct CodexUsageLogSource: UsageLedgerSource, @unchecked Sendable {
123
123
  dailyFormatter.dateFormat = "yyyy-MM-dd"
124
124
  dailyFormatter.timeZone = .current
125
125
 
126
- var dailyBuckets: [String: (input: Int, output: Int, cacheCreate: Int, cacheRead: Int, cost: Double, requests: Int, models: Set<String>)] = [:]
126
+ var dailyBuckets: [String: (
127
+ input: Int,
128
+ output: Int,
129
+ cacheCreate: Int,
130
+ cacheRead: Int,
131
+ cost: Double,
132
+ requests: Int,
133
+ models: Set<String>)] = [:]
127
134
  for entry in entries {
128
135
  let key = dailyFormatter.string(from: entry.timestamp)
129
136
  var bucket = dailyBuckets[key] ?? (0, 0, 0, 0, 0, 0, [])
@@ -477,6 +484,7 @@ private final class CodexISO8601FormatterBox: @unchecked Sendable {
477
484
  formatter.formatOptions = [.withInternetDateTime]
478
485
  return formatter
479
486
  }()
487
+
480
488
  let fractional: ISO8601DateFormatter = {
481
489
  let formatter = ISO8601DateFormatter()
482
490
  formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -486,8 +494,8 @@ private final class CodexISO8601FormatterBox: @unchecked Sendable {
486
494
 
487
495
  private let codexISOFormatterBox = CodexISO8601FormatterBox()
488
496
 
489
- private extension FileManager {
490
- func directoryExists(at url: URL) -> Bool {
497
+ extension FileManager {
498
+ fileprivate func directoryExists(at url: URL) -> Bool {
491
499
  var isDir: ObjCBool = false
492
500
  return self.fileExists(atPath: url.path, isDirectory: &isDir) && isDir.boolValue
493
501
  }
@@ -49,7 +49,10 @@ public actor LedgerCache {
49
49
  /// - Today's entry is ACCUMULATED (adds to existing totals for partial scans)
50
50
  /// - New days are appended
51
51
  public func mergeDailies(provider: String, newDailies: [CachedDaily], scanDate: Date, todayKey: String? = nil) {
52
- var ledger = self.loadCachedDailies(provider: provider) ?? CachedLedger(lastScanDate: scanDate, lastFullScanDate: nil, dailies: [])
52
+ var ledger = self.loadCachedDailies(provider: provider) ?? CachedLedger(
53
+ lastScanDate: scanDate,
54
+ lastFullScanDate: nil,
55
+ dailies: [])
53
56
 
54
57
  let resolvedTodayKey = todayKey ?? Self.dayKey(for: Date())
55
58
  var existingByKey: [String: CachedDaily] = [:]
@@ -119,7 +122,7 @@ public struct CachedLedger: Codable, Sendable {
119
122
  }
120
123
 
121
124
  public struct CachedDaily: Codable, Sendable {
122
- public let dayKey: String // "2026-03-23"
125
+ public let dayKey: String // "2026-03-23"
123
126
  public let inputTokens: Int
124
127
  public let outputTokens: Int
125
128
  public let cacheCreationTokens: Int
@@ -29,7 +29,7 @@ public enum OTelGenAILedgerAdapterError: LocalizedError, Sendable, Equatable {
29
29
  switch self {
30
30
  case .invalidUTF8:
31
31
  "OTel payload is not valid UTF-8."
32
- case .invalidJSON(let message):
32
+ case let .invalidJSON(message):
33
33
  "OTel JSON parse failed: \(message)"
34
34
  }
35
35
  }
@@ -46,7 +46,7 @@ public enum UsageLedgerAggregator {
46
46
  timeZone: TimeZone,
47
47
  groupByProject: Bool = true) -> [UsageLedgerDailySummary]
48
48
  {
49
- let calendar = calendarFor(timeZone)
49
+ let calendar = self.calendarFor(timeZone)
50
50
  var buckets: [DailyKey: AggregateAccumulator] = [:]
51
51
 
52
52
  for entry in entries {
@@ -59,7 +59,7 @@ public enum UsageLedgerAggregator {
59
59
  }
60
60
 
61
61
  return buckets.map { key, acc in
62
- let dayKey = dayKeyString(from: key.dayStart, timeZone: timeZone)
62
+ let dayKey = self.dayKeyString(from: key.dayStart, timeZone: timeZone)
63
63
  return UsageLedgerDailySummary(
64
64
  provider: key.provider,
65
65
  projectID: key.projectID,
@@ -138,7 +138,10 @@ public enum UsageLedgerAggregator {
138
138
  let isActive = now <= blockEnd && now.timeIntervalSince(lastTimestamp) <= blockDuration
139
139
  let acc = AggregateAccumulator(entries: currentEntries)
140
140
  let tokensPerMinute = isActive ? acc.tokensPerMinute(since: currentStart, now: now) : nil
141
- let projected = isActive ? acc.projectedTotalTokens(since: currentStart, now: now, duration: blockDuration) : nil
141
+ let projected = isActive ? acc.projectedTotalTokens(
142
+ since: currentStart,
143
+ now: now,
144
+ duration: blockDuration) : nil
142
145
  summaries.append(UsageLedgerBlockSummary(
143
146
  provider: key.provider,
144
147
  sessionID: key.sessionID,
@@ -283,7 +286,7 @@ public enum UsageLedgerAggregator {
283
286
  groupByProject: Bool) -> [UsageLedgerSpendForecast]
284
287
  {
285
288
  guard projectionDays > 0 else { return [] }
286
- let calendar = calendarFor(timeZone)
289
+ let calendar = self.calendarFor(timeZone)
287
290
  var buckets: [SpendForecastKey: SpendForecastAccumulator] = [:]
288
291
 
289
292
  for entry in entries {
@@ -334,7 +337,7 @@ public enum UsageLedgerAggregator {
334
337
 
335
338
  private static func dayKeyString(from date: Date, timeZone: TimeZone) -> String {
336
339
  let formatter = DateFormatter()
337
- formatter.calendar = calendarFor(timeZone)
340
+ formatter.calendar = self.calendarFor(timeZone)
338
341
  formatter.locale = Locale(identifier: "en_US_POSIX")
339
342
  formatter.timeZone = timeZone
340
343
  formatter.dateFormat = "yyyy-MM-dd"
@@ -343,7 +346,7 @@ public enum UsageLedgerAggregator {
343
346
 
344
347
  private static func hourKeyString(from date: Date, timeZone: TimeZone) -> String {
345
348
  let formatter = DateFormatter()
346
- formatter.calendar = calendarFor(timeZone)
349
+ formatter.calendar = self.calendarFor(timeZone)
347
350
  formatter.locale = Locale(identifier: "en_US_POSIX")
348
351
  formatter.timeZone = timeZone
349
352
  formatter.dateFormat = "yyyy-MM-dd'T'HH:00:00"
@@ -355,7 +358,7 @@ public enum UsageLedgerAggregator {
355
358
  timeZone: TimeZone,
356
359
  groupByProject: Bool = true) -> [UsageLedgerHourlySummary]
357
360
  {
358
- let calendar = calendarFor(timeZone)
361
+ let calendar = self.calendarFor(timeZone)
359
362
  var buckets: [HourlyKey: AggregateAccumulator] = [:]
360
363
 
361
364
  for entry in entries {
@@ -371,7 +374,7 @@ public enum UsageLedgerAggregator {
371
374
  }
372
375
 
373
376
  return buckets.map { key, acc in
374
- let hourKey = hourKeyString(from: key.hourStart, timeZone: timeZone)
377
+ let hourKey = self.hourKeyString(from: key.hourStart, timeZone: timeZone)
375
378
  return UsageLedgerHourlySummary(
376
379
  provider: key.provider,
377
380
  projectID: key.projectID,
@@ -445,7 +448,10 @@ private struct AggregateAccumulator {
445
448
  self.modelSet.sorted()
446
449
  }
447
450
 
448
- private mutating func mergeProjectIdentity(from identity: UsageLedgerProjectIdentity?, fallbackEntry: UsageLedgerEntry) {
451
+ private mutating func mergeProjectIdentity(
452
+ from identity: UsageLedgerProjectIdentity?,
453
+ fallbackEntry: UsageLedgerEntry)
454
+ {
449
455
  if let key = identity?.key, self.projectKey == nil {
450
456
  self.projectKey = key
451
457
  }
@@ -468,17 +474,16 @@ private struct AggregateAccumulator {
468
474
  let candidateProvenance = identity?.provenance
469
475
 
470
476
  guard let candidateName, !candidateName.isEmpty else { return }
471
- let shouldAdopt: Bool
472
- if self.projectName == nil {
473
- shouldAdopt = true
477
+ let shouldAdopt = if self.projectName == nil {
478
+ true
474
479
  } else if candidateConfidence.rank > self.projectNameConfidence.rank {
475
- shouldAdopt = true
480
+ true
476
481
  } else if candidateConfidence.rank == self.projectNameConfidence.rank,
477
482
  candidateName.count > (self.projectName?.count ?? 0)
478
483
  {
479
- shouldAdopt = true
484
+ true
480
485
  } else {
481
- shouldAdopt = false
486
+ false
482
487
  }
483
488
 
484
489
  if shouldAdopt {
@@ -586,7 +591,10 @@ private struct SpendForecastAccumulator {
586
591
  return lowerValue + ((upperValue - lowerValue) * weight)
587
592
  }
588
593
 
589
- private mutating func mergeProjectIdentity(from identity: UsageLedgerProjectIdentity?, fallbackEntry: UsageLedgerEntry) {
594
+ private mutating func mergeProjectIdentity(
595
+ from identity: UsageLedgerProjectIdentity?,
596
+ fallbackEntry: UsageLedgerEntry)
597
+ {
590
598
  if self.projectID == nil {
591
599
  let preferredID = identity?.projectID?.trimmingCharacters(in: .whitespacesAndNewlines)
592
600
  if let preferredID, !preferredID.isEmpty {
@@ -605,17 +613,16 @@ private struct SpendForecastAccumulator {
605
613
  let candidateProvenance = identity?.provenance
606
614
 
607
615
  guard let candidateName, !candidateName.isEmpty else { return }
608
- let shouldAdopt: Bool
609
- if self.projectName == nil {
610
- shouldAdopt = true
616
+ let shouldAdopt = if self.projectName == nil {
617
+ true
611
618
  } else if candidateConfidence.rank > self.projectNameConfidence.rank {
612
- shouldAdopt = true
619
+ true
613
620
  } else if candidateConfidence.rank == self.projectNameConfidence.rank,
614
621
  candidateName.count > (self.projectName?.count ?? 0)
615
622
  {
616
- shouldAdopt = true
623
+ true
617
624
  } else {
618
- shouldAdopt = false
625
+ false
619
626
  }
620
627
 
621
628
  if shouldAdopt {
@@ -654,7 +661,15 @@ private struct SessionAccumulator {
654
661
  }
655
662
  }
656
663
 
657
- var totals: UsageLedgerTotals { self.aggregate.totals }
658
- var models: [String] { self.modelSet.sorted() }
659
- var versions: [String] { self.versionSet.sorted() }
664
+ var totals: UsageLedgerTotals {
665
+ self.aggregate.totals
666
+ }
667
+
668
+ var models: [String] {
669
+ self.modelSet.sorted()
670
+ }
671
+
672
+ var versions: [String] {
673
+ self.versionSet.sorted()
674
+ }
660
675
  }
@@ -97,7 +97,7 @@ public enum UsageLedgerInsightsAdvisor {
97
97
  score -= 10
98
98
  signals.append("No project-level grouping is available.")
99
99
  } else {
100
- let unknownProjects = projectBreakdown.filter { $0.displayProjectName == "Unknown project" }.count
100
+ let unknownProjects = projectBreakdown.count(where: { $0.displayProjectName == "Unknown project" })
101
101
  let unknownRatio = Double(unknownProjects) / Double(max(1, projectBreakdown.count))
102
102
  if unknownRatio > 0.40 {
103
103
  score -= 10
@@ -60,11 +60,11 @@ public enum UsageLedgerProjectIdentityResolver {
60
60
  projectName: String?,
61
61
  budgetNameOverride: String? = nil) -> UsageLedgerProjectIdentity
62
62
  {
63
- let normalizedProjectID = normalizedIdentifier(projectID)
64
- let normalizedProjectName = normalizedDisplay(projectName)
65
- let budgetName = normalizedDisplay(budgetNameOverride)
66
- let workspaceCandidate = firstPathLikeValue(candidates: [projectID, projectName])
67
- let workspaceHash = workspaceCandidate.map(stablePathHash)
63
+ let normalizedProjectID = self.normalizedIdentifier(projectID)
64
+ let normalizedProjectName = self.normalizedDisplay(projectName)
65
+ let budgetName = self.normalizedDisplay(budgetNameOverride)
66
+ let workspaceCandidate = self.firstPathLikeValue(candidates: [projectID, projectName])
67
+ let workspaceHash = workspaceCandidate.map(self.stablePathHash)
68
68
  let workspaceSuffix = workspaceHash.map { "#w:\($0.prefix(8))" } ?? ""
69
69
 
70
70
  var displayName: String?
@@ -103,7 +103,7 @@ public enum UsageLedgerProjectIdentityResolver {
103
103
  if let normalizedProjectID {
104
104
  key = "id:\(normalizedProjectID.lowercased())\(workspaceSuffix)"
105
105
  } else if let seed = displayName {
106
- let slug = slugify(seed)
106
+ let slug = self.slugify(seed)
107
107
  key = slug.isEmpty ? nil : "name:\(slug)\(workspaceSuffix)"
108
108
  } else {
109
109
  key = nil
@@ -129,11 +129,11 @@ public enum UsageLedgerProjectIdentityResolver {
129
129
  if normalized.contains("/") || normalized.contains("\\") {
130
130
  return nil
131
131
  }
132
- return readableIdentifier(normalized)
132
+ return self.readableIdentifier(normalized)
133
133
  }
134
134
 
135
135
  private static func normalizedIdentifier(_ value: String?) -> String? {
136
- let cleaned = normalizedDisplay(value)
136
+ let cleaned = self.normalizedDisplay(value)
137
137
  guard let cleaned, !cleaned.isEmpty else { return nil }
138
138
  return cleaned
139
139
  }
@@ -149,6 +149,43 @@ public enum UsageLedgerProjectIdentityResolver {
149
149
 
150
150
  private static func inferredPathTail(from value: String?) -> String? {
151
151
  guard let value else { return nil }
152
+
153
+ // Claude Code uses dash-separated path IDs like "-Users-srinivaspendela-Sriinnu-AI-Runic".
154
+ // Extract the last meaningful segment(s) as the project name.
155
+ if value.hasPrefix("-Users-") || value.hasPrefix("-home-") {
156
+ let parts = value.split(separator: "-").map(String.init)
157
+ // Find the last non-user-path segment — skip Users, username, common dirs
158
+ let skipPrefixes: Set = [
159
+ "Users",
160
+ "home",
161
+ "Documents",
162
+ "Desktop",
163
+ "Projects",
164
+ "Personal",
165
+ "repos",
166
+ "code",
167
+ "src",
168
+ "dev",
169
+ "Work",
170
+ "Sites",
171
+ ]
172
+ var meaningful: [String] = []
173
+ var pastUserPath = false
174
+ for part in parts where !part.isEmpty {
175
+ if !pastUserPath {
176
+ if skipPrefixes.contains(part) || part.count <= 2 { continue }
177
+ pastUserPath = true
178
+ }
179
+ meaningful.append(part)
180
+ }
181
+ // Take the last 1-2 segments as the project name
182
+ if meaningful.count >= 2 {
183
+ return meaningful.suffix(2).joined(separator: "/")
184
+ } else if let last = meaningful.last {
185
+ return last
186
+ }
187
+ }
188
+
152
189
  let separators = CharacterSet(charactersIn: "/\\")
153
190
  guard value.rangeOfCharacter(from: separators) != nil else { return nil }
154
191
 
@@ -156,23 +193,23 @@ public enum UsageLedgerProjectIdentityResolver {
156
193
  ch == "/" || ch == "\\"
157
194
  }
158
195
  guard let last = parts.last else { return nil }
159
- let tail = normalizedDisplay(String(last))
196
+ let tail = self.normalizedDisplay(String(last))
160
197
  guard let tail else { return nil }
161
- guard !looksOpaqueIdentifier(tail) else { return nil }
198
+ guard !self.looksOpaqueIdentifier(tail) else { return nil }
162
199
  return tail
163
200
  }
164
201
 
165
202
  private static func readableIdentifier(_ value: String?) -> String? {
166
203
  guard let value else { return nil }
167
204
  guard !value.isEmpty else { return nil }
168
- guard !looksOpaqueIdentifier(value) else { return nil }
205
+ guard !self.looksOpaqueIdentifier(value) else { return nil }
169
206
  return value
170
207
  }
171
208
 
172
209
  private static func inferredNameToken(from value: String?) -> String? {
173
- let normalized = normalizedDisplay(value)
210
+ let normalized = self.normalizedDisplay(value)
174
211
  guard let normalized, !normalized.isEmpty else { return nil }
175
- let slug = slugify(normalized)
212
+ let slug = self.slugify(normalized)
176
213
  guard !slug.isEmpty else { return nil }
177
214
  let titleCased = slug.split(separator: "-")
178
215
  .map { token -> String in
@@ -197,7 +234,9 @@ public enum UsageLedgerProjectIdentityResolver {
197
234
  }
198
235
 
199
236
  private static func slugify(_ value: String) -> String {
200
- let folded = value.folding(options: [.diacriticInsensitive, .caseInsensitive, .widthInsensitive], locale: .init(identifier: "en_US_POSIX"))
237
+ let folded = value.folding(
238
+ options: [.diacriticInsensitive, .caseInsensitive, .widthInsensitive],
239
+ locale: .init(identifier: "en_US_POSIX"))
201
240
  var scalarBuffer: [UnicodeScalar] = []
202
241
  scalarBuffer.reserveCapacity(folded.unicodeScalars.count)
203
242
 
@@ -235,10 +274,10 @@ public enum UsageLedgerProjectIdentityResolver {
235
274
 
236
275
  private static func stablePathHash(_ value: String) -> String {
237
276
  let normalized = value.lowercased()
238
- var hash: UInt64 = 0xcbf29ce484222325
277
+ var hash: UInt64 = 0xCBF2_9CE4_8422_2325
239
278
  for byte in normalized.utf8 {
240
279
  hash ^= UInt64(byte)
241
- hash = hash &* 0x100000001b3
280
+ hash = hash &* 0x100_0000_01B3
242
281
  }
243
282
  return String(format: "%016llx", hash)
244
283
  }
@@ -43,21 +43,21 @@ enum CostUsageCacheIO {
43
43
  }
44
44
  }
45
45
 
46
- struct CostUsageCache: Codable, Sendable {
46
+ struct CostUsageCache: Codable {
47
47
  var version: Int = 1
48
48
  var lastScanUnixMs: Int64 = 0
49
49
 
50
- // filePath -> file usage
50
+ /// filePath -> file usage
51
51
  var files: [String: CostUsageFileUsage] = [:]
52
52
 
53
- // dayKey -> model -> packed usage
53
+ /// dayKey -> model -> packed usage
54
54
  var days: [String: [String: [Int]]] = [:]
55
55
 
56
- // rootPath -> mtime (for Claude roots)
56
+ /// rootPath -> mtime (for Claude roots)
57
57
  var roots: [String: Int64]?
58
58
  }
59
59
 
60
- struct CostUsageFileUsage: Codable, Sendable {
60
+ struct CostUsageFileUsage: Codable {
61
61
  var mtimeUnixMs: Int64
62
62
  var size: Int64
63
63
  var days: [String: [String: [Int]]]
@@ -66,7 +66,7 @@ struct CostUsageFileUsage: Codable, Sendable {
66
66
  var lastTotals: CostUsageCodexTotals?
67
67
  }
68
68
 
69
- struct CostUsageCodexTotals: Codable, Sendable {
69
+ struct CostUsageCodexTotals: Codable {
70
70
  var input: Int
71
71
  var cached: Int
72
72
  var output: Int
@@ -1,7 +1,7 @@
1
1
  import Foundation
2
2
 
3
3
  enum CostUsageJsonl {
4
- struct Line: Sendable {
4
+ struct Line {
5
5
  let bytes: Data
6
6
  let wasTruncated: Bool
7
7
  }