# Tokens for Breakfast — Agent Execution Prompts
## For Claude Code / Codex Agents in Xcode
### Version 1.0 | Sequential Build Order

---

## HOW TO USE THESE PROMPTS

These prompts are designed to be executed sequentially inside Xcode using Claude Code or the Codex agent. Each prompt is a self-contained task. Complete each task, verify it builds and runs, then move to the next.

**Before starting any prompt**:
1. Create the Xcode project using: File > New > Project > macOS > App
2. Set Bundle ID: `com.onekapisch.tokensforbreakfast`
3. Minimum deployment: macOS 13.0
4. Language: Swift, Interface: SwiftUI
5. Add the GRDB.swift package: `https://github.com/groue/GRDB.swift` via Swift Package Manager

**Agent behavior notes**:
- Always ask the agent to build and run after each prompt before moving on
- If the agent produces a file in the wrong location, correct it before proceeding
- These prompts assume the APP_SPEC.md is available in the project root for the agent to reference

---

## PHASE 1 — PROJECT FOUNDATION

### Prompt 1.1 — Convert to Menu Bar Agent App

```
Convert this Xcode macOS project to a menu bar agent app (no Dock icon, no main window).

1. In Info.plist, set `Application is agent (UIElement)` to YES (LSUIElement = true)
2. Delete ContentView.swift and the default SwiftUI window scene
3. Create AppDelegate.swift that conforms to NSApplicationDelegate
4. In TokensForBreakfastApp.swift, use NSApplicationDelegateAdaptor to wire AppDelegate
5. In AppDelegate, add a placeholder NSStatusItem with title "TFB" to NSStatusBar.system
6. The app should launch, show "TFB" in the menu bar, and have no Dock icon

Build and confirm this works before stopping.
```

---

### Prompt 1.2 — Design Tokens and Style System

```
Create a Swift file at Core/Utils/DesignTokens.swift that defines the full design system for this app.

Use these exact values:

Colors (as NSColor and Color extensions):
- backgroundPrimary:   #1C1917
- backgroundSecondary: #292524
- backgroundTertiary:  #3C3836
- accentAmber:         #F59E0B
- accentSuccess:       #34D399
- accentWarning:       #FBBF24
- accentDanger:        #F87171
- textPrimary:         #FAFAF9
- textSecondary:       #A8A29E
- textTertiary:        #78716C
- borderSubtle:        #44403C

Create it as:
- An extension on Color with static properties
- An extension on NSColor with static properties  
- A struct TFBSpacing with static let values: xs=4, sm=8, md=12, lg=16, xl=24, xxl=32
- A struct TFBRadius with static let values: small=6, medium=10, large=14
- A struct TFBFonts with static methods that return Font using custom font names:
  - display(size:) — uses "DMSerifDisplay-Regular"
  - body(size:) — uses "DMSans-Regular"
  - medium(size:) — uses "DMSans-Medium"
  - mono(size:) — uses "JetBrainsMono-Regular"
  - Falls back to system fonts if custom fonts not yet loaded

Add the font files to Resources/Fonts/ (placeholder — actual .ttf files to be added manually).
Register fonts in AppDelegate applicationDidFinishLaunching using CTFontManagerRegisterFontsForURL.
```

---

### Prompt 1.3 — SQLite Storage Layer

```
Set up the local SQLite database using GRDB.swift.

Create these files:

1. Core/Storage/UsageEvent.swift
   - Struct conforming to FetchableRecord, PersistableRecord, Codable
   - Fields: id (String UUID), provider (String), model (String?), tokensInput (Int?), tokensOutput (Int?), costUSD (Double?), sessionId (String?), capturedAt (Date)
   - Static var databaseTableName = "usage_events"

2. Core/Storage/DailyTotal.swift
   - Struct conforming to FetchableRecord, PersistableRecord, Codable
   - Fields: date (String), provider (String), tokensTotal (Int), costUSD (Double)
   - Composite primary key on (date, provider)

3. Core/Storage/Budget.swift
   - Struct conforming to FetchableRecord, PersistableRecord, Codable
   - Fields: provider (String), period (String — "daily" or "weekly"), limitUSD (Double)

4. Core/Storage/StorageManager.swift
   - Singleton: static let shared = StorageManager()
   - Creates database at ~/.tokensforbreakfast/data.db (create directory if needed)
   - Runs migrations using DatabaseMigrator to create all three tables
   - Methods:
     - func insertEvent(_ event: UsageEvent) throws
     - func dailyTotals(for days: Int) throws -> [DailyTotal]
     - func todayTotal() throws -> Double
     - func weekTotal() throws -> Double
     - func setBudget(_ budget: Budget) throws
     - func budgets() throws -> [Budget]
     - func recentEvents(limit: Int) throws -> [UsageEvent]

All database operations run on a background queue. Wrap public methods to be async/await safe.

Build and verify SQLite tables are created successfully on first run.
```

