Files
HomerandClaude Opus 5.5 f9bad01ed7
ci / check (push) Waiting to run
LLeMbas CLI 1.0.0
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM
API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a
link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64
and arm64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:59:03 +00:00

468 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The `_lembas` extension of ACP
How LLeMbas (the web UI) and LLeMbas CLI (on a device) talk over the link: the Agent Client
Protocol, plus the `_lembas/*` methods and `_meta.lembas` fields below. LLeMbas CLI is the
**agent**; LLeMbas is the **client**. The device dials out (`wss://<instance>/api/devices/link`,
the device token in the `Authorization` header) and one WebSocket message is one JSON-RPC 2.0
message; `lembas serve --stdio` speaks the same protocol on stdin/stdout, one message a line, to an
editor (which gets plain ACP: none of the `_lembas` notifications, no question or plan cards).
This file is part of the harness spec: LLeMbas vendors it with the rest, and a
change to the protocol is a change here first, then in both projects.
## Versions and discovery
- **`protocol`** (`_meta.lembas.protocol` in `initialize`, `protocol` in `_lembas/hello`):
**2**. CLI `src/acp/agent.ts:LEMBAS_PROTOCOL`, LLeMbas
`api/devices.py:PROTOCOL`.
- **Discovery** (`/.well-known/lembas.json`) says `protocol: 1` **and** `protocols: [1, 2]` —
`protocol` stays 1 on purpose, because a client that compares it for equality at login must
still get in. The CLI reads `protocols` (falling back to `protocol` where it is absent) at
login and again **every time the link dials**, keeps it with the login, and says protocol 2 in
`hello` and `initialize` only to an instance that lists 2; to any other it says 1 (an instance that
speaks only 1 closes a link whose hello says 2 with **4400**). A link closed with 4400 stops — it is not
redialled — saying *the instance and this CLI speak different link protocols — update one of them*.
- **A protocol 1 peer keeps working.** Every feature beyond protocol 1 is named by a **capability
string** in `initialize`'s `_meta.lembas.capabilities`, and **LLeMbas never calls a method, nor
relies on a field, the device did not list**. The CLI logs in to an instance whose discovery says
protocol 1 or 2.
- A client says it is LLeMbas with `clientCapabilities._meta.lembas` in `initialize`, and which
protocol it speaks with `_meta.lembas.protocol` (absent: 1 — a protocol 1 client sends `lembas: true`).
Only such a client gets `_lembas/event`; only one that says protocol **2 or later** gets
`_lembas/question` and `_lembas/plan`, and one that then answers them with method-not-found is
treated as protocol 1 for the rest of the link.
| Capability | What it promises |
|---|---|
| `directories` | `_lembas/directories` |
| `configure` | `_lembas/configure` |
| `steer` | `_lembas/steer`; `unsent` in a prompt's answer |
| `terminal` | `_lembas/terminal/*` (only with `remote.terminal: true`) |
| `sessions` | a session id lasts (opened from the store again); terminal sessions announced (`announce`, `left`, `history`) |
| `status` | `_lembas/session/status` |
| `delete` | `_lembas/session/delete`; `_lembas/session/deleted` |
| `compact` | `_lembas/session/compact` |
| `title` | `_lembas/session/title` |
| `files` | `_lembas/files/search`; `@path` in a prompt expanded |
| `commands` | `available_commands_update`, `_lembas/commands`; `/name` in a prompt expanded |
| `attachments` | `image` and `resource` blocks in `session/prompt`; `attachments` on prompt events and history |
| `ask` | `_lembas/question` |
| `plan` | `_lembas/plan` |
| `model` | the `model` event; `_meta.lembas.connection` in `initialize` |
| `turns` | `_meta.lembas.turnId` on `session/prompt`, deduplicated; `turnId` on `prompt`/`task` events; turn fields in `status` |
| `shell` | `shell` and `integration` in `_lembas/terminal/open`'s answer (listed only with `terminal`) |
## Standard ACP, as used here
### `initialize` — client → agent
Params: `{ protocolVersion: 1, clientCapabilities: { _meta?: { lembas?: { protocol: 2 } | true } } }`.
Result:
```json
{
"protocolVersion": 1,
"agentCapabilities": {
"loadSession": false,
"promptCapabilities": { "image": true, "audio": false, "embeddedContext": true },
"mcpCapabilities": { "http": false, "sse": false }
},
"authMethods": [],
"agentInfo": { "name": "LLeMbas CLI", "version": "1.0.0" },
"_meta": { "lembas": {
"protocol": 2,
"harness_spec": "1.0.0",
"limits": { "roots": ["~/work"], "max_mode": "edit" },
"connection": "example",
"capabilities": ["directories", "configure", "…"]
} }
}
```
`limits` is `null` on `serve --stdio`. `connection` is the device's name for the instance — the
login's connection — or `null`; an instance that does not speak protocol 2 recognises its own
models by it, as `<connection>/<name>`, while the `model` event and `announce` say it directly
(`instance`).
**How the device names an instance's models.** An instance that speaks protocol 2 serves every model
as `<provider>/<model>` (`deepseek/deepseek-flash`), and that served id **is the device's ref** for
it — each provider is, as the person sees it, a connection of its own, backed by the instance
(requests still go to its `/v1`, with the served id). Exceptions: a connection of the device's own
with the provider's name keeps the short form, and the instance's model is then
`<login>/<provider>/<model>`; with two instances serving one provider, the one logged in to first
keeps the short form and the other's are `<login>/<provider>/<model>`; an instance that does not
speak protocol 2 serves bare ids, which stay `<login>/<id>`. The provider is the model's `provider` field where the
instance gives one, else — only from an instance whose discovery lists protocol 2 — the part of the
id before its first `/`; an id with a `/` from any other instance is just an id (`<login>/<id>`). The old forms (`<login>/<model>`,
`<login>/<provider>/<model>`) still name the same model. `lembas_model` in `session/new` and
`model` in `_lembas/configure` are the instance's served name, as it serves it.
### `session/new` — client → agent
Params: `{ cwd: <absolute>, mcpServers?: [], _meta?: { mode?, lembas_model?, model?, effort? } }`.
`cwd` must be a directory inside `remote.roots`, in a trusted project when `remote.require_trust`
(the default) — otherwise `-32602` with the reason. `mode` is lowered to `remote.max_mode`.
`lembas_model` is the instance's name of a model (it becomes `<connection>/<name>` here); `model`
a ref of the device's own; `effort` a level or `"off"`.
Result: `{ sessionId: "ses_…", modes: { currentModeId, availableModes }, _meta: { lembas: { model, mode, effort } } }`.
The id is the device's store id and lasts: a session the client names later that is not in memory
is opened from the store under the same rules (capability `sessions`). With capability `commands`,
an `available_commands_update` follows the answer.
### `session/prompt` — client → agent
Params: `{ sessionId, prompt: ContentBlock[], _meta?: { lembas?: { turnId?: string } } }`.
Blocks (capability `attachments` for the last two kinds):
| Block | Becomes |
|---|---|
| `{ type: "text", text }` | the prompt's text (blocks joined by a blank line) |
| `{ type: "resource_link", uri }` | `(see <uri>)` in the text |
| `{ type: "image", data: <base64>, mimeType, uri? }` | written under the session's attachment directory and attached by path, as the TUI does with a pasted image: an image part for a vision model, a line saying it is there for any other. `mimeType`: `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
| `{ type: "resource", resource: { uri, mimeType?, text } }` | written there and attached like an `@file` (line-numbered, counted as read) |
| `{ type: "resource", resource: { uri, mimeType?, blob: <base64> } }` | written there and referenced by path (`(binary file, N bytes)`), or attached as an image when it is one |
Limits: 10 MB a block and 25 MB a prompt, decoded; over either, `-32602` with a sentence. The
attachment directory is `~/.local/state/lembas/attachments/<sessionId>/`, removed with the session;
each upload gets a name of its own there (`<time>-<random>-<name>`), so two of one name stay two.
A command's `mode` in a session shared under the limits is lowered to `remote.max_mode`, as it is in
a session the service runs; a command's `model` holds for its prompt, and the model and effort the
session had come back afterwards.
The text is then expanded **as the TUI would** (capabilities `files`, `commands`):
- A text starting with `/name`, where `name` is in the session's command set (below), is that
command: a prompt command sends its expanded text (shown in the transcript as typed); `compact`,
`undo` and `plan` without a task are actions and send nothing to the model. A `/name` not in the
set — a path, a regex, a TUI-only command — is sent to the model as plain text, `/` and all.
- **Writing a path:** a space or a `#` in a name is escaped with a backslash — `@my\ notes.md`,
`@a\#b.md` — which is how the TUI's picker writes it and how the expander reads it; an unescaped
`#` followed by digits is a line range (`@c.md#10-20`).
- `@path` mentions in what the person typed are attached by the TUI's expander, with its limits
(`project/attach.ts`): files line-numbered, `@path#10-20` a range, `@dir/` a listing, images by
vision. Never from a command's own text (a skill's, a custom command's body, a diff). **Only a
path that is — symlinks followed — inside the session's project and inside `remote.roots`, and is
not a protected path, is attached**; any other `@word` stays plain text and is not read. In a
terminal's shared session the same holds for a prompt from the web (the device's roots; only the
project when the session was shared by name and no roots are set).
Turn ids (capability `turns`): `turnId` is the client's id for this turn (LLeMbas: its user
message id). The device keeps, per session, the last 200 ids it accepted with their state
`queued | running | done` — the finished ones with their answers also in one file per session
(`~/.local/state/lembas/turns/<sessionId>.json`, written atomically and merged), which the service
and every terminal use alike, so they outlive a restart and a session moving between a terminal and
the service in either direction; files untouched for 30 days are pruned — and **a prompt with a known turnId does not run again**: it gets the
original's answer — when that one is done, or at once if it is. Without a `turnId` the device makes
one. A prompt that arrives while the session works waits its turn.
Result: `{ stopReason: "end_turn" | "max_turn_requests" | "max_tokens" | "cancelled", _meta: { lembas: { turnId, … } } }`,
where `_meta.lembas` may also carry:
| Field | When |
|---|---|
| `unsent: string[]` | messages sent with `_lembas/steer` that the turn ended before taking in — send them next |
| `command: string` | the prompt was that command |
| `summary: string` | `/compact` |
| `undone: { prompt, files } \| null` | `/undo` (null: nothing to undo) |
| `mode: string` | `/plan` without a task: the mode now |
| `message: string` | a command that had nothing to do (`/review` with no changes), said instead |
### `session/set_mode` — client → agent
Params: `{ sessionId, modeId }`, lowered to `remote.max_mode`. Result `{}`.
### `session/cancel` — client → agent (notification)
Params: `{ sessionId }`. Stops the running turn.
### `session/update` — agent → client (notification)
`{ sessionId, update }` with `sessionUpdate` one of `agent_message_chunk`, `agent_thought_chunk`,
`tool_call`, `tool_call_update`, `plan` (the todo list), `current_mode_update`, and
`available_commands_update` (capability `commands`):
```json
{ "sessionUpdate": "available_commands_update",
"availableCommands": [ { "name": "review", "description": "…", "input": { "hint": "head | <branch>" },
"_meta": { "lembas": { "kind": "builtin" } } } ] }
```
Sent after `session/new`, after a session is opened from the store or taken over by a new link,
when a terminal's session is shared, and after a turn whenever the set changed (a skill made, a
command file written). `kind`: `builtin`, `custom` (`.agent/commands`, `~/.config/lembas/commands`)
or `skill`. The set is the shared one only: the built-ins `compact`, `plan`, `undo`, `review`,
`changelog`, `init`, `continue`, then custom commands, then skills. Commands that need the TUI's
own screens (sessions, settings, theme, login, quit, …) are never listed; a custom command or skill
named like any TUI command is not a command (the built-in wins in the TUI).
### `session/request_permission` — agent → client
An approval card. Params: ACP's `{ sessionId, toolCall: { toolCallId, title, kind, status: "pending", rawInput, content? }, options }`
with options `once` (allow_once), `session` (allow_always, absent where "always" is never offered)
and `deny` (reject_once), and `_meta.lembas: { reason, purpose?, always?, patient? }`. `patient`: a
person may also answer at the terminal (a shared session), so the client does not time it out.
Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with
optional `_meta.lembas.command` (the corrected command line, `once` only) or
`_meta.lembas.feedback` (why it was denied). No answer within `remote.approval_timeout` is a denial.
## `_lembas/*` methods
Direction is who sends it. "→ result" marks a request; the others are notifications.
### `_lembas/hello` — agent → client
Sent once when the link opens, before `initialize`:
`{ protocol, version, harness_spec, device: { host, platform }, limits: { roots, max_mode, require_trust } }`.
### `_lembas/event` — agent → client
`{ sessionId, event }`: every engine event of the session, to a client that said `_meta.lembas`.
`event.type` is one of `session`, `prompt`, `step`, `title`, `text`, `reasoning`,
`tool_call_delta`, `progress`, `tool_start`, `tool_end`, `tool_denied`, `usage`, `inbox`,
`steered`, `task`, `notice`, `retract`, `todos`, `compacted`, `sub_tool`, `mode`, `model`, `flash`,
`setting`, `error`, `done` (`src/bus/index.ts`). Those this protocol relies on:
| Event | Fields |
|---|---|
| `prompt` | `text` (as the person sees it: a command's name, not its body), `turnId`, `attachments?: [{ name, mimeType, size }]` |
| `task` | `state: "start" \| "end"`, `turnId` — every turn has one: the client's, or one made here for a turn typed in a terminal |
| `steered` | `texts`, `ids?` — the `_lembas/steer` message ids taken in at this step |
| `mode` | `mode` |
| `model` | `ref` (the device's ref, above), `effort` (a level or `"off"`), `connection` (the device connection it is spoken to through) and **`instance: boolean`** — true exactly when that connection is the login of **this link's** instance, i.e. the model is one the instance serves (its served id is `ref`, or `ref` less a leading `<connection>/`). Whenever either changes, for any reason: `/model`, `/effort`, the settings tool, a fallback, `_lembas/configure` |
| `compacted` | `summary` |
| `title` | `id`, `title` |
### `_lembas/directories` — client → agent → result
Params `{ path?: string }`. Without a path: `{ path: "", places: Dir[] }`, the remote roots and every
trusted directory inside them. With one (absolute, inside the roots): `{ …Dir, parent, entries: Dir[], error? }`.
`Dir = { path, name, accepted: boolean, reason? }` — whether `session/new` would accept it.
Only on the link (an error on `serve --stdio`).
### `_lembas/configure` — client → agent → result
Params `{ sessionId, model?: <instance's name>, effort?: <level> | "off" }` between prompts. Result
`{ model: <ref>, effort }`. Emits a `model` event when something changed.
### `_lembas/steer` — client → agent → result
Params `{ sessionId, text, messageId? }`: a message for the running task, given to the model at its
next step. Result `{ queued: number }`. `-32602` when nothing runs: send it as a prompt instead.
### `_lembas/session/history` — client → agent → result
Params `{ sessionId }`. Result `{ sessionId, title, root, turns }`, each turn
`{ role: "user", text, turnId?, attachments?: [{ name, mimeType, size }] }` (`turnId`
the turn the message started — the client's, or the one made for a turn typed in a terminal — kept
with the message in the store; `attachments`, and the text without them) or `{ role: "assistant", text, reasoning, tools: [{ id, name, args, output?, isError? }] }`.
### `_lembas/session/status` — client → agent → result
Params `{ sessionId }`. Result
`{ sessionId, exists, held: "here" | "terminal" | "none", busy, title, turnId?, queued: string[], lastTurnId? }`
— the turn fields with capability `turns`: the turn running now, the ones waiting, the last
one finished (for a session not open, `held: "none"`, the last one it finished before). **Reattaching** after a restart or a dropped link, for an incomplete row with turn T:
T running → follow it; T queued → wait, and follow it when it starts; T done → import it from
`history`; T unknown → send the prompt again (safe: the device deduplicates).
### `_lembas/session/delete` — client → agent → result
Params `{ sessionId }`: the chat was deleted. The session stops if it works, leaves the store (its
attachments too), and a terminal that has it open starts a new one. Result `{ deleted: boolean }`;
`false` when there is no such session (any more), which is done as well.
Which sessions the device deletes: one the client holds
or a terminal shares now, one the client was ever given (made by `session/new`, opened from the
store, or shared from a terminal — the device keeps a mark), and otherwise one whose root is inside
`remote.roots` on a device whose remote work is on. **Trust is not asked**: deleting runs nothing in
the directory. Anything else is `-32602` with the reason. (Not the rule for *opening* a session,
trust included: that one would refuse sessions the client had been showing.)
The client keeps a delete it could not deliver — the device was not linked, did not answer, or
refused — and sends it again when the device next links, until it is answered (LLeMbas keeps it in its
database and gives up after five refusals). A delete is idempotent, so sending it twice
is safe. A session pending deletion is not made into a chat again by an `announce`.
### `_lembas/session/compact` — client → agent → result
Params `{ sessionId }`. Result `{ summary }`. `-32602` while it works.
### `_lembas/session/title` — client → agent → result
Params `{ sessionId, title }` (at most 200 characters). Result `{ title }`.
### `_lembas/session/announce` — agent → client
A terminal on the device shares its session:
`{ sessionId, cwd, title, model, instance, mode, effort, busy, origin: "terminal" }`. Sent when it is
shared and again whenever the link comes up. `instance`: the session's model is this link's
instance's, as on the `model` event.
### `_lembas/session/left` — agent → client
`{ sessionId, available: boolean }`: the terminal let go of it. `available`: the service can open
it from the store at the next prompt.
### `_lembas/session/deleted` — agent → client
`{ sessionId }`: deleted on the device (`/delete`, `/sessions delete`); delete the chat.
A delete is never dropped for want of a link. One made while no link is up — no
service running, the instance unreachable, or `lembas sessions delete` / `prune` from a shell — is
kept on the device, and every one kept is sent right after the next link's `initialize` (to a
client that said `_meta.lembas`), then forgotten. So the client must expect a `deleted` some time
after the fact, for a session it may never have had a chat for, or has deleted already: either is
ignored. Also sent for the empty session a terminal's resume discards (`-c`, `/sessions`) where the
web UI had been shown it.
### `_lembas/permission/settled` — agent → client
`{ sessionId, toolCallId }`: take the card with this `toolCallId` down — answered elsewhere (at the
terminal), or no longer waited for (the device's time ran out). It covers approvals,
questions and plans alike. Cards are keyed by `toolCallId`, never by session: several can be open
at once, and settling one never hides another.
### `_lembas/question` — agent → client → result (capability `ask`)
The `ask_user` tool, in a session the device holds or a terminal shares, asked of a client that said
`_meta.lembas`. Params:
```json
{ "sessionId": "ses_…", "toolCallId": "call_…", "why": "optional",
"questions": [ { "header": "DB", "question": "Which database?", "multiple": false,
"options": [ { "label": "SQLite", "description": "…", "recommended": true } ] } ],
"_meta": { "lembas": { "patient": true } } }
```
At most 4 questions of at most 6 options; the recommended option, if any, is first. An option's
`label` is at most 240 characters on the card (LLeMbas shows no more); answered with the cut label,
the model is given the whole one. Result
`{ outcome: "answered", answers: Answer[] }`, one per question in order, or `{ outcome: "dismissed" }`.
`Answer` is `{ kind: "options", labels: string[] }`, `{ kind: "custom", text }` or
`{ kind: "back", text }` (a question of the person's own, which the model answers before asking
again) or `{ kind: "skipped" }` (that question left blank; the model is told it has no answer).
Anything else reads as dismissed.
### `_lembas/plan` — agent → client → result (capability `plan`)
`plan_submit`, likewise. Params `{ sessionId, toolCallId, path, text, _meta: { lembas: { patient? } } }`
— `path` as the model sees it (relative to the project), `text` the plan's markdown. Result
`{ outcome: "approve", mode: "edit" | "manual" }` (the session continues in that mode, lowered to
`remote.max_mode`; where even `manual` is beyond it, nothing is decided),
`{ outcome: "revise", feedback }`, or `{ outcome: "dismissed" }`.
For both: **first answer wins.** In a terminal's shared session the card is shown at the terminal
and in the web UI at once (`patient: true`); whichever answers first decides, and the other card is
taken down (`_lembas/permission/settled` when the terminal answered). Not `patient`: LLeMbas times
the card out like an approval and answers `dismissed`; the device also stops waiting after
`remote.approval_timeout`, answers the model as dismissed, and sends `settled`. A client that did
not say `_meta.lembas` gets neither request: `ask_user` then tells the model nobody can be asked,
and `plan_submit` saves the plan and tells it to stop.
### `_lembas/files/search` — client → agent → result (capability `files`)
The TUI's `@` picker, for the web composer. Params
`{ sessionId?: string, cwd?: string, query: string, limit?: number }` — one of `sessionId` (the
session's directory) or `cwd` (absolute; the same rules as `session/new`); `limit` 20 by default, at
most 50. Answered from the device's own file index (`project/files.ts`: ripgrep's list, so
gitignored files are left out; fuzzy, nudged by frecency; an empty query gives the recent ones).
Result `{ root, files: [{ path, kind: "file" | "dir" }] }`, `path` relative to `root` and
`/`-separated. `root` is the directory `@path` mentions in that session's prompts are read from —
its working directory (for a terminal's session, the directory the terminal works in, which may be
below the project root the `announce` names), the project root for a session opened there. A chosen file goes into the
composer as `@path` text, which the device expands when the prompt comes.
### `_lembas/commands` — client → agent → result (capability `commands`)
Params `{ sessionId?: string, cwd?: string }`, for a composer that has a directory but no session
yet. Result `{ commands: [same shape as availableCommands] }`.
### `_lembas/terminal/open` — client → agent → result
Only with `remote.terminal: true`. Params `{ cwd?, cols?, rows? }` (`cwd` defaults to the first
root; 20–500 × 5–300). A login shell as the device's user, in a PTY, at most four per link. Result
`{ terminalId, cwd, cols, rows, shell: "bash" | "zsh" | "fish" | "other", integration: boolean }`
(`shell` and `integration` with capability `shell`).
**Shell integration**: for bash, zsh and fish, unless `remote.terminal_integration: false`
(global config), the shell starts with an rc wrapper that sources the user's own startup files first
— as a login shell reads them — and then marks every prompt with OSC 133 (`A` prompt start, `B`
command start, `C` output start, `D;<exit status>` when a command has finished; an empty line ends
no command) and OSC 7 (`file://<host><cwd>`, the path percent-encoded byte by byte). `B` is put back after any hook that
rebuilds the prompt (starship, powerline). bash runs as an interactive shell that reads the login
files itself (`/etc/profile` with PS1 unset, so `/etc/bash.bashrc` is read once), and keeps
`logout` and `~/.bash_logout`; zsh follows a `ZDOTDIR` set in the user's own `.zshenv`, and our
variables are gone before their `.zshrc` runs. Nothing else in the user's environment changes. The
web terminal reads the marks; nothing on the server parses them.
### `_lembas/terminal/input` — client → agent
`{ terminalId, data: <base64> }`: keystrokes.
### `_lembas/terminal/resize` — client → agent
`{ terminalId, cols, rows }`.
### `_lembas/terminal/close` — client → agent
`{ terminalId }`: hung up (SIGHUP). Every terminal closes with the link.
### `_lembas/terminal/output` — agent → client
`{ terminalId, data: <base64> }`.
### `_lembas/terminal/exit` — agent → client
`{ terminalId, code }`: the shell ended.
## The HTTP API the CLI uses (LLeMbas `/v1`, the device token's own user)
Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`:
- **`GET /v1/models`**: each entry may carry **`kind`** — `"chat"`, `"embedding"`, `"stt"`,
`"tts"` or `"image"`. Only `chat` (or no `kind`, from an instance that lists chat models only) is
a model the CLI talks to: its model refs, `/model` and the login's list are those. An `embedding`
model is the library's: logging in (or in again) sets config.yaml's `embedding:` to the first one
as `<connection>/<served id>` when no `embedding:` is set, and never replaces one that is;
`embedding: <served id>` also finds it. Voice uses the instance's own speech endpoints, not these
entries. Any other kind is ignored.
- **`GET /v1/usage`** →
`{ plan: { name, credits_per_month } | null, balance: number | null, month: { from, tokens_in, tokens_out, cost: number | null }, device: { tokens_in, tokens_out, cost: number | null }, unit: "credits" }`.
The CLI shows the balance in its status bar when `plan` is not null, and the whole of it under
`/usage`. A 404 (an instance without it) shows nothing.
Also read, every one optional (an instance without them gets credits alone):
`currency` (`"EUR"` | `"USD"` | `"GBP"`), `credit_value` (money per credit), `admin` (boolean:
no limit, never refused), `balance_money`, `month.cost_money` and `device.cost_money` (numbers or
`null`), and `windows: [{ window, limit, spent, frees_at }]` (rolling spending windows: `window`
names it, `limit` and `spent` in credits, `frees_at` an ISO time or `null`). Credits are shown
with up to two decimals below 1,000 and whole above; beside each, its money in `currency` where
given (`€1.23`, `$1.23`, `£1.23`) — `*_money` when sent, else the credits times `credit_value`.
`admin: true` shows `unlimited` in the status bar whatever the plan, and `/usage` says so, still
listing the month's and the device's spend; its windows are not listed. Otherwise `/usage` lists
each window's spend, limit and when it frees.
- **`GET /v1/me/personalization`** →
`{ enabled, available, personality, personality_custom, instructions }`; **`PUT`** the same fields
(403 when personalization is not `available` **or** the token is not a device's token — the
response's `detail` says which, and the CLI shows it, else "not allowed"; 422 for an `enabled`
that is not a boolean or a field that is not a string). Logged in to exactly one instance that has it `available`, the CLI uses
these in place of its local `personality`, `personality_custom` and `instructions` (none of them
when `enabled` is false; a preset the CLI does not know is none here), and `/settings` (or
`lembas config set`) writes a change back with a `PUT` of **only the field changed** and
`enabled: true` — never the session's copy of the other two, which may be blanked or mapped — so
the instance keeps what it has saved. LLeMbas must accept a partial `PUT`. Logged out, the local
keys apply.
- **The library**: logged in to one instance whose token has the `library` scope, a config that sets
no `library:` uses the account's (`lembas`); `library: local` written stays local.
## Notes
- `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions
resolve against (for a session opened at the project root, the project root). `/plan` without a
task is an action, with a task a prompt run in plan mode. Questions and plans also stop waiting
after the device's `remote.approval_timeout`, like approvals.
- The CLI's login accepts an instance whose discovery says protocol 1 or 2.