# macos reference

> Reference for the code under `macos`: what it exports, how it is invoked, its options and defaults, and its error cases.

- Repository: egoist/lorca
- GitHub: https://github.com/egoist/lorca
- Human docs: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6
- Complete Markdown: https://grok-wiki.com/public/docs/egoist-lorca-2cf67495e5e6/llms-full.txt

## Source Files

- `macos/Sources/Lorca/App/CLILauncher.swift`
- `macos/Sources/Lorca/App/AppDelegate.swift`
- `macos/Package.swift`
- `macos/Sources/Lorca/Model/Protocol.swift`
- `crates/cli/src/api.rs`
- `macos/Sources/Lorca/main.swift`

---

---
title: "macos reference"
description: "Reference for the code under `macos`: what it exports, how it is invoked, its options and defaults, and its error cases."
---

`macos/` is the AppKit SPM package that builds **Lorca** / **Lorca Dev**. The executable talks only to a local `lorca` CLI over `ws://127.0.0.1:<port>/ws`, probes that port on launch, and if nothing answers spawns `lorca serve` with readiness on stdout.

## Package surface

| Item | Value |
| --- | --- |
| Package name | `Lorca` (`macos/Package.swift`) |
| Platform | macOS 14+ |
| Executable target | `Lorca` → `Sources/Lorca` |
| Markdown FFI | binary `LorcaMarkdownFFI` + Swift target `LorcaMarkdown` |
| Updater | Sparkle 2.9+ (release builds) |
| Tests | `LorcaTests` under `Tests/Notifications` |

:::files
macos/
├── Package.swift
├── Resources/          # icns, en.lproj, zh-Hans.lproj
├── Sources/Lorca/
│   ├── main.swift      # NSApplication + AppDelegate
│   ├── App/            # launch, prefs, updater, menus
│   ├── Model/          # AppStore, CLIClient, Wire protocol
│   ├── Main/ Chat/ Sidebar/ Settings/ …
│   └── …
├── Libraries/          # LorcaMarkdownFFI.xcframework (build output)
└── Tests/
:::

**Bundle IDs** (from `scripts/app.ts`): production `app.lorca` → app name **Lorca**; debug `app.lorca.dev` → **Lorca Dev**.

## How the app starts

```mermaid
sequenceDiagram
  participant Main as main.swift
  participant AD as AppDelegate
  participant Store as AppStore
  participant Launch as CLILauncher
  participant Worker as CLILaunchWorker
  participant CLI as lorca serve
  participant WS as CLIClient

  Main->>AD: NSApplication.run
  AD->>Store: start()
  Store->>Launch: ensureRunning()
  Launch->>Worker: probe 127.0.0.1:port (1.5s)
  alt port already open
    Worker-->>Launch: ready(external: true)
  else no listener
    Worker->>CLI: serve --port --parent-pid --ready-stdout
    CLI-->>Worker: JSON line event=ready
    Worker-->>Launch: ready(external: false)
  end
  Launch-->>Store: onReady
  Store->>WS: connect ws://127.0.0.1:port/ws
  WS->>CLI: bootstrap
  CLI-->>Store: Snapshot
```

1. `main.swift` creates `AppDelegate`, sets activation policy `.regular`, runs `NSApplication`.
2. `AppDelegate.applicationDidFinishLaunching` builds the menu, observes `AppStore`, calls `store.start()`.
3. With a prior identity (`Preferences.hadIdentity`), the main window opens immediately; otherwise the app waits for the CLI answer before choosing main vs onboarding.
4. After the first successful (or failed) startup path leaves `isStarting`, notifications and Sparkle start once.

Quit path: `applicationWillTerminate` and SIGTERM both call `store.stop()` → disconnect websocket and terminate the child CLI.

## CLI process control

### Locate binary

`CLILaunchWorker.locateBinary` checks, in order:

1. `LORCA_CLI` env path  
2. `Contents/Resources/bin/lorca` in the app bundle  
3. `~/.cargo/bin/lorca`  
4. `/opt/homebrew/bin/lorca`  
5. `/usr/local/bin/lorca`  
6. each `PATH` entry + `/lorca`

### Spawn arguments

When the probe fails, the worker runs:

```bash
lorca serve --port <port> --parent-pid <app_pid> --ready-stdout
```