---

### Prompt 1.4 — Keychain Helper

```
Create Core/Utils/KeychainHelper.swift

A struct with static methods for secure credential storage:

static func save(key: String, value: String) -> Bool
static func load(key: String) -> String?
static func delete(key: String) -> Bool

Use kSecClassGenericPassword with service name "com.onekapisch.tokensforbreakfast".
The key parameter becomes kSecAttrAccount.
Store and retrieve as UTF-8 Data.

Also create a convenience enum KeychainKeys with string constants:
- openAIAPIKey = "openai_api_key"
- geminiAPIKey = "gemini_api_key"
- openRouterAPIKey = "openrouter_api_key"
- claudeCodeAccessToken = "claude_code_access_token"
- licenseKey = "license_key"

No UI. Pure utility. Build and confirm it compiles.
```

---

### Prompt 1.5 — Model Pricing Table

```
Create Core/Pricing/ModelPricingTable.swift

A struct with:
- A static func costUSD(provider: String, model: String, inputTokens: Int, outputTokens: Int) -> Double
- Hardcoded pricing data for these models (cost per million tokens):

Claude:
- claude-opus-4-6: input $15.00, output $75.00
- claude-sonnet-4-6: input $3.00, output $15.00  
- claude-haiku-4-5: input $0.80, output $4.00
- claude-3-5-sonnet: input $3.00, output $15.00
- claude-3-haiku: input $0.25, output $1.25

OpenAI:
- gpt-4o: input $2.50, output $10.00
- gpt-4o-mini: input $0.15, output $0.60
- o1: input $15.00, output $60.00
- o3-mini: input $1.10, output $4.40

Gemini:
- gemini-2.0-flash: input $0.075, output $0.30
- gemini-1.5-pro: input $1.25, output $5.00

- If model is unknown, use $2.00/$8.00 as fallback (reasonable average)
- Formula: ((inputTokens / 1_000_000.0) * inputRate) + ((outputTokens / 1_000_000.0) * outputRate)

Also create Core/Utils/CostFormatter.swift:
- static func format(_ usd: Double) -> String
  - Under $0.01: "<$0.01"
  - Under $10: "$X.XX" (2 decimal places)
  - $10 and above: "$XX.X" (1 decimal place)
  - $100 and above: "$XXX" (no decimal)
```

---

## PHASE 2 — DATA COLLECTORS

### Prompt 2.1 — Data Collection Manager + Protocol

```
Create Core/DataCollection/DataCollectionManager.swift

1. Define a protocol ProviderCollector:
   - var providerName: String { get }
   - var isEnabled: Bool { get set }
   - func startCollecting()
   - func stopCollecting()

2. Create DataCollectionManager as an ObservableObject singleton:
   - static let shared = DataCollectionManager()
   - @Published var collectors: [any ProviderCollector] = []
   - @Published var currentHourCostUSD: Double = 0
   - func registerCollector(_ collector: any ProviderCollector)
   - func startAll() — calls startCollecting() on all enabled collectors
   - func stopAll()
   - func refreshTodayTotal() — reads from StorageManager, updates a @Published var todayTotalUSD: Double

Collectors post new UsageEvents to a shared NotificationCenter notification named "TFBNewUsageEvent".
DataCollectionManager observes this notification and calls StorageManager.insertEvent().
After each insert, it calls refreshTodayTotal().

Build and confirm it compiles.
```

---

### Prompt 2.2 — Claude Code Collector

```
Create Core/DataCollection/ClaudeCodeCollector.swift

This collector tracks Claude Code usage and the 5-hour rate limit window.

1. Conforms to ProviderCollector
2. providerName = "claude"

Data collection approach:
- Check if Claude Code CLI credentials exist in Keychain under the key "claude_code_access_token"
- If credentials exist, poll the Anthropic usage API endpoint every 60 seconds
- Use URLSession async/await for network calls
- API endpoint: https://api.anthropic.com/v1/usage (or the correct endpoint — check Anthropic docs)
- Parse response JSON to extract tokens_input, tokens_output, model name

Rate limit window tracking:
- @Published var windowUsagePercent: Double = 0  (0.0 to 1.0)
- @Published var windowResetDate: Date? = nil
- @Published var isRateLimited: Bool = false
- Calculate the rolling 5-hour window by fetching recent events from StorageManager
- windowUsagePercent = (total tokens in last 5 hours) / (known limit for user's plan)
- For plan limit, default to Claude Pro 5-hour token limit; allow override in settings

Post UsageEvent notifications when new usage is detected (compare last known total to new total).

Use a Timer-based polling loop wrapped in a Task. Handle errors gracefully — log to console, do not crash.

Note: If the exact Anthropic usage API endpoint is uncertain, add a TODO comment and implement a stub that returns mock data for now, so the rest of the UI can be built and tested.
```

