@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 KeychainFireworksTokenStore: FireworksTokenStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
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 KeychainFireworksTokenStore: FireworksTokenStoring {
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 KeychainFireworksTokenStore: FireworksTokenStoring {
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,17 @@ struct KeychainGroqTokenStore: GroqTokenStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
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 KeychainGroqTokenStore: GroqTokenStoring {
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 KeychainGroqTokenStore: GroqTokenStoring {
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,17 @@ struct KeychainKimiTokenStore: KimiTokenStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
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 KeychainKimiTokenStore: KimiTokenStoring {
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 KeychainKimiTokenStore: KimiTokenStoring {
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,17 @@ struct KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
56
 
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteHeader(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.deleteHeader(dataProtection: false)
61
+ try? self.deleteHeader(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 KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
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 KeychainMiniMaxCookieHeaderStore: MiniMaxCookieHeaderStoring {
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,17 @@ struct KeychainMiniMaxGroupIDStore: MiniMaxGroupIDStoring {
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.deleteGroupID(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.deleteGroupID(dataProtection: false)
61
+ try? self.deleteGroupID(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 KeychainMiniMaxGroupIDStore: MiniMaxGroupIDStoring {
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 KeychainMiniMaxGroupIDStore: MiniMaxGroupIDStoring {
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,17 @@ struct KeychainMiniMaxTokenStore: MiniMaxTokenStoring {
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 KeychainMiniMaxTokenStore: MiniMaxTokenStoring {
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 KeychainMiniMaxTokenStore: MiniMaxTokenStoring {
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,17 @@ struct KeychainMistralTokenStore: MistralTokenStoring {
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 KeychainMistralTokenStore: MistralTokenStoring {
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 KeychainMistralTokenStore: MistralTokenStoring {
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,17 @@ struct KeychainOpenRouterTokenStore: OpenRouterTokenStoring {
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 KeychainOpenRouterTokenStore: OpenRouterTokenStoring {
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 KeychainOpenRouterTokenStore: OpenRouterTokenStoring {
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,17 @@ struct KeychainPerplexityTokenStore: PerplexityTokenStoring {
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 KeychainPerplexityTokenStore: PerplexityTokenStoring {
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 KeychainPerplexityTokenStore: PerplexityTokenStoring {
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,17 @@ struct KeychainQwenTokenStore: QwenTokenStoring {
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 KeychainQwenTokenStore: QwenTokenStoring {
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 KeychainQwenTokenStore: QwenTokenStoring {
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
  }