- stderr (and readiness stdout lines) append to `~/Library/Logs/<AppInfo.name>/cli.log`
- Ready signal: one JSON line with `"event":"ready"` and `"port"` matching the requested port
- Child exit triggers exponential restart: delay starts at **1s**, doubles up to **15s**

### Launcher status

| Status | Meaning |
| --- | --- |
| `idle` | Not started yet |
| `probing` | TCP probe in flight |
| `starting` | Child process spawning |
| `running(external:)` | Port ready; `true` if reused, `false` if app-spawned |
| `failed(String)` | User-visible failure message |

`offlineStatus` in the UI is `launcher.status.message`.

## Environment and defaults

### Ports and home

| Build | Bundle ID | Default port | Default `LORCA_HOME` | Suggested serve command |
| --- | --- | --- | --- | --- |
| Lorca | `app.lorca` | `4862` | `~/.lorca` | `lorca serve` |
| Lorca Dev | `app.lorca.dev` | `4863` | `~/.lorca-dev` | `lorca serve --home ~/.lorca-dev --port 4863` |

### Environment variables the app reads or sets

<ParamField body="LORCA_PORT" type="int">
Overrides `Preferences.cliPort` when `> 0`. Wins over UserDefaults.
</ParamField>

<ParamField body="LORCA_CLI" type="path">
Absolute path to the `lorca` binary; skips bundle/PATH search.
</ParamField>

<ParamField body="LORCA_HOME" type="path">
CLI data directory. If unset at spawn time, set to `AppInfo.defaultCLIHome`.
</ParamField>

<ParamField body="LORCA_DEFAULT_RELAY_URL" type="url">
Release builds set this to `https://relay.lorca.app` when unset. Dev builds leave it alone.
</ParamField>

<ParamField body="RUST_LOG" type="string">
Defaults to `lorca=info` for the child if unset.
</ParamField>

<ParamField body="LORCA_MOCK" type="1">
Skips the real CLI; `AppStore` uses seeded mock data and `ReplyEngine`.
</ParamField>

<ParamField body="LORCA_TRACE_STARTUP" type="1">
Writes launch milestones to `~/Library/Logs/<AppInfo.name>/startup.log`.
</ParamField>

Websocket URL the client always uses:

```text
ws://127.0.0.1:<Preferences.cliPort>/ws
```

Maximum WebSocket message size: **256 MiB** (snapshots carry full chat histories).

## Preferences (`UserDefaults`)

| Key | Default | Role |
| --- | --- | --- |
| `lorca.hadIdentity` | `false` | Open main window immediately on next launch |
| `lorca.cliPort` | build default (`4862` / `4863`) | Local CLI port; overridden by `LORCA_PORT` |
| `lorca.relayURL` | `""` | Settings › Advanced relay override |
| `lorca.selection` | nil | Persist selected chat (dev relaunch) |
| `lorca.showsInspector` | `true` | Inspector visibility |
| `lorca.sendOnReturn` | `true` | Composer Return sends |
| `lorca.showTimestamps` | `true` | Message timestamps |
| `lorca.dictationLanguage` | nil | Speech locale; nil = system |
| `AppleLanguages` (app domain) | nil | App UI language (`en`, `zh-Hans`, or system) |

`Preferences.reset()` clears only `hadIdentity` and `selection`.

Changing the CLI port in Settings updates `Preferences.cliPort` and reconnects through `AppStore.reconnect()` → launcher re-probe/spawn.

## WebSocket request surface

Frames: `{ "id": <int>, "method": "<name>", "params": {…} }`.  
Success: `{ "id", "result" }`. Error: `{ "id", "error": { "message" } }`.  
Push events: `{ "event": "<name>", "data": … }` (snake_case keys; Swift decodes with `convertFromSnakeCase`).

### Lifecycle / identity

| Method | Role |
| --- | --- |
| `hello` | Version, identity flags, relay status |
| `bootstrap` | Full `Wire.Snapshot` (first connect) |
| `identity.create` / `restore` / `forget` / `delete` | Account lifecycle |
| `pair.start` / `status` / `cancel` / `accept` / `abort` | Device pairing |
| `device.rename` / `device.unpair` | Device management |
| `config.set` | Set `relay_url` |
| `sync.wake` | Force relay sync |
| `ui.watching` | Chat currently on screen (`chat_id` or null) |

### Chats, bots, routines, plugins