---

### Prompt 2.3 — OpenAI Collector

```
Create Core/DataCollection/OpenAICollector.swift

1. Conforms to ProviderCollector
2. providerName = "openai"

Behavior:
- Load OpenAI API key from Keychain using KeychainKeys.openAIAPIKey
- If no key, isEnabled = false
- Poll https://api.openai.com/v1/usage every 5 minutes (300 seconds)
- The usage endpoint returns daily token usage by model
- Parse the response to extract: model name, n_context_tokens_total, n_generated_tokens_total
- Calculate cost using ModelPricingTable.costUSD()
- Create a UsageEvent for each model entry and post via NotificationCenter

Handle 401 (invalid key) by setting a @Published var credentialError: Bool = true.
Handle rate limiting on the usage API (429) with exponential backoff.

Use the authorization header: "Bearer \(apiKey)"
```

---

### Prompt 2.4 — Cursor Collector (File Watcher)

```
Create Core/DataCollection/CursorCollector.swift

1. Conforms to ProviderCollector
2. providerName = "cursor"

Behavior:
- Watch the directory ~/Library/Application Support/Cursor/ for file changes using DispatchSource.makeFileSystemObjectSource
- When changes are detected, scan for log files containing AI request records
- Parse any JSON log entries that contain token usage information
- Create UsageEvents for new entries (track last-parsed position to avoid duplicates)

Since Cursor's exact log format may vary by version, implement this with a flexible parser:
- Look for JSON objects containing fields like "inputTokens", "outputTokens", "model"
- If the exact format is unknown, add a TODO comment and implement a stub that fires a test event every 30 seconds with mock data
- @Published var lastActivityTime: Date? — updated whenever cursor activity is detected

The file watcher should restart after 30 seconds if it encounters an error, to handle Cursor updates that change log locations.
```

---

## PHASE 3 — MENU BAR UI

### Prompt 3.1 — Menu Bar Status Item and Popover Shell

```
Implement the menu bar status item and popover shell in StatusBarController.swift.

1. Create MenuBar/StatusBarController.swift
   - Manages NSStatusItem with variable width
   - Creates and positions an NSPopover
   - Left-click on status item: toggles popover open/closed
   - Right-click on status item: shows a simple NSMenu with items: "Open History", "Settings", "Quit"

2. The status item button should show:
   - A coffee cup SF Symbol ("cup.and.saucer.fill") as the template image, 16pt
   - A text label to the right showing today's cost: "$X.XX"
   - Use NSStatusBarButton with an attributed string combining image + text
   
3. The popover:
   - contentSize: NSSize(width: 340, height: 480)
   - animates: true
   - behavior: .transient (closes when user clicks elsewhere)
   - contentViewController hosts a NSHostingController wrapping PopoverView (SwiftUI)

4. Subscribe to DataCollectionManager.$todayTotalUSD using Combine
   - Update the status item label whenever todayTotalUSD changes
   - Use main thread dispatch for UI updates

5. Color logic for the label:
   - Under $5: .accentAmber
   - $5-$10: .accentWarning  
   - Over $10: .accentDanger
   - Or if any budget is set: use percentage of daily budget for color logic

Build and verify the menu bar item appears with a cost label.
```

---

### Prompt 3.2 — Popover View (Main SwiftUI UI)

