@sriinnu/runic 1.0.0 → 1.2.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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -29
  3. package/Scripts/package_app.sh +2 -2
  4. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +10 -1
  5. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +30 -2
  6. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +2 -5
  7. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +9 -33
  8. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +9 -33
  9. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +9 -33
  10. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +9 -33
  11. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +9 -33
  12. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +9 -33
  13. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +9 -33
  14. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +9 -33
  15. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +9 -33
  16. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +9 -33
  17. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +9 -33
  18. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +9 -33
  19. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +9 -33
  20. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +9 -33
  21. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +9 -33
  22. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +9 -33
  23. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +9 -33
  24. package/Sources/Runic/Core/Stores/SettingsStore.swift +10 -1
  25. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +9 -33
  26. package/Sources/Runic/Core/Stores/UsageStore.swift +50 -9
  27. package/Sources/Runic/Core/Stores/XAITokenStore.swift +9 -33
  28. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +9 -34
  29. package/Sources/Runic/Resources/provider-context-windows.json +20 -0
  30. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +9 -16
  31. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +192 -55
  32. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +12 -65
  33. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +71 -1
  34. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +11 -0
  35. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
  36. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +2 -0
  37. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
  38. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
  39. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
  40. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1 -0
  41. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +1 -0
  42. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +1 -4
  43. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +2 -6
  44. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +1 -3
  45. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +3 -9
  46. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +117 -19
  47. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +49 -3
  48. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +158 -0
  49. package/appcast.xml +39 -0
  50. package/package.json +1 -1
  51. package/version.env +2 -2
@@ -53,29 +53,17 @@ struct KeychainSambaNovaTokenStore: SambaNovaTokenStoring {
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
56
 
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
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)
70
62
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
73
66
 
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
79
67
 