| Group | Methods |
| --- | --- |
| Bots | `bots.create`, `bots.update`, `bots.delete`, `bots.memory`, `bots.memory.write` |
| Chats | `chats.create`, `chats.dm`, `chats.send`, `chats.stop`, `chats.compact`, `chats.delete`, `chats.rename`, `chats.pin`, `chats.add_bot`, `chats.remove_bot`, `chats.set_owner`, `chats.search`, `chats.messages`, `chats.mark_read`, `chats.permission` |
| Routines | `routines.create`, `routines.update`, `routines.delete`, `routines.run`, `routines.describe` |
| Marketplace / plugins | `marketplace`, `plugins.install`, `uninstall`, `set_variables`, `connect`, `detail` |
| Auto-review | `auto_review.set` |
| Shell cards | `bash.stdin`, `bash.stop` |
| Files | `files.path` |
| Providers | `providers.api_key`, `providers.connect_*`, `providers.auth.cancel`, `providers.disconnect` |

Unknown methods return `unknown method <name>` from `crates/cli/src/api.rs`.

### Events `AppStore` handles

`snapshot`, `roster.changed`, `message.added` / `message.updated` / `message.removed`, `chat.removed`, `job.started` / `job.finished` / `job.retry` / `job.thinking`, `chat.usage`, `relay.status`, `identity.changed`.

Bootstrap buffers events until the snapshot applies, then drains them so the UI never flashes empty roster state.

## Error cases

| Failure | Cause | What you see / do |
| --- | --- | --- |
| `missingBinary` | No bundled CLI and none on PATH/`LORCA_CLI` | “The lorca CLI is not bundled…” — install CLI or set `LORCA_CLI` |
| `launch(binary, reason)` | `Process.run` threw | “Could not start \<path\>: …” |
| `exited(code, log)` | Child terminated | “The CLI exited with code N. See cli.log.” — app retries with backoff |
| `startupClosed(log)` | stdout closed before ready JSON | “The CLI closed its startup channel…” — check `cli.log` |
| Probe timeout (1.5s) | Nothing on port | Spawn path above |
| `The Lorca CLI is not running` | Request while disconnected | Wait for reconnect or use Reconnect |
| `The CLI connection closed/dropped` | Socket died mid-request | Pending requests fail; client reconnects (0.4s → up to 5s) |
| `bootstrap failed` | Connected but snapshot request failed | Loading ends; offline UI with `offlineStatus` |
| Startup deadline | No snapshot in **2.5s** | `isStarting` clears; offline recovery controls |
| `relayUpdateRequired` | Relay rejects this protocol | One alert per launch: update via Sparkle when enabled |
| `unknown method` | Client/CLI version skew | Method string in error message |

<Warning>
On launcher `.starting` or `.failed`, `AppStore` disconnects the websocket so retries do not race an exiting child.
</Warning>

## Build, run, verify

| Command | Result |
| --- | --- |
| `bun run dev` | Rebuild CLI + Lorca Dev on Rust/Swift changes; relaunch via `open` |
| `bun run build` | Release `Lorca.app` via `scripts/app.ts` / `build.ts` |
| `bun run release-mac <version>` | Sign, notarize, publish Sparkle feed (see `docs/releasing-mac.md`) |
| `bun run test:mac-startup` | Compiles and runs launch-concurrency tests for `CLILaunchWorker` |
| `swift test` (in `macos/`) | Notification-focused unit tests |

Release updater: Sparkle feed `https://mac-releases.lorca.app/appcast.xml`, EdDSA key in Info.plist (`SUPublicEDKey`). **DEBUG** builds disable Sparkle (`Updater.isEnabled == false`).

<Check>
After a healthy launch: websocket connects, `bootstrap` returns a snapshot, `isConnected == true`, and chats or onboarding appear. With `LORCA_TRACE_STARTUP=1`, `startup.log` shows marks through `snapshot presented`.
</Check>

## Related pages

<CardGroup>
  <Card title="Architecture" href="/architecture">
    App ↔ CLI ↔ relay process boundaries and data flow.
  </Card>
  <Card title="crates/cli reference" href="/ref-crates-cli">
    `lorca serve` flags, home directory layout, and websocket server details.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Env vars and config keys shared across app and CLI.
  </Card>
  <Card title="Deployment" href="/deployment">
    Mac release, notarization, and Sparkle publishing.
  </Card>
</CardGroup>

Next: open `macos/Sources/Lorca/App/CLILauncher.swift` and confirm your port/`LORCA_HOME` match the Lorca vs Lorca Dev row above.