```
Create the primary popover SwiftUI view at UI/Popover/PopoverView.swift

The view should use the design tokens from DesignTokens.swift throughout.
Background: Color.backgroundPrimary
All text: use TFBFonts

Structure (VStack, no ScrollView needed, fixed 340pt width):

SECTION 1 — Header (height ~56pt)
- HStack: coffee cup icon (SF Symbol, amber color, 20pt) + "Tokens for Breakfast" in TFBFonts.medium(size: 13) + Spacer + gear button (settings) + arrow-up-right button (open history)
- Subtext: today's date formatted as "Saturday, March 28" in TFBFonts.body(size: 11), textTertiary color
- Bottom divider: 1pt line, borderSubtle color

SECTION 2 — Today Overview (height ~88pt)
- Large cost number: CostFormatter.format(todayTotal) in TFBFonts.display(size: 32), textPrimary
- Subtext: "spent today across all providers" in body 11pt, textSecondary
- Provider split bar: a horizontal bar showing each provider's share as colored segments
  - Claude: accentAmber, OpenAI: Color(hex: "#10A37F"), Cursor: Color(hex: "#7C3AED")
  - Rounded full, height 6pt
- If a daily budget is set: show "X% of $XX daily budget" below the bar

SECTION 3 — Provider Cards (use a ForEach over active providers)
Create UI/Popover/ProviderCardView.swift as a separate component:
- RoundedRectangle background: backgroundTertiary, radius 10pt
- HStack: provider logo (SF Symbol or initials badge) + provider name (medium 13pt) + Spacer + cost today (mono 13pt, amber)
- For Claude Code specifically, show a second row with:
  - "Rate limit window:" label + progress bar (accentSuccess/accentWarning/accentDanger based on %)
  - Reset countdown: "resets in 2h 14m" in mono 11pt, textSecondary

SECTION 4 — 7-Day Sparkline
Create UI/Popover/SparklineView.swift:
- Use Swift Charts BarChart
- 7 bars showing daily cost for last 7 days
- Bar color interpolates from accentSuccess (low) to accentDanger (high) based on relative value
- X axis: day abbreviations (M T W T F S S), 9pt body font
- Height: 70pt
- No Y axis labels (sparkline style)

SECTION 5 — Footer (height ~44pt)
- HStack: "View History" button (text style, amber) + Spacer + "Settings" button (text style, textSecondary)
- If user is on Basic tier: "Upgrade to Pro" button with subtle amber border

All sections separated by 1pt dividers in borderSubtle color.
Observe DataCollectionManager as @ObservedObject for reactive updates.
```

---

### Prompt 3.3 — Menu Bar Icon Animation

```
Add subtle animation to the menu bar coffee cup icon in MenuBar/MenuBarIconView.swift

Create a custom NSView subclass MenuBarIconView that:
1. Draws the coffee cup using an NSImage (SF Symbol "cup.and.saucer.fill")
2. When DataCollectionManager.currentHourCostUSD > 1.0, start a gentle pulsing animation:
   - The icon alpha pulses from 1.0 to 0.6 and back over a 2-second cycle
   - Use Core Animation CABasicAnimation on the view's layer opacity
3. When isRateLimited is true on ClaudeCodeCollector:
   - Replace the icon with a dimmed version (0.4 alpha)
   - Add a small timer overlay using a CATextLayer showing remaining minutes
4. Normal state: icon at full alpha, no animation

Respect NSWorkspace.shared.accessibilityDisplayShouldReduceMotion — skip animation if true.

Update StatusBarController to use MenuBarIconView as the status item's custom view instead of button image.
```

---

## PHASE 4 — SETTINGS WINDOW

### Prompt 4.1 — Settings Window Shell

```
Create the settings window system.

1. UI/Settings/SettingsWindowController.swift
   - NSWindowController subclass
   - Window: 500pt wide, 560pt tall, not resizable
   - Title: "Tokens for Breakfast — Settings"
   - Hosts NSHostingController with SettingsView
   - Singleton: static let shared = SettingsWindowController()
   - func showWindow() — brings window to front, activates app

2. UI/Settings/SettingsView.swift
   - TabView with .tabViewStyle(.automatic) or a custom segmented control
   - 5 tabs: Providers, Budgets, Notifications, License, About
   - Use backgroundPrimary as background (NSWindow.backgroundColor must also be set to match)
   - Each tab content is a separate view component (stub them as placeholder Text views for now)

Wire the gear icon in the popover footer to call SettingsWindowController.shared.showWindow().
```

---

### Prompt 4.2 — Providers Settings Tab

```
Implement UI/Settings/ProvidersSettingsView.swift

A List or VStack of provider rows. Each row contains:
- Provider logo/icon (SF Symbol or initials in colored circle)
- Provider name (medium 14pt)
- Toggle to enable/disable
- When enabled: an expandable area showing:
  - For API key providers (OpenAI, Gemini, OpenRouter, DeepSeek, Mistral):
    - SecureField for API key, placeholder "sk-..." 
    - "Save" button that stores in Keychain via KeychainHelper
    - "Test" button that attempts a minimal API call and shows success/error inline
  - For Claude Code:
    - If credentials found in Keychain: "Connected — Claude Code credentials found" with a green checkmark
    - If not found: "Connect" button that attempts to read Claude CLI credentials from the system Keychain (service: "claude") and copies relevant token to our Keychain
    - "Disconnect" button to remove stored credentials

Provider list order: Claude Code, OpenAI, Cursor, Gemini, GitHub Copilot, OpenRouter, DeepSeek, Mistral.

Store enabled/disabled state in UserDefaults with key "enabled_providers" as an array of provider name strings.

Persist changes immediately (no Save button at the bottom — inline save).
```

