# 🤖 eleven — the command-line client

`eleven` is Eleven Messenger in your terminal: a full E2EE member, not a
gateway or a bot. It signs in with the same passkey backup or invite links
every device uses, holds its private keys in local files, and
encrypts/decrypts everything itself — the servers see exactly what they see
from any other device: ciphertext.

It's a separate small binary (like `push/` and `home/`): members install the
client, operators install the server.

## Installing

**From the Mac app** (how members get it): the Mac app bundles the CLI at
`Eleven.app/Contents/Helpers/eleven` — universal, signed, and updated by
Sparkle alongside the app (the "Embed eleven CLI" build phase in
`ios/project.yml` compiles it into every build). Settings → General →
**Command-line tool** → Install… drops a symlink where you choose
(`/usr/local/bin` keeps it on PATH); the app is sandboxed, so the folder pick
runs through the powerbox, and when a folder needs an administrator the pane
hands you the one Terminal command instead:

```sh
sudo mkdir -p /usr/local/bin && \
  sudo ln -sf "/Applications/Eleven.app/Contents/Helpers/eleven" /usr/local/bin/eleven
```

**From source** (developers):

```sh
go build -o eleven ./eleven
```

## Sign in with Eleven (`eleven signin`)

One passkey, every space. `eleven signin` pulls the spaces saved in your
Eleven account (the home server's passkey-protected backup) and signs the
terminal into all of them at once.

A terminal can't run a passkey ceremony, so the browser does it — the same
handoff the native Mac app uses when its passkey provider can't help:

1. `eleven signin` mints an ephemeral keypair + nonce, opens a local loopback
   listener, and sends your browser to home's restore page (payload in the
   `#fragment` — the home server never sees it).
2. You approve with Face ID / Touch ID / your passkey. The page decrypts your
   backup **in the browser**, seals the home session to the ephemeral key, and
   redirects to `http://127.0.0.1:<port>` — hardcoded loopback, ciphertext all
   the way.
3. The CLI unwraps it, pulls the vault, and writes one identity per space
   under `~/.eleven/spaces/`. Archived spaces are skipped.

SSH'd into a box whose browser is elsewhere? `eleven signin --paste` prints
the link and takes the sealed code by paste instead of a redirect.

The home session lands in `~/.eleven/home.json`, so later `eleven signin`
runs re-sync silently — no browser. Note the trust profile: that file (like
the native apps' Keychain copy) can fetch and open your whole backup, and the
space session files are the space keys themselves. `~/.eleven` *is* your key
ring; deleting it signs the terminal out of everything.

## Joining one space by invite (`eleven join`)

```sh
eleven join                      # prompts for an invite link (and your name)
eleven join --name "Paul" 'https://yourspace.11ch.at/join/…#…'
```

All three link shapes work:

- **A fresh invite** makes you a new member with a new keypair.
- **A friend's invite link** (the kind that opens a DM with them) joins you
  *and* unlocks that chat — the DM key is re-wrapped to your new keypair
  locally during the claim.
- **An add-device link** (from your own account) signs the terminal in as
  *you*: the link carries your keypair in its `#fragment`, so the terminal
  becomes another of your devices and can read everything your others can.

Your identity lands in `~/.eleven/spaces/<host>.json` (0600) — or pass
`-session file` (`ELEVEN_SESSION`) to pin one identity file and ignore the
store, for scripts. Session files **are** private keys: anyone who reads one
can read those chats, and deleting your only device's key loses it for good —
same as any device.

Two honest gaps on a *fresh* join, both inherent to how keys travel: your
roster name waits for the instance key, which another member's device grants
once it's online (`eleven` retries the publish on every run until it lands —
your messages carry your name regardless), and chats appear only once someone
DMs you or adds you to a group. Joining a space where Eleven runs, the
welcome DM shows up on its own.

## Talking

```sh
eleven spaces                    # what this terminal is signed into
eleven chats                     # your chats across every space
eleven chat                      # open your most recently active chat
eleven chat kitchen              # …or match one by name (case-insensitive)
eleven chat family/kitchen       # space/name scopes when names collide
eleven send eleven "what's the weather like for Saturday?"
git log --oneline -5 | eleven send eleven   # pipe stdin as the message
```

`eleven chat` is a live loop: incoming messages print as they arrive over the
WebSocket, and anything you type sends on Enter (Ctrl-C or Ctrl-D leaves).
DMing one of the space's bots this way is Claude-in-your-terminal over your
own E2EE infrastructure — the bot can't tell the terminal from the app.

## What v1 deliberately leaves out

Rendered messages are new chat lines (plus `/me`-style action lines and a
"[n button(s)]" note on messages carrying chips). Not yet rendered: edits,
deletions, reactions, typing indicators, attachments, link previews — and
there's no thread creation from the CLI (start chats in the app). Incoming
lines also interleave with whatever you're typing, classic-IRC style; a
proper composer line is a follow-up.

## How it's built

`internal/client` is the E2EE member client layer — join/claim
(`join.go`, mirroring `web/static/join.js` headlessly), the WebSocket
transport with thread-key unwrap, cursor-gated delivery and reconnect
(`client.go`, the transport half of `internal/bot`'s runtime with the bot
policy left out), and the invite-link parser (`invite.go`, mirroring
`web/static/invite.js`). The bot sidecar still carries its own copy of the
transport; moving it onto `internal/client` is the planned follow-up, so the
two can't drift. Session files are byte-compatible across the CLI, the Go bot,
and the Node sidecar.

E2E coverage lives in `eleven_client_test.go` (root package, against the real
router): all three join paths, name decryption, live delivery, and both-ways
sends.
