# CLI & Scriptability API

> Control crystl from the command line. Open gems, manage shards, approve permissions, and read terminal output programmatically.

The `crystl` CLI lets you control crystl from any terminal. List gems, approve permissions, read terminal output, and script multi-agent workflows, all without touching the GUI.

> **Free vs Guild.** The rule is simple: any **read** is free, any **mutation** needs [Guild](/pricing) (filing feedback and `copy` are the exceptions, free on every tier). Free on any crystl: `status`, `gems`, `shards`, `screen`, `history`, `fs`, `pending`, `events`, `wait`, `docs`, `token`, `doctor`, `render`, `copy`, `hero list`, `party list`, `resurrect list`, `workbench list`, `sidequest status`, the `list` forms of `facet` and `askuser`, and `report`. Guild-only (they **change** something, so without a membership they return `403 guild_required`): `open`, `close`, `shard create`, `shard close`, `shard rename`, `gauntlet`, `resurrect`, `hero summon`, `merge`, `send`, `approve`, `deny`, `quest start`/`clear`/`end`, `sidequest start`/`end`, `party create`/`delete`, workbench edits and panel controls (alias `backlog`), `facet add`/`slot`/`remove`, `ssh bridge-address`, and `askuser answer`. This is why [`/fanout`](/docs/fanout), which spawns and merges workers, is Guild-only.

## Setup

The CLI ships inside `Crystl.app`. Open crystl and choose **Help → install command line tool…**, which symlinks `/usr/local/bin/crystl` to the binary inside the app bundle (`Crystl.app/Contents/Helpers/crystl`). You'll see one macOS admin prompt the first time, then `crystl` is available in any new shell.

Prefer to do it by hand:

```bash
sudo ln -sfh /Applications/Crystl.app/Contents/Helpers/crystl /usr/local/bin/crystl
```

The CLI automatically reads the auth token from `~/.crystl-bridge-token`. No configuration needed.

**Requires Crystl.app to be running.** The CLI talks to crystl's local HTTP bridge.

### For agents running inside crystl

When the **tell agents about the crystl CLI** toggle is on (default), crystl upserts a short `Crystl CLI` section into each gem's `CLAUDE.md` and `AGENTS.md` as the project is opened, so any agent in a crystl shard discovers it can coordinate with sibling gems and shards. The section is bracketed with `<!-- crystl-cli:begin -->` / `<!-- crystl-cli:end -->` so re-running is idempotent and removal is one delete. If a project has no agent instruction file yet, crystl can seed `AGENTS.md` and a tiny `CLAUDE.md` import entrypoint unless that creation toggle is disabled.

## Commands

### status

Overview of all gems, shards, and running agents. This is the default command. A worker parked on an in-terminal approval prompt shows `⏸ awaiting input` next to its gem, so a stalled fan-out is visible at a glance instead of looking idle.

Each running agent is annotated with how long it has been in its current state and when it last produced output (e.g. `idle 5s · last output 2s ago`). crystl computes these durations itself so the numbers are consistent whether you read them here, on a paired phone, or off the `agent_status_changed` event — agents themselves are weak at time math, so the app owns the clock rather than asking the agent.

```bash
crystl status
crystl status --json
```

### gems

List all open gems with their shards and agent status.

```bash
crystl gems
crystl gems --json
```

### open

Open a project directory as a new gem.

```bash
crystl open ~/Projects/myapp
crystl open .
```

### close

Close a gem by name or UUID. Quote names that contain spaces. Reopening a path that is already a gem focuses the existing one instead of stacking a duplicate.

```bash
crystl close myapp
crystl close "Competitor research"
```

### fs

Browse directories so you know what paths you can `open`. Defaults to your projects directory. Read-only.

```bash
crystl fs                  # list your projects directory
crystl fs ~/Code           # list a specific path
crystl fs --json
```

### shards