---

### Prompt 4.3 — Budgets Settings Tab (Pro Feature Gate)

```
Implement UI/Settings/BudgetsSettingsView.swift

If LicenseManager.shared.tier == .basic, show a Pro upgrade prompt instead of the full UI:
"Budget management is a Pro feature. Upgrade for $15 one-time to set daily and weekly spend limits per provider."
Include an "Upgrade to Pro" button that opens the pricing URL.

If tier == .pro, show:
- A global section: "Daily Budget" with a numeric field ($XX.XX) and a stepper
- Per-provider section: ForEach over enabled providers, each showing:
  - Provider name
  - Daily limit field
  - Weekly limit field
  - Alert threshold picker: "Alert me at 70% / 80% / 90% / 100%" (Picker with these options)

Below the provider list:
- "Reset all budgets" button (destructive, shows confirmation)

Save budget changes immediately to StorageManager.setBudget() on field commit (onSubmit or onChange with debounce).

Also trigger BudgetEngine.evaluate() after each change so alerts are re-evaluated immediately.
```

---

### Prompt 4.4 — Notifications Settings Tab

```
Implement UI/Settings/NotificationsSettingsView.swift

Sections:

1. Morning Digest
   - Toggle: "Send morning report" (default: ON)
   - Time picker: DatePicker showing only hours and minutes, default 8:00 AM
   - "Preview" button: generates and shows what today's digest would say as an alert

2. Rate Limit Warnings (Claude Code)
   - Toggle: "Warn when approaching rate limit" (default: ON)
   - Slider: "Alert threshold" — values 60%, 70%, 80%, 90%. Default 80%.
   - Label shows current selection: "Alert when window is 80% full"

3. Budget Alerts (Pro only — show upgrade prompt for Basic)
   - Toggle: "Alert when approaching daily budget" (default: ON)
   - Toggle: "Alert when daily budget is exceeded" (default: ON)

4. "Request notification permission" button — calls UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) if not yet granted.
   - Show current permission status: Allowed / Not Allowed / Not Requested

Persist all toggles to UserDefaults immediately.
```

---

### Prompt 4.5 — License Settings Tab

```
Implement UI/Settings/LicenseSettingsView.swift

And Core/License/LicenseManager.swift

LicenseManager:
- enum Tier: basic, pro, none
- @Published var tier: Tier = .none
- @Published var isActivated: Bool = false
- static let shared = LicenseManager()
- func activate(licenseKey: String) async throws — calls Lemon Squeezy validation
- func loadCachedLicense() — reads from Keychain on init, sets tier

Lemon Squeezy activation:
- POST to https://api.lemonsqueezy.com/v1/licenses/validate
- Body: { "license_key": key, "instance_name": "Mac-\(deviceID)" }
- Parse response: check status == "active", extract variant_name to determine tier
- On success: cache license key and tier in Keychain
- On failure: throw descriptive error

Device ID: use IOKit to get hardware UUID:
func getDeviceID() -> String — use IOPlatformUUID from IOKit

LicenseSettingsView layout:
- Current status card:
  - If .none: "Not activated" in textSecondary, with "Buy Basic ($8)" and "Buy Pro ($15)" buttons
  - If .basic: "Basic — Lifetime License" with green checkmark, device ID shown small
  - If .pro: "Pro — Lifetime License" with amber ☕ icon, device ID shown small
- License key input:
  - TextField placeholder: "XXXX-XXXX-XXXX-XXXX"
  - "Activate" button — calls LicenseManager.activate() async, shows loading spinner
  - Error message display below field
- "Contact support" link: opens mailto:hello@tokensforbreakfast.app
```

---

## PHASE 5 — HISTORY WINDOW

### Prompt 5.1 — History Window and Timeline Chart

