@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.
- package/LICENSE +1 -1
- package/README.md +40 -29
- package/Scripts/package_app.sh +2 -2
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +10 -1
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +30 -2
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +2 -5
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +9 -33
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +9 -33
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/SettingsStore.swift +10 -1
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/UsageStore.swift +50 -9
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +9 -33
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +9 -34
- package/Sources/Runic/Resources/provider-context-windows.json +20 -0
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +9 -16
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +192 -55
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +12 -65
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +71 -1
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +11 -0
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +2 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1 -0
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +1 -0
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +1 -4
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +2 -6
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +1 -3
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +3 -9
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +117 -19
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +49 -3
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +158 -0
- package/appcast.xml +39 -0
- package/package.json +1 -1
- 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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
72
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
72
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 =
|
|
72
|
-
|
|
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
|
|
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 .
|
|
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 = .
|
|
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
|
|
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
|