List shards in a gem. A shard parked on an in-terminal approval prompt (the agent's own "Do you want to proceed?" menu, which is not a `pending` request) is flagged `⏸ awaiting input`, so you can spot a fanned-out worker that is silently waiting on you. The same flag streams over `crystl events` as `agent_status_changed.awaitingInput`.

```bash
crystl shards
crystl shards --gem myapp
```

### send

Send text to a terminal shard. Like typing into the terminal.

```bash
crystl send "git status"
crystl send --gem myapp "claude 'fix the tests'"
crystl send --gem myapp --shard opal "ls -la"
```

### notify

Notify an orchestrator shard with an **attributed** worker ping. Unlike `send` — which injects raw text into the target's input line as if the human typed it — `notify` carries the *sending* shard's identity, so the lead's chat dock attributes the message to the worker while still waking the orchestrator agent. This is the push half of a fan-out: workers can stay silent and report only when they finish or get blocked, instead of the lead polling them.

```bash
crystl notify --gem myapp --shard diamond "DONE opal: refactor complete"
crystl notify --shard diamond "BLOCKED opal: needs the API key"
crystl notify --shard diamond --from opal "DONE: tests green"
```

The sender is auto-detected from the `CRYSTL_SHARD` env a Crystl shell exports; pass `--from` to override it. Like `send`, it's a control command (Guild-gated over the desktop CLI, free over a direct-LAN phone connection).

| Flag | Purpose |
|------|---------|
| `--shard <name>` | Target lead/orchestrator shard (defaults to the gem's first shard) |
| `--gem <name>` | Gem name or ID (defaults to the selected gem) |
| `--from <name>` | Sending shard name (defaults to `$CRYSTL_SHARD`) |

### copy

Hand the user something to copy. `crystl copy` surfaces text in the [copy bar](/features/copy-bar) — an editable strip docked at the bottom of the shard with a one-click **Copy** button — so they don't have to drag-select it out of wrapped terminal lines. Reach for it whenever your output contains something the user needs *out* of the conversation (a token, a URL, a generated snippet), and **especially for commands they'll run elsewhere**, where a hand-retyped flag or path is easy to get wrong. **Free on every tier.**

```bash
crystl copy "npx some-cli --flag value"          # text goes to the bar
some-command | crystl copy                        # or pipe it in
crystl copy "deploy step 1" --label "deploy"      # name the tab
crystl copy "second snippet"                       # appends a 2nd tab (up to 10)
crystl copy "fixed" --tab 1                         # overwrite tab 1 instead of appending
crystl copy --gem myapp --shard opal "..."         # target a specific shard
```

The bar holds up to **10 numbered tabs**, so an agent emitting several snippets can stage them all — a plain `copy` appends a tab, `--tab <N>` overwrites one. The user can edit the text before clicking Copy. The window grows taller to make room, so the terminal keeps its size.

| Flag | Purpose |
|------|---------|
| `--label <title>` | Name the tab's chip |
| `--tab <N>` | Overwrite tab N instead of appending |
| `--gem <name>` / `--shard <name>` | Target a specific shard (defaults to the selected one) |

### screen

Dump the terminal screen content. See what an agent is doing without switching tabs.

```bash
crystl screen
crystl screen --gem myapp
crystl screen --gem myapp --shard opal
```

### history

Print a shard's structured Claude transcript (past turns and tool calls) so an agent can recover context after a compaction or in a fresh shard. Claude Code shards only; see the [History Navigator](/docs/conversation-history) for the GUI.

Each turn is annotated with its **model, token usage, and duration**, and a **session summary** (total tokens, tool calls, models used, wall-clock active time) prints on top — useful for judging a worker's cost and context before acting on its output.

```bash
crystl history                          # active shard
crystl history --gem myapp --shard opal
crystl history --tools-only             # only tool calls and results
crystl history --limit 20               # last 20 rows
crystl history --json                   # full metadata: per-turn usage/model/stopReason/cwd/gitBranch
```

### pending

List pending permission requests.

```bash
crystl pending
```

### approve / deny

Approve or deny a pending permission request by ID.

```bash
crystl pending          # see request IDs
crystl approve 1        # approve request #1
crystl deny 2           # deny request #2
```

### askuser

List and answer the questions agents raise through `AskUserQuestion`, the same prompts the floating question panel shows, answerable from any shell. Listing is free; `answer` resolves the prompt, so it's Guild-gated.

```bash
crystl askuser                       # list open questions (default subcommand)
crystl askuser list --json
crystl askuser answer <id> "2 weeks" # answer by text, or by an option label
```

For a multi-question prompt, the answer applies to every question in the series.

### shard create

Create a new shard inside an existing gem. Useful for orchestrator agents that want to fan work out into parallel shards.

```bash
crystl shard create --gem myapp                                   # shared cwd
crystl shard create --gem myapp --isolated                        # git worktree
crystl shard create --gem myapp --crystal-name opal               # pick the name
crystl shard create --gem myapp --prompt "add tests"              # launch an agent on a task
crystl shard create --gem myapp --agent codex --prompt "add tests"  # spawn a different agent
crystl shard create --gem myapp -c "npm test"                     # run a raw shell command
crystl shard create --gem myapp --hero wizard --isolated          # summon a solo hero
crystl shard create --gem myapp --hero seeker --prompt "audit auth" # hero with an opening task
```

To spawn a worker on a task, use `--prompt` (and optionally `--agent`/`--model`). crystl launches the agent and sends the task as its first message. This is **agent-neutral**: a Claude orchestrator can spawn a Codex worker, or vice versa. Reach for `-c` only when you want a raw shell command rather than an agent; the raw command path is mutually exclusive with agent and hero launches.

| Flag | Purpose |
|------|---------|
| `--isolated` | Back the shard with a git worktree so it can't clobber sibling shards |
| `--crystal-name <name>` | Pick the shard's crystal name (auto-assigned otherwise) |
| `--prompt, --task <text>` | Task sent to the launched agent as its first message |
| `--agent <name>` | Agent to launch: `claude`, `codex`, or `antigravity` (the sunset `gemini` still resolves for back-compat). Omit to auto-resolve (see below) |
| `--model <id>` | Pin the agent's model/version (e.g. `gpt-5.1-codex-max`) |
| `-c, --command <cmd>` | Run a raw shell command once the shard starts (mutually exclusive with `--agent`, `--prompt`, and `--hero`) |
| `--approval <mode>` | Pre-set the shard's approval mode: `manual`, `smart`, `all`, or `default` |
| `--quest` | Install the fanout comms channel so a worker can report back over `quest_msg` (see [Fanout](/docs/fanout)) |
| `--hero <name>` | Summon a solo [hero shard](/docs/hero-shards) by catalog name (e.g. `wizard`) — applies its agent, model, and persona. May pair with `--prompt`/`--task` for the hero's opening task and `--model` for a per-summon model override; mutually exclusive with `--agent` and `-c`. Requires Quest enabled. See `hero` below for discovery |
| `--json` | Emit the created shard as JSON |

**Agent resolution.** When `--agent` is omitted, crystl resolves the spawned agent in order: the gem's per-gem **Default Agent** (set in gem settings) → the agent already detected running in the gem → the global **Default Agent** (Settings → General, a typed `agent [model]` field like `codex gpt-5.1-codex-max`). If none of those resolve, the command errors rather than assuming an agent: pass `--agent`, run an agent in the gem first, or set a default.

### shard close

Close a shard in a gem by name. This is a mutation, so it's Guild-gated like `shard create`, `merge`, and `send`.

```bash
crystl shard close --gem myapp --shard opal
crystl shard close --shard opal              # --gem defaults to the selected gem
crystl shard close --gem myapp --shard opal --json
```

Closing an **isolated** shard is non-destructive to committed work: crystl removes the git worktree but **preserves** any commits or uncommitted changes on the shard's branch, so you can reopen the branch later from the [Isolation panel](/docs/isolated-sessions). It refuses to close the **last remaining shard** in a gem, since a gem always needs at least one. Eligible local shards are also recorded in the resurrect list before they close; remote shards and Quest party members are skipped.

### shard rename

Give a shard a short **display label** — a friendly name shown on its pill, in the window title, and in `crystl shards` / `status` (and on the phone). Useful for labelling the workers you fan out (`parser`, `tests`, `api`). This is a mutation, so it's Guild-gated like `shard create`.

```bash
crystl shard rename --gem myapp --shard opal --name "auth-refactor"   # set a label
crystl shard rename --shard auth-refactor --name "backend"            # address by the current label
crystl shard rename --gem myapp --shard opal --name ""                # clear it → back to the crystal name
```

The label is **display-only** — it never changes the shard's crystal name, git branch, or worktree, so a rename can't break identity or addressing. It's normalized to a short canonical form (lowercased, ≤14 characters, control characters stripped) so it reads the same everywhere it's shown or typed. After renaming, you can address the shard by its new **label**, its **crystal name**, or its **id** — all case-insensitive; `--shard` matches identity (name/id) first, then a label, and errors if a label is ambiguous. An empty `--name` clears the label.

Rename is **refused** for shards whose name isn't yours to change: [Quest](/docs/quest-coordination) party members (their name comes from the hero/role) and [side-quest](/docs/quest-coordination) participants (the name is the live message routing key). Rename a solo hero instead, or edit the hero in **Settings → Heroes**.

### resurrect

Undo-close for recently closed **local** shards — bring back a shard you just closed in a gem. `resurrect list` is read-only and free; the `resurrect` action itself is a mutation, so it's Guild-gated like `shard create`, `merge`, and `send`.

```bash
crystl resurrect list --gem myapp              # what can be brought back
crystl resurrect list --gem myapp --json
crystl resurrect --gem myapp                   # bring back the most recently closed shard
crystl resurrect --gem myapp --shard opal      # bring back a specific one by name
crystl resurrect --gem myapp --shard opal --json
```

Without `--shard`, `resurrect` restores the **most recent** entry. It restores the shard's name and color, and makes a best-effort attempt to restore its isolation (git worktree) and agent session. **Quest shards are excluded** — they aren't tracked for resurrection.

| Command | Free? | Purpose |
|---------|-------|---------|
| `resurrect list [--gem <name\|id>]` | Free | List recently closed shards that can be brought back |
| `resurrect [--gem <name\|id>] [--shard <name>]` | Guild | Bring back a closed shard (most recent if `--shard` omitted) |

### merge

Merge an isolated shard's git worktree branch back into the gem's main branch. This is the integration step [`/fanout`](/docs/fanout) runs when a worker finishes. It routes through crystl's own merge machinery, so worker agents never merge their own branches.

```bash
crystl merge --gem myapp --shard opal
crystl merge --gem myapp --shard opal --json
```

Returns a structured result: `success` (with a commit count), `nothingToMerge`, `dirty`, `conflicts`, `failed`, or `notIsolated`. Conflicts and dirty trees are left for you to resolve in the Isolation panel; `merge` never forces.

### hero

Discover and summon solo [hero shards](/docs/hero-shards) — a single catalog specialist (its own name, role, persona, agent, and model) started outside a Quest party. `list` is free; `summon` is a mutation, so it's Guild-gated, and (like the desktop swords button) it requires Quest enabled.

```bash
crystl hero list                                   # built-in catalog + this gem's local heroes
crystl hero list --gem myapp                        # local heroes for a specific gem
crystl hero list --json
crystl hero summon wizard                           # summon into the selected gem
crystl hero summon wizard --gem myapp --isolated    # in a git worktree
crystl hero summon seeker --gem myapp --prompt "review the auth diff"
crystl hero summon ranger --gem myapp --model gpt-5.1-codex-max
```

`hero list` shows the names you can pass to `summon` (built-ins like `wizard`, `warrior`, `ranger`, plus any [project-local heroes](/docs/local-heroes) in `.crystl/heroes/`). Party-only roles such as `healer` are excluded from solo summon. `crystl hero summon <name>` is a focused alias for `crystl shard create --hero <name>` — same behaviour, same gating. Add `--prompt` / `--task` to give the hero its opening assignment immediately, and `--model` to override the hero's configured model for that summon only.

If you've customized a built-in hero in Settings (model, persona, stats, agent), solo summon uses your edited version — the same customizations a Quest party would apply. Project-local heroes are taken as-is from their `.crystl/heroes/` definition.

| Command | Free? | Purpose |
|---------|-------|---------|
| `hero list [--gem <name>]` | Free | List summonable heroes (name · role · agent) |
| `hero summon <name> [--gem <name>] [--isolated] [--prompt <task>] [--model <id>]` | Guild | Summon a solo hero shard |

### gauntlet

Launch a release-readiness crew as solo hero shards: two Seekers, one Monk, and one Scribe. The default mode is review-only and isolated, so each hero gets its own git worktree and reports findings without editing files. Pass `--fix` when you want the isolated crew to make focused fixes.

```bash
crystl gauntlet "final release readiness"
crystl gauntlet --check "swift test" --check "npm test"
crystl gauntlet --fix --prompt "stabilize the xterm release"
crystl gauntlet --profile release --dry-run --json
```

The Scribe is instructed to watch the other gauntlet shards, consolidate a PASS/WARN/FAIL/BLOCKED verdict, and add concrete follow-ups to the [Workbench](/features/workbench) when useful.

| Flag | Purpose |
|------|---------|
| `--gem <name>` | Target gem (defaults to the selected one) |
| `--prompt, --task <text>` | Goal or scope for this gauntlet |
| `--check <cmd>` | Check command to ask the Monk to run; repeat for multiple checks |
| `--fix` | Allow isolated shards to make focused fixes instead of only reporting findings |
| `--shared` | Use shared shards instead of isolated worktrees |
| `--profile <name>` | Load defaults from `.crystl/gauntlet.json` |
| `--dry-run` | Print the planned crew and prompts without creating shards |

Optional project profiles live in `.crystl/gauntlet.json`:

```json
{
  "profiles": {
    "release": {
      "prompt": "Final release readiness pass",
      "checks": ["swift test", "npm test"],
      "party": ["seeker", "seeker", "monk", "scribe"]
    }
  }
}
```

### party

Manage [Quest](/features/crystl-quest) parties — a named roster of heroes you launch together. Build a party here, then launch it with `crystl quest start --party <name>`. `list` is free; `create` and `delete` are mutations, so they're Guild-gated.

```bash
crystl party list                                       # parties available to the selected gem
crystl party list --gem myapp --json
crystl party create "Design & Dev" --heroes wizard,ranger,warrior
crystl party create "Ship It" --heroes wizard,ranger --gem myapp --local
crystl party delete "Design & Dev" --gem myapp
```

`party create` seats the named heroes by identity (precedence **project-local > custom > built-in**, the same resolution a Quest party uses). Pass `--local` to commit the party to `<gem>/.crystl/parties/*.json` so it ships with the repo — everyone who clones the gem gets it. Without `--local` the party is global to your machine.

| Command | Free? | Purpose |
|---------|-------|---------|
| `party list [--gem <name>]` | Free | List parties available to a gem |
| `party create <name> --heroes <a,b,c> [--gem <name>] [--local]` | Guild | Seat a new party (`--local` commits it to the repo) |
| `party delete <name> [--gem <name>]` | Guild | Remove a party |

### quest

Launch, clear, or end a [crystl Quest](/features/crystl-quest) party from the command line. `quest start` is the CLI parity for the desktop "begin quest" panel: it summons the gem's party so a fleet of role-played agents start working in a shared chat room. `quest clear` archives the chat. `quest end` stops the active quest and turns heroes back into normal solo shards. These are mutations, so they're Guild-gated.

```bash
crystl quest start                              # launch the gem's active party
crystl quest start --gem myapp --party "Design & Dev"
crystl quest start --sealed                     # each hero gets its own git worktree
crystl quest start --fresh                      # archive prior chat, then launch
crystl quest clear                              # archive the gem's quest chat history
crystl quest clear --gem myapp
crystl quest end --gem myapp
```

| Flag | Purpose |
|------|---------|
| `--gem <name>` | Target gem (defaults to the selected one) |
| `--party <name>` | Which party to summon (defaults to the gem's active/first party) |
| `--sealed` | Sealed quest, where each hero runs in its own git worktree |
| `--fresh` | Archive any prior quest chat before launching |

`quest clear` archives the chat aside (recoverably) and leaves the root quest docs (`QUEST-LOG.md`, `DECISIONS.md`, `HANDOFF.md`) untouched. `quest end` ends the active quest without deleting the shards.

### sidequest

CLI parity for a [side quest](/docs/side-quest): bridge two shards you already have open into an ad-hoc two-agent chat, without summoning a party. `start` pairs two shards by name; `status` shows the active pairing; `end` tears it down. The two shards can live in different gems and even on different hosts. `start`/`end` are mutations (Guild-gated over the desktop CLI); `status` is free.

```bash
crystl sidequest status                              # show the active pairing (or "none")
crystl sidequest start --gem myapp --a diamond --b jade         # same-gem
crystl sidequest start --a crystl-web/garnet --b crystl/citrine # fully cross-gem
crystl sidequest start --gem crystl-web --a garnet --b crystl/diamond  # --gem as default scope
crystl sidequest end                                 # end the active side quest
crystl sidequest end --gem myapp
```

`--gem` is optional. It sets a default scope for bare shard names, while `--a` and `--b` each accept a qualified `<gem>/<shard>` form so an endpoint can name its own gem, that is how a side quest pairs shards across gems. A bare shard name with no `--gem` resolves globally across every open gem, and must be unique or crystl returns an `ambiguous; pass gem` error.

| Flag | Purpose |
|------|---------|
| `--gem <name>` | Default gem scope (name or ID) for bare shard names. Optional; endpoints can override it with a qualified `<gem>/<shard>` |
| `--a <shard>` | First shard to pair, bare or `<gem>/<shard>` (`start` only) |
| `--b <shard>` | Second shard to pair, bare or `<gem>/<shard>` (`start` only) |

### events

Stream live bridge events as JSON lines to stdout. One event per line, pipe-friendly:

```bash
crystl events                              # every type
crystl events --type pending_changed       # filter
crystl events --type pending_changed,askuser_changed,notification,agent_status_changed,backlog_changed --limit 5
```

Frame envelope: `{"type": "<event>", "data": <payload>}`. Common event types include `pending_changed`, `askuser_changed`, `notification`, `agent_status_changed`, `backlog_changed`, `quest_changed`, and `shard_update`. Ctrl-C exits cleanly.

### wait

Block until a condition is met, then exit. Built on `events`, so agents can coordinate without polling.

```bash
crystl wait pending                  # block until a permission request exists
crystl wait pending --timeout 30     # give up after 30s (exits with status 1)
crystl wait pending --json           # emit the matching request as JSON
crystl wait askuser --timeout 60     # block until an AskUserQuestion opens
crystl wait notification --type Stop # block until a matching hook notification arrives
crystl wait awaiting --gem myapp     # block until a worker parks on an in-terminal prompt
crystl wait idle --shard opal        # block until a worker finishes a turn
```

`wait pending` and `wait askuser` have a fast path: if a matching item already exists when the command starts, it prints and exits without subscribing. `wait notification` listens for fresh notifications. `wait awaiting` catches in-terminal prompts that are not structured permission requests; `wait idle` exits on a working-to-idle edge.

### token

Print the current bearer token for debugging.

```bash
crystl token
```

### facet

Manage [facet inserts](/docs/facet-inserts), the snippet library behind the facet bar, from the CLI. `list` is free; `add`, `slot`, and `remove` change the library, so they're Guild-gated.

```bash
crystl facet list                                  # show the library (ids, labels, slots)
crystl facet list --json
crystl facet add "review" "review this branch for bugs"
crystl facet add "deploy" "npm run deploy" --kind command --slot 1
crystl facet slot <id> 2                           # bind an existing insert to bar slot 2
crystl facet remove <id>
```

| `facet add` flag | Purpose |
|------|---------|
| `--scope <all\|shared\|isolated>` | Which shards the insert is offered in (default: all) |
| `--action <insert\|submit>` | Insert the text, or insert-and-submit (default: submit) |
| `--kind <prompt\|command>` | Categorize the insert (default: prompt) |
| `--slot <1\|2\|3>` | Also bind it to a facet-bar slot |
| `--json` | Emit the created insert as JSON |

### workbench

Show or hide the [Workbench](/docs/task-backlog) panel, or read and edit the `WORKBENCH.md` / `BACKLOG.md` task list from the CLI. Panel controls and edits are mutations, so they're Guild-gated. `workbench list` is read-only and free.

```bash
crystl workbench               # slide the panel into view (default subcommand)
crystl workbench open --gem myapp
crystl workbench close
crystl workbench toggle        # flip open/closed
crystl workbench list --gem myapp
crystl workbench add "fix login redirect" --section "Next"
crystl workbench start 12      # mark task 12 [~]
crystl workbench check 12      # mark task 12 [x]
crystl workbench uncheck 12    # mark task 12 [ ]
crystl workbench cycle 12      # todo -> in progress -> done -> todo
crystl workbench delete 12
crystl workbench section "Later"
```

`crystl backlog …` is kept as an alias (the panel was renamed from "backlog"), so older scripts and habits keep working. Task indexes come from `crystl workbench list`.

### docs

Search and read crystl's documentation from the terminal. Handy for an agent answering "how do I X?" or "is this a bug?" without bloating its context. Pulls the docs index live from crystl.dev (cached locally for 6h). Read-only.

```bash
crystl docs                       # list every doc page
crystl docs notifications         # exact page id → prints the full page
crystl docs "isolated shards"     # otherwise searches; prints ranked matches
crystl docs isolated --full       # print full bodies of all matches
crystl docs --fresh               # bypass the cache and refetch
```

### doctor

Check local CLI, bridge, and hook wiring. Useful before debugging approval/notification issues.

```bash
crystl doctor
crystl doctor --json
```

### render

Render terminal bytes offline through crystl's terminal grid. This does not require a running bridge; it's for debugging terminal parser/rendering behavior.

```bash
crystl render --feed transcript.bin --cols 120 --rows 40
crystl render --feed transcript.bin --dump-cells
crystl render --feed transcript.bin --json
```

### ssh

Configure the direct bridge address used by SSH sessions when the automatic tunnel is not enough.

```bash
crystl ssh bridge-address 100.64.0.1:19280
crystl ssh bridge-address ""              # clear override
```

### report

Send a bug report, idea, or praise to the crystl team. Free on every tier. Write the report in plain language and **never** include terminal output, file paths, or secrets.

```bash
crystl report bug "approval panel hangs after Ctrl-C"
crystl report idea "let me pin a shard to the top"
crystl report praise "the rail is gorgeous"
crystl report other "..." --email me@example.com    # optional reply address
```

## Scripting examples

### Monitor an agent

```bash
# What is the agent doing right now?
crystl screen --gem myapp

# Poll every 5 seconds
watch -n5 crystl screen --gem myapp
```

### Approve from another terminal

```bash
crystl pending        # see what's waiting
crystl approve 1      # approve it
```

### Launch parallel agents

```bash
crystl open ~/Projects/frontend
crystl open ~/Projects/backend
crystl send --gem frontend "claude 'update the API client'"
crystl send --gem backend "claude 'add the new endpoint'"
```

### JSON for scripting

```bash
crystl gems --json | jq '.gems[].name'
crystl pending --json | jq '.pending | length'
```

## HTTP API

The CLI wraps a local HTTP API at `http://127.0.0.1:19280/api/v1/`. You can call it directly with curl:

```bash
TOKEN=$(head -1 ~/.crystl-bridge-token)

# List gems
curl -s -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19280/api/v1/gems | jq

# Send a command
GEM_ID="..."  # from gems response
SHARD_ID="..."  # from gems response
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"ls\r"}' \
  "http://127.0.0.1:19280/api/v1/gems/$GEM_ID/shards/$SHARD_ID/send"

# Subscribe to real-time events (SSE)
curl -N -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19280/api/v1/events
```

### Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/v1/gems` | GET | List open gems |
| `/api/v1/gems` | POST | Open a gem |
| `/api/v1/gems/:id` | GET | Get gem details |
| `/api/v1/gems/:id` | DELETE | Close a gem |
| `/api/v1/gems/:id/shards` | POST | Create a shard (incl. `hero`) |
| `/api/v1/gems/:id/shards/:sid` | GET | Get shard details |
| `/api/v1/gems/:id/shards/:sid` | DELETE | Close a shard |
| `/api/v1/heroes` | GET | List summonable heroes (`?gem=` for local) |
| `/api/v1/gems/:id/shards/:sid/send` | POST | Send text to terminal |
| `/api/v1/gems/:id/shards/:sid/notify` | POST | Send an attributed worker notification |
| `/api/v1/gems/:id/shards/:sid/rename` | POST | Set/clear the shard's display label |
| `/api/v1/gems/:id/shards/:sid/copy` | POST | Push text to the copy bar (free) |
| `/api/v1/gems/:id/shards/:sid/screen` | GET | Get screen content |
| `/api/v1/gems/:id/shards/:sid/merge` | POST | Merge an isolated shard's branch |
| `/api/v1/gems/:id/shards/:sid/transcript` | GET | Get structured shard history |
| `/api/v1/gems/:id/resurrect` | GET | List resurrectable closed shards → `{ gem_id, entries }` (free) |
| `/api/v1/gems/:id/resurrect` | POST | Resurrect a closed shard (optional body `{ "crystalName": "opal" }`) → `ShardResponse` |
| `/api/v1/pending` | GET | List pending permissions |
| `/api/v1/pending/:id/decide` | POST | Approve or deny |
| `/api/v1/history` | GET | Recent decisions |
| `/api/v1/notifications` | GET | Recent notifications |
| `/api/v1/events` | GET | SSE event stream |
| `/api/v1/askuser` | GET | List open agent questions |
| `/api/v1/askuser/:id/answer` | POST | Answer an agent question |
| `/api/v1/gems/:id/quest/start` | POST | Launch a quest party |
| `/api/v1/gems/:id/quest/clear` | POST | Archive the quest chat |
| `/api/v1/gems/:id/quest/end` | POST | End the active quest |
| `/api/v1/gems/:id/quest` | GET | Quest state (active, messages, heroes) |
| `/api/v1/gems/:id/quest/message` | POST | Post a message into the quest bus |
| `/api/v1/parties` | GET/POST | List or create parties |
| `/api/v1/parties/:id` | DELETE | Delete a party |
| `/api/v1/gems/:id/backlog` | GET | Read the workbench/backlog |
| `/api/v1/gems/:id/backlog` | POST | Open, close, or toggle the workbench panel |
| `/api/v1/gems/:id/backlog/toggle` | POST | Toggle a task checkbox |
| `/api/v1/gems/:id/backlog/cycle` | POST | Cycle a task through todo / in progress / done |
| `/api/v1/gems/:id/backlog/task` | POST | Add a task |
| `/api/v1/gems/:id/backlog/task/:index` | POST/DELETE | Edit or delete a task |
| `/api/v1/gems/:id/backlog/section` | POST | Add a section |
| `/api/v1/gems/:id/backlog/section/:index` | POST/DELETE | Rename or delete a section |
| `/api/v1/facets` | GET/POST | List or add facet inserts |
| `/api/v1/settings/ssh` | POST | Set or clear the SSH bridge address |
| `/api/v1/feedback` | POST | File a bug / idea (free on every tier) |

All endpoints require `Authorization: Bearer <token>` header.

## Security

- **Auth:** Every request requires a bearer token read from `~/.crystl-bridge-token` (0600 permissions, regenerated each launch)
- **Remote mutation gate:** Local loopback callers can use the CLI directly. Non-loopback callers can mutate terminals only when remote access is enabled; otherwise control routes return 403.
- **Rate limited:** 100 requests per second per client IP

---
Source: https://crystl.dev/docs/cli/