```
Create the full history window.

1. UI/History/HistoryWindowController.swift
   - NSWindowController, 680pt x 520pt, resizable (min 560x400)
   - Title: "Tokens for Breakfast — History"
   - Singleton pattern
   - Hosts HistoryView in NSHostingController

2. UI/History/HistoryView.swift
   Main layout: HSplitView or HStack with:
   
   LEFT SIDEBAR (200pt fixed):
   - "Date Range" picker as a List-style selection:
     - Today
     - Last 7 Days  
     - Last 30 Days
     - Custom (shows a date range picker when selected)
   - Divider
   - Provider filter: CheckboxGroup for each enabled provider
   - Divider
   - Big total number: "TOTAL" label (tertiary 10pt caps) + cost in TFBFonts.display(32pt)
   - Token count: "X.X M tokens" in mono 13pt, textSecondary
   
   MAIN AREA (remaining width):
   - Custom segmented control tabs: Timeline / Providers / Models / Sessions
   - Content area below showing the selected tab view

3. UI/History/TimelineChartView.swift
   - Swift Charts BarChart
   - Data: DailyTotal array for selected range
   - Bars stacked by provider (different colors per provider)
   - X axis: date labels (auto-formatted based on range)
   - Y axis: cost in USD
   - ChartOverlay for hover state showing tooltip with day's breakdown

4. Wire "View Full History" button in PopoverView to open HistoryWindowController.shared.
```

---

### Prompt 5.2 — Export CSV (Pro)

```
Implement CSV export in the History window.

1. Add an "Export CSV" button in the HistoryView header, only visible if LicenseManager.shared.tier == .pro

2. On click, show a NSSavePanel:
   - Default filename: "tokens-for-breakfast-\(dateRange).csv"
   - Allowed file types: ["csv"]

3. CSV generation:
   - Headers: date,provider,model,tokens_input,tokens_output,cost_usd,session_id
   - Query StorageManager for UsageEvents in the selected date range
   - Format each row, escape fields properly
   - Write to chosen URL using String.write(to:atomically:encoding:)

4. After successful export, show a brief inline success message: "Exported X rows to Downloads"

5. For Basic users, show the button grayed out with a tooltip: "CSV export requires Pro — $15 lifetime"
```

---

### Prompt 5.3 — Model Optimization Tips (Pro)

```
Create Core/Budget/BudgetEngine.swift and the optimization tip logic.

BudgetEngine:
- static let shared = BudgetEngine()
- func evaluate() — called after each new usage event insert
  - Checks daily/weekly totals against budgets
  - Fires UNUserNotification if a threshold is crossed (debounced — don't fire more than once per hour per budget)

- func generateOptimizationTips() async -> [OptimizationTip]
  - Query last 7 days of UsageEvents grouped by model
  - For each model in the "expensive tier" (Opus, GPT-4o, o1), calculate total spend
  - Find the next cheaper model in the same provider family
  - Calculate hypothetical savings: same token volume at lower model price
  - Return tips where savings > $1.00

Create a struct OptimizationTip: Codable with fields:
- expensiveModel: String
- cheaperModel: String
- actualCostUSD: Double
- projectedCostUSD: Double
- savingsUSD: Double

Create UI/Components/OptimizationTipCard.swift:
- A SwiftUI view showing the tip with a lightbulb icon (💡)
- Two cost numbers side by side (actual vs projected)
- "Dismiss" button that marks the tip as dismissed in UserDefaults (don't show again for 7 days)

Display OptimizationTipCard views at the bottom of HistoryView, below the chart, in a VStack.
Only show tips if tier == .pro.
```

---

## PHASE 6 — ONBOARDING

### Prompt 6.1 — Onboarding Flow

```
Create the first-launch onboarding flow.

1. UI/Onboarding/OnboardingWindowController.swift
   - NSWindowController, 460pt x 520pt, not resizable, centered on screen
   - No close button (window has no title bar)
   - Shows on first launch (check UserDefaults key "hasCompletedOnboarding")

2. UI/Onboarding/OnboardingView.swift
   - State machine with @State var currentStep: Int = 0
   - 5 steps, animated transitions using .transition(.move(edge: .trailing).combined(with: .opacity))

Step 0 — Welcome:
- Large coffee cup emoji or SF Symbol (64pt)
- Title: "Good morning." in TFBFonts.display(size: 36)
- Subtitle: "Your AI spend, tracked locally, served every morning." in body 16pt, textSecondary
- "Get Started →" button in amber

Step 1 — Choose Providers:
- Title: "Which AI tools do you use?"
- Grid of provider logos with checkboxes (use ProviderLogoView component)
- "Continue →" button

Step 2 — Connect Providers (dynamic, one provider at a time):
- For each selected provider:
  - Show provider logo + name
  - Show appropriate credential input (API key or Connect button)
  - "Next" advances to next provider, "Skip" skips current

Step 3 — Set Budget:
- Title: "Set a daily budget (optional)"
- Subtitle: "We'll alert you when you're approaching it."
- A large Slider from $0 to $50, labeled with current value
- "I'll set this later" skip link
- "Set Budget →" button

Step 4 — Done:
- Coffee cup animation (scale in)
- "You're all set."
- "Your menu bar is ready. We'll check in tomorrow morning."
- "Open Menu Bar →" button — closes onboarding, sets UserDefaults "hasCompletedOnboarding" = true

After completing onboarding, set UserDefaults "hasCompletedOnboarding" = true and dismiss the window.
In AppDelegate, check this key on launch and show onboarding if false.
```

