|
1 | | -# AGENTS.md |
| 1 | +ClaudeMeter is a macOS 14+ SwiftUI menu bar app; keep UI state on `@MainActor @Observable` types and non-UI work in actor services/repositories. |
2 | 2 |
|
3 | | -## Project Overview |
| 3 | +Build with `xcodebuild clean build -project ClaudeMeter.xcodeproj -scheme ClaudeMeter -configuration Debug`; test with `xcodebuild test -project ClaudeMeter.xcodeproj -scheme ClaudeMeter -configuration Debug`. |
4 | 4 |
|
5 | | -macOS 14+ menu bar app monitoring Claude.ai usage limits. Swift 6 / SwiftUI with @MainActor @Observable state management and actor-isolated services. |
6 | | - |
7 | | -## Setup |
8 | | - |
9 | | -```bash |
10 | | -# Clone and open |
11 | | -git clone git@github.com:eddmann/ClaudeMeter.git |
12 | | -cd ClaudeMeter |
13 | | -open ClaudeMeter.xcodeproj |
14 | | - |
15 | | -# Build from CLI (requires Xcode 16+) |
16 | | -xcodebuild clean build \ |
17 | | - -project ClaudeMeter.xcodeproj \ |
18 | | - -scheme ClaudeMeter \ |
19 | | - -configuration Debug |
20 | | -``` |
21 | | - |
22 | | -Press ⌘R in Xcode to run. App appears in menu bar (not Dock). |
23 | | - |
24 | | -## Common Commands |
25 | | - |
26 | | -| Task | Command | |
27 | | -|------|---------| |
28 | | -| Build (Debug) | `xcodebuild clean build -project ClaudeMeter.xcodeproj -scheme ClaudeMeter -configuration Debug` | |
29 | | -| Build (Release) | `xcodebuild clean build -project ClaudeMeter.xcodeproj -scheme ClaudeMeter -configuration Release -derivedDataPath ./build -arch x86_64 -arch arm64` | |
30 | | -| Run Tests | `xcodebuild test -project ClaudeMeter.xcodeproj -scheme ClaudeMeter -configuration Debug` | |
31 | | -| Open in Xcode | `open ClaudeMeter.xcodeproj` | |
32 | | - |
33 | | -No linting or formatting tools configured. |
34 | | - |
35 | | -## Code Conventions |
36 | | - |
37 | | -### File Organization |
38 | | -``` |
39 | | -ClaudeMeter/ |
40 | | -├── App/ # AppModel.swift, ClaudeMeterApp.swift (entry point) |
41 | | -├── Models/ # Data types, API/, Errors/ |
42 | | -├── Services/ # Actor-isolated business logic, Protocols/ |
43 | | -├── Repositories/ # Data persistence (Keychain, UserDefaults, Cache), Protocols/ |
44 | | -└── Views/ # SwiftUI components: MenuBar/, Settings/, Setup/ |
45 | | -``` |
46 | | - |
47 | | -### Naming |
48 | | -- One type per file, filename matches type: `UsageData.swift`, `NetworkService.swift` |
49 | | -- Protocols in `Protocols/` subdirectory: `UsageServiceProtocol.swift` |
50 | | -- Error types suffixed: `AppError.swift`, `NetworkError.swift` |
51 | | - |
52 | | -### Concurrency Model |
53 | | -- `@MainActor @Observable` for AppModel and all views |
54 | | -- `actor` for services and repositories (thread-safe) |
55 | | -- `async/await` end-to-end |
56 | | -- Mark non-published dependencies `@ObservationIgnored` |
57 | | - |
58 | | -### Pattern Examples |
59 | | - |
60 | | -**Observable state owner:** |
61 | | -```swift |
62 | | -@MainActor @Observable final class AppModel { |
63 | | - var settings: AppSettings = .default |
64 | | - @ObservationIgnored private let service: ServiceProtocol |
65 | | -} |
66 | | -``` |
67 | | - |
68 | | -**Actor-isolated service:** |
69 | | -```swift |
70 | | -actor UsageService: UsageServiceProtocol { |
71 | | - private static let logger = Logger(subsystem: "com.claudemeter", category: "UsageService") |
72 | | - func fetchUsage() async throws -> UsageData { } |
73 | | -} |
74 | | -``` |
75 | | - |
76 | | -**Dependency injection (constructor with defaults):** |
77 | | -```swift |
78 | | -init( |
79 | | - settingsRepository: SettingsRepositoryProtocol = SettingsRepository(), |
80 | | - keychainRepository: KeychainRepositoryProtocol = KeychainRepository() |
81 | | -) { } |
82 | | -``` |
83 | | - |
84 | | -### Imports |
85 | | -System frameworks first, no third-party deps in main target: |
86 | | -```swift |
87 | | -import SwiftUI |
88 | | -import Observation |
89 | | -import os |
90 | | -``` |
91 | | - |
92 | | -## Tests & CI |
93 | | - |
94 | | -### Running Tests |
95 | | -```bash |
96 | | -# All tests |
97 | | -xcodebuild test -project ClaudeMeter.xcodeproj -scheme ClaudeMeter |
98 | | - |
99 | | -# Specific test class |
100 | | -xcodebuild test -project ClaudeMeter.xcodeproj -scheme ClaudeMeter \ |
101 | | - -only-testing ClaudeMeterTests/AppModelTests |
102 | | -``` |
103 | | - |
104 | | -### Test Structure |
105 | | -- `ClaudeMeterTests/` - XCTest with `@MainActor` async tests |
106 | | -- `TestDoubles/` - Stubs, fakes, spies for protocol-based DI |
107 | | -- `__Snapshots__/` - Snapshot test reference images (SnapshotTesting library) |
108 | | - |
109 | | -### CI |
110 | | -- No automated PR checks (build/tests not run on PRs) |
111 | | -- Release workflow: manual trigger via GitHub Actions |
112 | | -- Builds universal binary, signs with Developer ID, notarizes with Apple |
113 | | - |
114 | | -## PR & Workflow Rules |
115 | | - |
116 | | -### Commit Format |
117 | | -``` |
118 | | -type(scope): description |
119 | | -``` |
120 | | - |
121 | | -Examples: |
122 | | -``` |
123 | | -feat(menu-bar): add battery icon style |
124 | | -fix(settings): remove icon preview flicker |
125 | | -refactor(app): modernize app lifecycle |
126 | | -docs: add session key instructions |
127 | | -``` |
128 | | - |
129 | | -Types: `feat`, `fix`, `refactor`, `docs`, `test` |
130 | | - |
131 | | -### Branch |
132 | | -- `main` is the release branch |
133 | | -- No PR templates or CODEOWNERS |
134 | | - |
135 | | -### Release Process |
136 | | -1. Trigger GitHub Actions workflow with version (e.g., `1.0.0`) |
137 | | -2. Builds, signs, notarizes automatically |
138 | | -3. Creates GitHub release with signed ZIP |
139 | | -4. Updates Homebrew tap |
140 | | - |
141 | | -## Security & Gotchas |
142 | | - |
143 | | -### Session Keys |
144 | | -- Format: `sk-ant-*` (validated by `SessionKey` initializer) |
145 | | -- Stored in Keychain only (service: `com.claudemeter.sessionkey`) |
146 | | -- Never serialize to disk or logs |
147 | | -- May contain embedded org UUID |
148 | | - |
149 | | -### Files to Never Commit |
150 | | -- `*.p12`, `*.mobileprovision` (certificates) |
151 | | -- API keys, session keys |
152 | | -- `xcuserdata/`, `DerivedData/` |
153 | | -- `build/` output directory |
154 | | - |
155 | | -### Non-Obvious Patterns |
156 | | -- Cache TTL is 55 seconds (< 60s minimum refresh interval) |
157 | | -- Staleness threshold: 1200 seconds (shows "stale" indicator) |
158 | | -- Retry: 2^n for network errors, 3^n for rate limits, no retry for auth failures |
159 | | -- `@ObservationIgnored` required for non-UI dependencies in AppModel |
160 | | -- Cancel existing `Task` before creating new ones (prevent duplicates) |
161 | | - |
162 | | -### Constants |
163 | | -Key values in `ClaudeMeter/Models/Constants.swift`: |
164 | | -- `Constants.Cache.ttl` = 55 seconds |
165 | | -- `Constants.Network.maxRetries` = 3 |
166 | | -- `Constants.Refresh.minimum` = 60 seconds |
167 | | -- `Constants.Refresh.maximum` = 600 seconds |
168 | | - |
169 | | -### Adding a New Setting |
170 | | -1. Add property to `AppSettings` struct |
171 | | -2. Settings auto-persist via `SettingsRepository` on change |
172 | | -3. Add UI control in `SettingsView` bound to `appModel.settings` |
173 | | -4. If behavior depends on setting, react in `AppModel.scheduleSettingsSave()` |
174 | | - |
175 | | -### Adding a New API Endpoint |
176 | | -1. Define response model in `Models/API/` |
177 | | -2. Add method to protocol in `Services/Protocols/` |
178 | | -3. Implement in service with retry logic |
179 | | -4. Call from `AppModel` or helper |
| 5 | +New `AppSettings` keys must persist through `SettingsRepository`, appear in `SettingsView` when user-facing, and decode old saved settings safely. |
0 commit comments