80
68
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
69
  guard addStatus == errSecSuccess else {
@@ -91,6 +79,7 @@ struct KeychainSambaNovaTokenStore: SambaNovaTokenStoring {
91
79
  var query = self.baseQuery(dataProtection: dataProtection)
92
80
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
81
  query[kSecReturnData as String] = true
82
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
83
 
95
84
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
85
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -127,17 +116,4 @@ struct KeychainSambaNovaTokenStore: SambaNovaTokenStoring {
127
116
  return q
128
117
  }
129
118
 
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
119
  }
@@ -325,6 +325,12 @@ final class SettingsStore {
325
325
  didSet { self.userDefaults.set(self.insightsReportDays, forKey: "insightsReportDays") }
326
326
  }
327
327
 
328
+ /// How many days of usage history to scan for ledger data (charts, breakdowns).
329
+ /// Options: 3, 7, 30, 365. Default: 30.
330
+ var ledgerMaxAgeDays: Int {
331
+ didSet { self.userDefaults.set(self.ledgerMaxAgeDays, forKey: "ledgerMaxAgeDays") }
332
+ }
333
+
328
334
  var randomBlinkEnabled: Bool {
329
335
  didSet { self.userDefaults.set(self.randomBlinkEnabled, forKey: "randomBlinkEnabled") }
330
336
  }
@@ -699,6 +705,7 @@ final class SettingsStore {
699
705
  _ = self.costUsageEnabled
700
706
  _ = self.insightsMenuMaxItems
701
707
  _ = self.insightsReportDays
708
+ _ = self.ledgerMaxAgeDays
702
709
  _ = self.randomBlinkEnabled
703
710
  _ = self.claudeWebExtrasEnabled
704
711
  _ = self.showOptionalCreditsAndExtraUsage
@@ -893,6 +900,7 @@ final class SettingsStore {
893
900
  self.costUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? true
894
901
  self.insightsMenuMaxItems = userDefaults.object(forKey: "insightsMenuMaxItems") as? Int ?? 4
895
902
  self.insightsReportDays = userDefaults.object(forKey: "insightsReportDays") as? Int ?? 7
903
+ self.ledgerMaxAgeDays = userDefaults.object(forKey: "ledgerMaxAgeDays") as? Int ?? 30
896
904
  self.randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false
897
905
  self.claudeWebExtrasEnabled = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false
898
906
  let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool
@@ -945,7 +953,8 @@ final class SettingsStore {
945
953
  self.sambaNovaAPIToken = (try? sambaNovaTokenStore.loadToken()) ?? ""
946
954
  self.qwenAPIToken = (try? qwenTokenStore.loadToken()) ?? ""
947
955
  self.azureOpenAIAPIToken = (try? azureOpenAITokenStore.loadToken()) ?? ""
948
- self.selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider")
956
+ // Always start on Overview tab.
957
+ self.selectedMenuProviderRaw = nil
949
958
  self.providerDetectionCompleted = userDefaults.object(
950
959
  forKey: "providerDetectionCompleted") as? Bool ?? false
951
960
  self.toggleStore = ProviderToggleStore(userDefaults: userDefaults)
@@ -53,29 +53,17 @@ struct KeychainTogetherTokenStore: TogetherTokenStoring {
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
56
 
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
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)
70
62
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
73
66
 
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
79
67
 
80
68
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
69
  guard addStatus == errSecSuccess else {
@@ -91,6 +79,7 @@ struct KeychainTogetherTokenStore: TogetherTokenStoring {
91
79
  var query = self.baseQuery(dataProtection: dataProtection)
92
80
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
81
  query[kSecReturnData as String] = true
82
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
83
 
95
84
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
85
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -127,17 +116,4 @@ struct KeychainTogetherTokenStore: TogetherTokenStoring {
127
116
  return q
128
117
  }
129
118
 
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
119
  }
@@ -635,7 +635,7 @@ final class UsageStore {
635
635
  @ObservationIgnored private let tokenFetchTTL: TimeInterval = 60 * 60
636
636
  @ObservationIgnored private let tokenFetchTimeout: TimeInterval = 10 * 60
637
637
  @ObservationIgnored private let ledgerRefreshTTL: TimeInterval = 90
638
- @ObservationIgnored private let ledgerMaxAgeDays: Int = 3
638
+ @ObservationIgnored private var ledgerMaxAgeDays: Int { self.settings.ledgerMaxAgeDays }
639
639
  @ObservationIgnored private let providerHistoryCacheTTL: TimeInterval = 90
640
640
  @ObservationIgnored private let providerHistoryMaxScanDays: Int = 180
641
641
  @ObservationIgnored nonisolated(unsafe) private let performanceStorage: PerformanceStorageImpl?
@@ -1289,6 +1289,15 @@ final class UsageStore {
1289
1289
  }
1290
1290
  }
1291
1291
 
1292
+ /// Refresh a single provider (used when user clicks Ping on a specific provider tab).
1293
+ func refreshSingleProvider(_ provider: UsageProvider) async {
1294
+ self.isRefreshing = true
1295
+ defer { self.isRefreshing = false }
1296
+ await self.refreshProvider(provider, trigger: .manual)
1297
+ await self.refreshStatus(provider, trigger: .manual)
1298
+ self.scheduleLedgerRefresh(force: true, inactiveProviders: [])
1299
+ }
1300
+
1292
1301
  func refresh(trigger: RefreshTrigger = .manual, forceTokenUsage: Bool = false) async {
1293
1302
  guard !self.isRefreshing else { return }
1294
1303
  let now = Date()
@@ -1332,7 +1341,7 @@ final class UsageStore {
1332
1341
  await self.refreshCreditsIfNeeded()
1333
1342
  }
1334
1343
 
1335
- self.scheduleLedgerRefresh(force: forceTokenUsage, inactiveProviders: inactiveProviders)
1344
+ self.scheduleLedgerRefresh(force: true, inactiveProviders: inactiveProviders)
1336
1345
 
1337
1346
  // Refresh custom providers
1338
1347
  await self.refreshCustomProviders()
@@ -1660,6 +1669,34 @@ final class UsageStore {
1660
1669
  }
1661
1670
  if self.ledgerRefreshTask != nil { return }
1662
1671
 
1672
+ // Immediately populate from cache so the UI has data before the background scan.
1673
+ let providersToCache = providers.filter { self.ledgerAllDailySummaries[$0] == nil || self.ledgerAllDailySummaries[$0]?.isEmpty == true }
1674
+ if !providersToCache.isEmpty {
1675
+ Task {
1676
+ let cache = LedgerCache.shared
1677
+ for provider in providersToCache {
1678
+ let providerKey = provider.rawValue
1679
+ guard let cached = await cache.loadCachedDailies(provider: providerKey) else { continue }
1680
+ let summaries = cached.dailies.compactMap { $0.toLedgerDailySummary(provider: provider) }
1681
+ if !summaries.isEmpty {
1682
+ await MainActor.run { [weak self] in
1683
+ guard let self else { return }
1684
+ if self.ledgerAllDailySummaries[provider] == nil || self.ledgerAllDailySummaries[provider]?.isEmpty == true {
1685
+ self.ledgerAllDailySummaries[provider] = summaries
1686
+ // Also populate today's summary from cache.
1687
+ let todayStart = Calendar.current.startOfDay(for: now)
1688
+ if self.ledgerDailySummaries[provider] == nil,
1689
+ let todaySummary = summaries.first(where: { $0.dayStart == todayStart })
1690
+ {
1691
+ self.ledgerDailySummaries[provider] = todaySummary
1692
+ }
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+
1663
1700
  self.ledgerRefreshTask = Task.detached(priority: .utility) { [weak self] in
1664
1701
  guard let self else { return }
1665
1702
  let result = await self.loadLedgerInsights(sources: sources, now: now)
@@ -1783,13 +1820,17 @@ final class UsageStore {
1783
1820
  self?.tokenRefreshSequenceTask = nil
1784
1821
  }
1785
1822
  }
1786
- for provider in UsageProvider.allCases {
1787
- if Task.isCancelled { break }
1788
- await self.refreshTokenUsage(
1789
- provider,
1790
- force: force,
1791
- trigger: trigger,
1792
- inactiveProviders: inactiveProviders)
1823
+ await withTaskGroup(of: Void.self) { group in
1824
+ for provider in UsageProvider.allCases {
1825
+ group.addTask {
1826
+ guard !Task.isCancelled else { return }
1827
+ await self.refreshTokenUsage(
1828
+ provider,
1829
+ force: force,
1830
+ trigger: trigger,
1831
+ inactiveProviders: inactiveProviders)
1832
+ }
1833
+ }
1793
1834
  }
1794
1835
  }
1795
1836
  }
@@ -53,29 +53,17 @@ struct KeychainXAITokenStore: XAITokenStoring {
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
56
 
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
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)
70
62
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
73
66
 
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
79
67
 
80
68
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
69
  guard addStatus == errSecSuccess else {
@@ -91,6 +79,7 @@ struct KeychainXAITokenStore: XAITokenStoring {
91
79
  var query = self.baseQuery(dataProtection: dataProtection)
92
80
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
81
  query[kSecReturnData as String] = true
82
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
83
 
95
84
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
85
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -127,17 +116,4 @@ struct KeychainXAITokenStore: XAITokenStoring {
127
116
  return q
128
117
  }
129
118
 
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
119
  }
@@ -53,29 +53,16 @@ struct KeychainZaiTokenStore: ZaiTokenStoring {
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
56
 
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
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)
70
62
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
73
-
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
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 KeychainZaiTokenStore: ZaiTokenStoring {
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 {
@@ -127,17 +115,4 @@ struct KeychainZaiTokenStore: ZaiTokenStoring {
127
115
  return q
128
116
  }
129
117
 
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
118
  }
@@ -0,0 +1,20 @@
1
+ {
2
+ "claude": { "model": "Claude Opus 4.6", "contextK": 1000 },
3
+ "codex": { "model": "GPT-5 Codex", "contextK": 400 },
4
+ "cursor": { "model": "GPT-4o", "contextK": 128 },
5
+ "gemini": { "model": "Gemini 2.5 Pro", "contextK": 1000 },
6
+ "copilot": { "model": "GPT-4o", "contextK": 128 },
7
+ "zai": { "model": "GLM-4.6", "contextK": 205 },
8
+ "groq": { "model": "Llama 3.3 70B", "contextK": 128 },
9
+ "deepseek": { "model": "DeepSeek Chat", "contextK": 64 },
10
+ "fireworks": { "model": "Llama 3.1 70B", "contextK": 128 },
11
+ "mistral": { "model": "Mistral Large", "contextK": 128 },
12
+ "perplexity": { "model": "Sonar", "contextK": 128 },
13
+ "kimi": { "model": "Moonshot v1", "contextK": 128 },
14
+ "together": { "model": "Llama 3.1", "contextK": 128 },
15
+ "cohere": { "model": "Command R+", "contextK": 128 },
16
+ "xai": { "model": "Grok 2", "contextK": 128 },
17
+ "cerebras": { "model": "Llama 3.3 70B", "contextK": 128 },
18
+ "sambanova": { "model": "Llama 3.1 70B", "contextK": 128 },
19
+ "qwen": { "model": "Qwen 2.5 72B", "contextK": 128 }
20
+ }
@@ -7,28 +7,23 @@ import SwiftUI
7
7
  @MainActor
8
8
  struct InlineUsageChartView: View {
9
9
  enum TimeRange: String, CaseIterable {
10
- case oneHour = "1h"
11
- case sixHours = "6h"
12
- case oneDay = "1d"
10
+ case threeDays = "3d"
13
11
  case sevenDays = "7d"
14
12
  case thirtyDays = "30d"
13
+ case quarter = "90d"
14
+ case year = "1y"
15
15
 
16
16
  var cutoffInterval: TimeInterval {
17
17
  switch self {
18
- case .oneHour: return -3600
19
- case .sixHours: return -21600
20
- case .oneDay: return -86400
18
+ case .threeDays: return -259200
21
19
  case .sevenDays: return -604800
22
20
  case .thirtyDays: return -2592000
21
+ case .quarter: return -7776000
22
+ case .year: return -31536000
23
23
  }
24
24
  }
25
25
 
26
- var usesHourlyData: Bool {
27
- switch self {
28
- case .oneHour, .sixHours, .oneDay: return true
29
- default: return false
30
- }
31
- }
26
+ var usesHourlyData: Bool { false }
32
27
  }
33
28
 
34
29
  private struct ChartPoint: Identifiable {
@@ -40,15 +35,13 @@ struct InlineUsageChartView: View {
40
35
  let dailySummaries: [UsageLedgerDailySummary]
41
36
  let hourlySummaries: [UsageLedgerHourlySummary]
42
37
  let width: CGFloat
43
- @State private var selectedRange: TimeRange = .oneDay
38
+ @State private var selectedRange: TimeRange = .sevenDays
44
39
  @Environment(\.menuItemHighlighted) private var isHighlighted
45
40
 
46
41
  private static let lineColor = RunicColors.chartColor(at: 0)
47
42
 
48
43
  var body: some View {
49
- let points = self.selectedRange.usesHourlyData
50
- ? Self.hourlyPoints(from: self.hourlySummaries, range: self.selectedRange)
51
- : Self.dailyPoints(from: self.dailySummaries, range: self.selectedRange)
44
+ let points = Self.dailyPoints(from: self.dailySummaries, range: self.selectedRange)
52
45
 
53
46
  VStack(alignment: .leading, spacing: RunicSpacing.xs) {
54
47
  // Time range picker