---

## PHASE 7 — MORNING DIGEST

### Prompt 7.1 — Digest Scheduler and Notification

```
Create Core/Digest/DigestScheduler.swift

Responsibilities:
1. Schedule a UNUserNotificationCenter request for the user's chosen digest time
2. Regenerate the notification each morning after delivery

func scheduleDigest(at time: Date) — cancels existing "morning-digest" notification identifier, then schedules a new repeating UNCalendarNotificationTrigger for the chosen hour/minute

func generateDigestContent() async -> (title: String, body: String)
- Query StorageManager for yesterday's total and per-provider breakdown
- Query for the 7-day total and weekly budget if set
- Format strings:

Basic tier body:
"Yesterday: \(CostFormatter.format(yesterdayTotal)) · \(topProvider) \(topProviderPercent)% · This week: \(CostFormatter.format(weekTotal))"

Pro tier body (richer):
"Yesterday: \(cost) (\(direction)\(percentDiff)% vs avg)\nWeek: \(weekCost) · \(budgetPercent)% of \(weekBudget) budget\nTop: \(topModel)"

Title: "☕ Morning Report — \(formattedDate)"

3. Register notification category "MORNING_DIGEST" with actions:
- "VIEW_HISTORY" — opens history window (Deep link via URL scheme tfb://open-history)
- "DISMISS" — no action

4. Implement UNUserNotificationCenterDelegate in AppDelegate to handle notification responses:
- If action == "VIEW_HISTORY": show HistoryWindowController

Register URL scheme "tfb" in Info.plist with handler in AppDelegate.
```

---

## PHASE 8 — POLISH AND FINAL WIRING

### Prompt 8.1 — Weekly Summary Export View (Pro)

```
Create UI/Components/WeeklySummaryExportView.swift

A SwiftUI view designed to be rendered off-screen and exported as a PNG image (1200x900 @2x).

Layout (fixed size, no adaptive layout needed):
- Dark background: backgroundPrimary
- Top left: coffee cup icon + "TOKENS FOR BREAKFAST" in display font, 18pt, amber
- Top right: "Weekly Report" + date range in body 12pt, textSecondary

Row 1 (large numbers):
- Two side-by-side stat blocks:
  - "TOTAL SPEND" label + big number in display 48pt
  - "DAILY AVERAGE" label + number in display 48pt

Row 2: Compact 7-bar chart using Swift Charts (non-interactive version)

Row 3: Provider breakdown horizontal bars:
- For each provider: name + colored bar (width proportional to %) + cost + percentage
- Max 4 providers shown

Row 4: Two callout stats:
- "Most used model: [model name]"
- "Peak day: [day name] ([cost])"

Footer:
- "tokensforbreakfast.app" in mono 11pt, textTertiary
- Subtle horizontal line separator above footer

Export function:
func exportAsPNG(data: WeeklySummaryData) async -> URL?
- Renders WeeklySummaryExportView using ImageRenderer
- Saves PNG to ~/Desktop/tokens-for-breakfast-weekly-YYYY-MM-DD.png
- Returns file URL for share sheet

Add "Share Weekly Report" button to HistoryView (Pro only), calling this export.
After export: show NSSharingServicePicker targeting the exported file.
```

---

### Prompt 8.2 — Auto-Launch at Login

```
Add "Launch at login" functionality.

In macOS 13+, use SMAppService (no LaunchAgent plist needed):
import ServiceManagement

In SettingsView or AppDelegate:
func setLaunchAtLogin(_ enabled: Bool) {
    if enabled {
        try? SMAppService.mainApp.register()
    } else {
        try? SMAppService.mainApp.unregister()
    }
}

func isLaunchAtLoginEnabled() -> Bool {
    return SMAppService.mainApp.status == .enabled
}

Add a toggle in the About settings tab: "Launch at login" bound to these functions.
Default: ask user during onboarding (Step 4 could include this toggle).
```

---

### Prompt 8.3 — Version Update Check

```
Add a lightweight update check mechanism.

1. Create a file VERSION at the project root with content: 1.0.0

2. Host a JSON file at: https://tokensforbreakfast.app/latest-version.json
   (Placeholder — implement the check now, host the file when the website is live)
   JSON format: { "version": "1.0.0", "download_url": "https://...", "release_notes": "..." }

3. Create Core/Utils/UpdateChecker.swift:
   func checkForUpdate() async -> UpdateInfo?
   - Fetches the JSON
   - Compares to current bundle version (CFBundleShortVersionString)
   - Returns UpdateInfo if remote version > current, nil otherwise

4. In AppDelegate applicationDidFinishLaunching:
   - After a 10-second delay, run the update check once
   - If an update is available, show a banner in the popover footer:
     "Version X.X available — Download" (amber text, tappable, opens download URL)

5. In Settings > About tab:
   - Show current version from Bundle
   - "Check for updates" button that runs checkForUpdate() manually and shows result inline
```

