Fazm — a macOS desktop app (Swift). Open source at github.com/mediar-ai/fazm.
See scripts/SESSION-RECORDING.md for full guide — toggle per-user recording, view chunks, architecture.
When investigating a user-reported bug, always start by pulling their Sentry + PostHog logs (user-logs skill or user-issue-triage skill) before reading code.
- App log file:
/private/tmp/fazm-dev.log(dev builds) or/private/tmp/fazm.log(production)
Replay the post-onboarding tutorial:
xcrun swift -e 'import Foundation; DistributedNotificationCenter.default().postNotificationName(.init("com.omi.replayTutorial"), object: nil, userInfo: nil, deliverImmediately: true); RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0))'Send a text query to the floating bar (no voice/UI needed):
xcrun swift -e 'import Foundation; DistributedNotificationCenter.default().postNotificationName(.init("com.fazm.testQuery"), object: nil, userInfo: ["text": "your query here"], deliverImmediately: true); RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0))'Run the full tutorial programmatically (skips overlay, auto-sends all 3 steps). See test-tutorial skill for details:
xcrun swift -e 'import Foundation; DistributedNotificationCenter.default().postNotificationName(.init("com.fazm.testTutorial"), object: nil, userInfo: nil, deliverImmediately: true); RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0))'Full programmatic control of the floating bar, replacing the need for macOS accessibility/MCP automation. Send a com.fazm.control distributed notification with ["command": "<cmd>"] in userInfo.
Get state (writes JSON to /tmp/fazm-control-state.json):
xcrun swift -e 'import Foundation; DistributedNotificationCenter.default().postNotificationName(.init("com.fazm.control"), object: nil, userInfo: ["command": "getState"], deliverImmediately: true); RunLoop.current.run(until: Date(timeIntervalSinceNow: 1.0))'
cat /tmp/fazm-control-state.jsonSupported commands:
| Command | Description |
|---|---|
getState |
Writes full state JSON to /tmp/fazm-control-state.json |
newChat |
Starts a new chat session |
popOut |
Pops conversation out to a detached window |
setModel:<id> |
Sets AI model (e.g. setModel:claude-sonnet-4-6 or setModel:claude-opus-4-6) |
toggleVoice |
Toggles voice response (TTS) on/off |
setVoice:on / setVoice:off |
Explicitly sets voice response |
show / hide / toggle |
Controls floating bar visibility |
openInput |
Opens the AI input field |
sendFollowUp:<text> |
Sends a follow-up message in active conversation |
setWorkspace:<path> |
Sets the working directory |
State JSON includes: model, modelLabel, voiceEnabled, workspace, isVisible, showingAIConversation, showingAIResponse, isAILoading, isVoiceListening, chatHistoryCount, displayedQuery, queueCount, isTutorialActive, availableModels, and optionally currentMessagePreview/isStreaming.
Messages are stored in ~/Library/Application Support/Fazm/users/<UUID>/fazm.db (both prod and dev share this directory). To find the active user for the currently running build:
defaults read com.fazm.desktop-dev auth_userId # dev build (Fazm Dev)
defaults read com.fazm.app auth_userId # prod build (Fazm)These return different UUIDs even for the same Apple ID — dev and prod create separate user records. Always use this before querying or polling any SQLite DB; never guess by timestamp.
Check errors in the latest (or specific) release using the sentry-release skill:
./scripts/sentry-release.sh # new issues in latest version (default)
./scripts/sentry-release.sh --version X # specific version
./scripts/sentry-release.sh --all # include carryover issues
./scripts/sentry-release.sh --quota # billing/quota statusSee .claude/skills/sentry-release/SKILL.md for full documentation.
When debugging issues for a specific user (crashes, errors, behavior), use the user-logs skill:
# Sentry (crashes, errors, breadcrumbs)
./scripts/sentry-logs.sh <email>
# PostHog (events, feature usage, app version)
./scripts/posthog_query.py <email>See .claude/skills/user-logs/SKILL.md for full documentation and API queries.
A MacStadium Mac mini (no Xcode, no Homebrew, no Node) is available for testing what real users experience. Use the macstadium skill when reproducing user-reported bugs, validating onboarding/first-run flows, or checking that a release works on a fresh machine. The macos-use-remote MCP provides GUI automation on it.
Push a v*-macos tag to trigger a release:
git tag v0.2.4+16-macos && git push origin v0.2.4+16-macosCodemagic (codemagic.yaml, workflow fazm-desktop-release) — runs on Mac mini M2:
- Builds universal binary (arm64 + x86_64)
- Signs with Developer ID, notarizes with Apple
- Creates DMG + Sparkle ZIP
- Publishes GitHub release Sparkle auto-update delivers the new version to users.
Pushing Backend/** changes to main auto-deploys to Cloud Run via .github/workflows/deploy-backend.yml.
Uses Workload Identity Federation (no stored keys) → github-actions-deploy@fazm-prod.iam.gserviceaccount.com.
Codemagic CLI & API:
- Token:
$CODEMAGIC_API_TOKEN(set in~/.zshrc) - App ID:
69a8b2c779d9075efc609b8d - List builds:
curl -s -H "x-auth-token: $CODEMAGIC_API_TOKEN" "https://api.codemagic.io/builds?appId=69a8b2c779d9075efc609b8d" | python3 -c "import json,sys; [print(f\"{b.get('status','?'):12} tag={b.get('tag','-'):30} start={(b.get('startedAt') or '-')[:19]}\") for b in json.load(sys.stdin).get('builds',[])[:5]]"
To promote: ./scripts/promote_release.sh <tag> (staging → beta → stable). Never promote without explicit user approval — releasing to staging, beta, and stable are separate decisions.
Runtime env vars (.env.app):
- Local: edit
.env.app(gitignored, contains secrets) - CI/CD: the
FAZM_APP_ENVsecret in Codemagic'sfazm_secretsgroup holds the base64-encoded.env.app - When adding/changing env vars in
.env.app, you MUST also updateFAZM_APP_ENVin Codemagic UI (Settings → Environment variables → fazm_secrets). The Codemagic API cannot read/write team-level variable groups — UI only. - Generate the base64 value:
cat .env.app | base64 - The build will fail if required Vertex vars are missing (verified in codemagic.yaml)
Bundled skills live in Desktop/Sources/Resources/BundledSkills/ as {name}.skill.md files. This is the only place to manage them — adding or removing a file there is all that's needed. SkillInstaller.swift auto-discovers them at runtime; no code change required.
Category display for onboarding is in categoryMap inside SkillInstaller.swift.
Do NOT touch ~/fazm/skills/ for bundling purposes — that directory is for publishing skills to skillhu.bz/skills.sh and is unrelated to the app bundle.
./run.sh is the ONLY command you ever run. It builds everything (ACP bridge, Swift app, app bundle), copies all resources, and launches. There is ONE flow, ONE command.
NEVER run any build command directly:
- No
npm run build, noxcrun swift build, noswift build, noxcodebuild - No
open, no launching frombuild/ run.shdoes ALL of this. Running builds independently creates stale processes, orphaned locks, and duplicate work.
run.sh manages ONE lock: /tmp/fazm-build.lock. It acquires it automatically on start, releases on exit. Do NOT create locks manually.
./run.shbuilds "Fazm Dev" → installs to/Applications/Fazm Dev.app(bundle ID:com.fazm.desktop-dev)- Production "Fazm" (bundle ID:
com.fazm.app) is built by the Codemagic CI pipeline only - To check app state:
cat /tmp/fazm-dev-status(see "Checking App State" below) - Legacy
com.omi.*bundle IDs still appear in cleanup/migration code (TCC permission resets, old app bundle removal) for users who had the app when it was called Omi
Multiple agents work on this codebase simultaneously. run.sh handles locking automatically; it will wait if another build is active, and detects stale locks from dead processes.
- Just run
./run.sh; it handles everything. If another agent holds the lock, it waits (up to 5 min). - NEVER manually delete
/tmp/fazm-build.lockor runrm -rf /tmp/fazm-build.lock. The lock is a directory managed by the scripts. Manually deleting it defeats the entire concurrency system and causes parallel builds to collide. - NEVER kill the app (
pkill -f "Fazm Dev") before building.run.shhandles stopping the old app as part of its flow. Killing it externally orphans the lock. - If you only need to test with distributed notifications (e.g.,
com.fazm.testQuery) and the app is already running, you do NOT need to runrun.sh. Just send the notification.
Always read /tmp/fazm-dev-status first. This is the single source of truth for the app lifecycle. Do NOT guess state from pgrep, ps aux, or log tailing.
cat /tmp/fazm-dev-statusThe file contains one line in the format <state> <pid> <unix_timestamp>:
building <run.sh_pid> <ts>= build in progress, wait for itrunning <app_pid> <ts>= app is running, verify withkill -0 <app_pid>exited <app_pid> <ts>= app exited, safe to run./run.shfailed <ts> <reason>= last build/launch failed
Decision tree:
- Read
/tmp/fazm-dev-status - If
running <pid>: checkkill -0 <pid> 2>/dev/null. If alive, the app is running; send test notifications directly. If dead, the status is stale; safe to run./run.sh. - If
building <pid>: checkkill -0 <pid> 2>/dev/null. If alive, wait. If dead, stale; safe to run./run.sh. - If
exitedorfailedor file missing: safe to run./run.sh.
NEVER use pgrep, ps aux | grep, or log file checks to determine whether to build/kill/restart. Use the status file.
The watchdog holds the lock as long as the app process is alive (checked every 10s via the app PID). It only releases the lock and exits when the app process dies. The log file (/private/tmp/fazm-dev.log) is append-only; it is never truncated between runs.
If run.sh itself appears stalled (e.g., swift-build at 0% CPU for >10 minutes), first check if the holder PID is alive:
cat /tmp/fazm-build.lock/pid && cat /tmp/fazm-build.lock/script
# Then check: ps -p <pid>Only if the holder process is confirmed dead AND the stale-lock detection hasn't cleaned it up, escalate to the user. Do NOT manually delete the lock.
- ALWAYS test your changes — see global CLAUDE.md "After Implementing Changes — MANDATORY Testing" for the full workflow
- UI/visual changes: run
./run.sh, then use macOS automation (MCP macos-use) to navigate to the relevant screen and screenshot to verify - Logic/backend changes: use programmatic test hooks (distributed notifications, etc.) to trigger and verify
- Use the
test-localskill for the build → run → test → iterate workflow - See
.claude/skills/test-local/SKILL.mdfor details
After completing a desktop task with user-visible impact, append a one-liner to unreleased in desktop/CHANGELOG.json:
python3 -c "
import json
with open('CHANGELOG.json', 'r') as f:
data = json.load(f)
data.setdefault('unreleased', []).append('Your user-facing change description')
with open('CHANGELOG.json', 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
"Guidelines:
- Write from the user's perspective: "Fixed X", "Added Y", "Improved Z"
- One sentence, no period at the end
- Skip internal-only changes (refactors, CI config, code cleanup)
- HTML is allowed for links:
<a href='...'>text</a> - Commit CHANGELOG.json with your other changes (same commit is fine)