Files
busymirror/AGENTS.md
T
tomas.kracmarandClaude Sonnet 5 61cea918a6 Release 1.6.1
- Sync reminders: new "Sync reminders when mirroring" option (global and
  per-route) copies source event alarms into mirrored placeholders. CLI flag
  --sync-reminders.
- CLI polish: --help, --list-calendars, --status (all support --json).
  Scheduled/headless runs record last-run time/result/summary. Failure paths
  now exit with distinct nonzero codes (2 = no calendar access, 3 = no saved
  routes) instead of always exiting 0.
- Fix settings silently wiped on upgrade: SettingsPayload's auto-synthesized
  Codable threw on any settings blob missing a field added since (e.g. the
  new syncReminders key), failing the whole decode and letting the next
  autosave persist an empty state over real routes/filters. Custom decode
  now falls back per-field like Route already did, and loadSettingsFromDefaults
  recovers routes from the legacy routes.v1 key if settings.v2 comes back
  empty, repairing installs already hit by this.
- Roadmap: event-driven background sync (SMAppService + EKEventStoreChanged +
  wake notification) and an external MCP wrapper around the CLI queued next;
  decided against direct Google/CalDAV API integration since EventKit already
  covers it via System Settings accounts.

All 47 unit tests pass (45 existing + 2 new SettingsPayloadTests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 13:10:23 +02:00

9.5 KiB
Raw Blame History

BusyMirror — Agent Reference

This file is written for AI coding agents. It assumes you know nothing about the project.

Project Overview

BusyMirror is a macOS menu-bar utility that mirrors calendar events from a source calendar into one or more target calendars, creating busy-placeholder events so availability stays consistent across accounts and devices.

It is a single-platform macOS app written in Swift 5 and SwiftUI, using EventKit to read and write calendar data. The app runs as a menu-bar-only app (LSUIElement) with no Dock icon.

Key capabilities:

  • Manual or route-driven multi-source mirroring
  • Privacy mode: hide details (placeholder title)
  • DRY-RUN mode to preview changes without writing
  • Scheduled headless runs via a self-installed launchd LaunchAgent
  • Settings autosave/restore, plus Import/Export JSON
  • CLI support for headless/scripted runs

Technology Stack

Layer Technology
Language Swift 5.0
UI Framework SwiftUI + AppKit (menu bar, panels)
Calendar API EventKit (EKEventStore, EKEvent, EKCalendar)
Persistence UserDefaults (JSON-encoded settings), @AppStorage
Scheduling launchd / launchctl (user LaunchAgent)
Build System Xcode project (BusyMirror.xcodeproj) + Makefile
Target OS macOS 15.5+
Signing Ad-hoc (CODE_SIGN_IDENTITY = "-") — not notarized

No external Swift Package Manager dependencies are used. The project is self-contained.

Project Structure

BusyMirror/
├── BusyMirrorApp.swift          # App entry point; defines Window + MenuBarExtra
├── ContentView.swift            # Main UI, settings, CLI, scheduling (≈1800 lines)
├── MirrorEngine.swift           # EventKit mirror engine (read, deduplicate, merge, create/update/delete)
├── MirrorConfig.swift           # Configuration struct passed to the engine
├── MirrorUtils.swift            # URL builders, mirror detection, calendar labels
├── BlockMath.swift              # Block merging, gap calculation, overlap logic (Block.span factory)
├── EventFilters.swift           # Work-hours, title, and organizer filters
├── MenuBarSupport.swift         # `BusyMirrorAppController` (state coordinator) + menu bar view
├── AppLogStore.swift            # File-backed log store with rotation (AppLogStore enum)
├── Info.plist                   # LSUIElement, calendar usage descriptions
├── BusyMirror.entitlements      # App sandbox + calendar access entitlement
└── Assets.xcassets/             # AppIcon set and accent color

BusyMirror.xcodeproj/            # Xcode project (PBXFileSystemSynchronizedRootGroup — new .swift files are auto-included)
BusyMirrorTests/                 # Unit tests: BlockMathTests, EventFiltersTests, MirrorUtilsTests (45 tests)
BusyMirrorUITests/               # UI tests (empty)

Architecture note: ContentView.swift handles the SwiftUI view hierarchy, settings serialization, CLI argument parsing, launchd scheduling, and logging. The EventKit mirror engine lives in MirrorEngine.swift and is invoked from ContentView via makeEngine(). Pure helper logic (block math, filters, URL utilities) has been extracted into standalone files for testability.

When making changes, keep the existing data flow (@EnvironmentObject, @AppStorage, @State) intact in ContentView.swift.

Build and Release Commands

Makefile targets

make build-debug      # Debug build via xcodebuild
make build-release    # Release build via xcodebuild
make sign-app         # Ad-hoc sign the Release app (strip xattr, codesign)
make app              # Verify signed app exists
make package          # Create BusyMirror-<version>-macOS.zip + .sha256
make clean            # Clean derived data

Built products:

  • Unsigned release: build/DerivedData/Build/Products/Release/BusyMirror.app
  • Signed release: build/ReleaseSigned/BusyMirror.app

Xcode

  1. Open BusyMirror.xcodeproj.
  2. Select BusyMirror scheme → My Mac.
  3. Product → Build (or Archive for distribution).

Versioning

  • MARKETING_VERSION and CURRENT_PROJECT_VERSION live in project.pbxproj.
  • The Makefile extracts MARKETING_VERSION automatically for ZIP naming.
  • Update both Debug and Release build configurations when bumping the version.

Code Style Guidelines

  • Language: all code, comments, and user-facing strings are in English.
  • Concurrency: @MainActor is required on methods that mutate SwiftUI @State or call EventKit on the main thread. The compiler enforces strict concurrency.
  • Formatting: standard Swift style (4-space indentation). No external linter is configured.
  • Logging: use the log(_:) method inside ContentView; it appends to both the on-screen log editor and the persistent file log (~/Library/Logs/BusyMirror/BusyMirror.log).
  • Error handling: EventKit errors are caught and logged; they must never crash the app. The file logger swallows its own errors silently.

Testing

  • Unit tests exist in BusyMirrorTests/ for BlockMath, EventFilters, and MirrorUtils.
  • When adding logic, prefer extracting pure functions (e.g., block merging, gap calculation, filter logic) so they can be unit-tested.
  • Manual testing checklist for releases:
    1. Grant Calendar permission.
    2. Select a source and target, run DRY-RUN, verify log output.
    3. Toggle WRITE and run Mirror Now; verify placeholders appear in the target calendar.
    4. Move a source event and re-run; verify the placeholder updates.
    5. Test Cleanup Placeholders (dry-run and write).
    6. Add a route, install a schedule, verify the LaunchAgent plist is created in ~/Library/LaunchAgents/.
    7. Trigger a menu-bar sync and confirm the window opens if not visible.

Security and Privacy Considerations

  • Calendar data: the app reads and writes the users calendars via EventKit. It must handle permission denial gracefully.
  • Sandbox: the app uses the macOS app sandbox (com.apple.security.app-sandbox) and the com.apple.security.personal-information.calendars entitlement.
  • Signing: releases are ad-hoc signed only (codesign --sign -). They are not notarized. Gatekeeper may block the app on first launch; users may need to right-click → Open.
  • Loop guard: a sessionGuard set prevents mirroring an event into the same target twice in one run, and prefix-based detection (titlePrefix) prevents re-mirroring already-mirrored placeholders.
  • Logging: log files are written to the users ~/Library/Logs/BusyMirror/. No log data is transmitted externally.

CLI and Scheduling

The binary supports headless execution:

# Run saved routes (used by the LaunchAgent)
BusyMirror.app/Contents/MacOS/BusyMirror --run-saved-routes --write 1 --exit

# Manual route via 1-based UI indices
BusyMirror.app/Contents/MacOS/BusyMirror --routes "1->2,3" --write 1 --exit

Relevant flags: --privacy, --copy-notes, --sync-reminders, --all-day, --days-forward, --days-back, --merge-gap-hours, --mode, --exclude-titles, --exclude-organizers, --cleanup-only, --exit.

Diagnostic/query flags (no calendar write, exit immediately): --help/-h, --list-calendars [--json], --status [--json]. --status reads lastRunAtISO/lastRunOK/lastRunSummary in UserDefaults, written by recordRunResult(ok:summary:) at the end of every --routes/--run-saved-routes invocation. Exit codes: 2 = no calendar access, 3 = --run-saved-routes with no saved routes.

Scheduled runs are implemented by generating a launchd plist in ~/Library/LaunchAgents/com.cqrenet.BusyMirror.saved-routes.plist and bootstrapping it with launchctl. The app removes and re-bootstraps the agent on every "Install Schedule" click.

Key Files to Know

File Purpose
BusyMirror/ContentView.swift UI, settings, CLI, scheduling
BusyMirror/MirrorEngine.swift EventKit mirror engine (runMirror, runCleanup, index persistence)
BusyMirror/MirrorConfig.swift Configuration struct for mirror runs
BusyMirror/MirrorUtils.swift Mirror URL builders, event detection, calendar labels
BusyMirror/BlockMath.swift Block merging, gap calculation, overlap logic
BusyMirror/EventFilters.swift Work-hours, title, and organizer filters
BusyMirror/BusyMirrorApp.swift App struct, window scene, menu-bar extra
BusyMirror/MenuBarSupport.swift @MainActor app controller + menu bar SwiftUI view
BusyMirror/AppLogStore.swift File-backed log with rotation (~/Library/Logs/BusyMirror/)
BusyMirror/Info.plist LSUIElement, calendar usage descriptions
BusyMirror/BusyMirror.entitlements Sandbox + calendar entitlement
Makefile Reproducible build, sign, and package targets
CHANGELOG.md Release notes (human-readable)
ROADMAP.md Planned features

Notes for Agents

  • Do not add third-party dependencies unless the user explicitly asks. The project intentionally has zero external packages.
  • If you refactor ContentView.swift, preserve @AppStorage keys and UserDefaults keys exactly; users have existing settings on disk.
  • The mirror engine (MirrorEngine.swift) is @MainActor and accepts an EKEventStore plus a logging closure. It does not directly mutate SwiftUI @State; ContentView manages all view state.
  • When modifying build settings, update both Debug and Release configurations in project.pbxproj, and update CHANGELOG.md if the change is user-visible.
  • Do not run git commit, git push, or similar operations unless explicitly asked.