---

### Prompt 8.4 — Final Integration and Build Verification

```
Final integration pass. Complete the following:

1. In AppDelegate.applicationDidFinishLaunching:
   - Call StorageManager.shared initialization
   - Call LicenseManager.shared.loadCachedLicense()
   - Register fonts from bundle
   - Check UserDefaults "hasCompletedOnboarding" — if false, show OnboardingWindowController
   - If true, initialize DataCollectionManager.shared with all collectors and call startAll()
   - Initialize StatusBarController
   - Schedule DigestScheduler with user's saved time preference
   - Request notification permission if not already granted
   - Start update check (10s delay)

2. Verify all navigation paths work:
   - Popover opens/closes on menu bar click
   - Gear icon opens Settings
   - Arrow icon opens History
   - "View History" button in popover opens History
   - Right-click context menu works (Open History / Settings / Quit)
   - Notification tap opens appropriate window

3. Verify data flow:
   - ClaudeCodeCollector fires a test event
   - StorageManager inserts it correctly
   - DataCollectionManager.todayTotalUSD updates
   - StatusBarController label updates
   - PopoverView reflects new total

4. Build for Release (not Debug) and verify:
   - No Dock icon
   - No default white window appears
   - Menu bar item appears correctly
   - App bundle is signed (requires developer certificate)

5. For distribution preparation:
   - Set "Hardened Runtime" entitlements in project
   - Add entitlements file with:
     - com.apple.security.network.client = true (for API calls)
     - keychain-access-groups (for Keychain access)
   - Export for notarization: Archive > Distribute App > Direct Distribution
```

---

## APPENDIX A — Stub Data for Testing

During development, use this stub data injection to test UI without live API calls.

```swift
// In DataCollectionManager, add a debug mode:
#if DEBUG
func injectStubData() {
    let events: [UsageEvent] = [
        UsageEvent(id: UUID().uuidString, provider: "claude", model: "claude-sonnet-4-6",
                   tokensInput: 15000, tokensOutput: 4000, costUSD: 2.32,
                   sessionId: "sess_1", capturedAt: Date()),
        UsageEvent(id: UUID().uuidString, provider: "openai", model: "gpt-4o",
                   tokensInput: 8000, tokensOutput: 2000, costUSD: 0.75,
                   sessionId: "sess_2", capturedAt: Date()),
        UsageEvent(id: UUID().uuidString, provider: "cursor", model: "unknown",
                   tokensInput: 5000, tokensOutput: 1000, costUSD: 0.35,
                   sessionId: "sess_3", capturedAt: Date()),
    ]
    for event in events {
        try? StorageManager.shared.insertEvent(event)
    }
    refreshTodayTotal()
}
#endif
```

Add a "Inject Test Data" button in About tab (only visible in Debug builds).

---

## APPENDIX B — SwiftUI Preview Configuration

Add these preview configurations to key views for fast iteration in Xcode Canvas:

```swift
#Preview {
    PopoverView()
        .environmentObject(DataCollectionManager.preview)
        .frame(width: 340)
        .background(Color.backgroundPrimary)
}

extension DataCollectionManager {
    static var preview: DataCollectionManager {
        let manager = DataCollectionManager()
        manager.todayTotalUSD = 3.42
        // Add preview providers
        return manager
    }
}
```

---

## APPENDIX C — Lemon Squeezy Setup Checklist

Before shipping, complete these steps in Lemon Squeezy dashboard:

1. Create store: "Tokens for Breakfast"
2. Create product: "Tokens for Breakfast — Basic" at $8.00, type: Single payment
3. Create product: "Tokens for Breakfast — Pro" at $15.00, type: Single payment
4. Enable license keys for both products (Settings > License Keys)
5. Set license key format: XXXX-XXXX-XXXX-XXXX
6. Note your Store ID for use in LicenseManager activation calls
7. Set your store URL as the checkout destination for "Buy" buttons in the app
8. Configure order confirmation email to include license key prominently
9. Set up webhook (optional) for advanced activation tracking

The app's LicenseManager calls:
- POST https://api.lemonsqueezy.com/v1/licenses/validate
- No API key needed for validation — just the license key and instance name
