# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is Two crates: - **`outline-sdk`** (root, lib name `outline`) — an async Rust client library for the [Outline](https://www.getoutline.com) knowledge base API. Pure API layer, no GUI/CLI code. - **`cli/`** (`outline-cli`) — a minimal terminal client (`login`, `search`, `get`) built on top of the library via a path dependency. **Separate Cargo workspace** with its own `Cargo.lock` — commands run from the root do not build/test the CLI, and vice versa. ## Commands Run from the repo root (`outline-sdk`): ```sh cargo fmt --check cargo clippy --all-features --all-targets -- -D warnings cargo test --all-features cargo doc --no-deps --all-features ``` Run a single test: `cargo test --all-features ` (e.g. `cargo test --all-features normalizes_bare_host`). Live smoke tests against the real API are `#[ignore]`d by default (see `tests/live.rs`): ```sh OUTLINE_API_KEY=ol_api_... cargo test -- --ignored ``` Run from `cli/` (separate workspace, `cd cli` first): ```sh cargo build --release # binary at target/release/outline cargo fmt --check cargo clippy --all-features --all-targets -- -D warnings ``` CI (`.github/workflows/ci.yml`) runs fmt check, clippy (`-D warnings`), `cargo test --all-features`, and `cargo doc` for the root crate only. ## Architecture (root crate) The Outline API is **RPC-style**: every endpoint is `POST /api/.` with a JSON body and a `{ok, data, pagination, policies}` envelope — there's no REST-y resource routing to reason about. The whole crate is built around that one shape: - **`client.rs`** — `Client` holds a `reqwest::Client`, base URL, and `Auth`, wrapped in an `Arc` (cheap to clone, shared connection pool). All real HTTP happens through `Client::send`, which three thin wrappers sit on top of: `rpc` (single object → `data`), `rpc_list` (list → `Page`), `rpc_ok` (fire-and-forget, e.g. `delete`). Every endpoint is declared as a `const MethodDef` (name + `idempotent` flag) at the top of its `api/*.rs` file and passed into one of these three. - **`envelope.rs`** — the `{data, pagination, policies}` wrapper every success response is deserialized into before being unwrapped by `Client::rpc`/`rpc_list`. - **`api/`** — one module per resource (`auth`, `documents`, `collections`, `users`), each exposed as `client.()` returning a `*Api<'a>` struct borrowing the client. List/search/create/ update calls return a **builder** (e.g. `ListDocuments`, `SearchDocuments`) with chained setter methods and a terminal `.send()` (single page) or `.paginate()` (see below). Adding a new endpoint means: add a `const MethodDef`, add a method on the resource's `*Api` struct, and (for list/search) add a params struct implementing `Paginated` plus a builder. - **`page.rs`** — pagination is generic over any params type implementing `Paginated` (i.e. embeds `PageParams` via `#[serde(flatten)]`). `Paginator::next_page` advances `offset` itself using the returned item count (a page shorter than the requested limit means done) — it does **not** trust the API's `nextPath` hint, since that field isn't part of the published spec. `paginate()` also gets a `into_stream()` under the `stream` feature. - **`error.rs`** — single `Error` enum (`Config`, `Transport`, `Decode`, `Api`). `Error::kind()` gives a coarse `ErrorKind` (maps HTTP status *and* the API's own `error` string code, since Outline doesn't always use the "right" status code) for match-free handling; `is_retryable()` and `retry_after()` build on top of it. - **`models/`** — response types, deliberately decoupled from `Client` so they can be held independently of a live connection (e.g. in GUI app state). Two forward-compatibility patterns used throughout, because the API is unversioned and self-hosted instances vary in age: - `Id` is a transparent `String` newtype, not `uuid::Uuid` — an unexpected id format from an older/newer server must not fail deserialization of a whole response. - String enums carry an `Unknown(UnknownVariant)` variant via `#[serde(untagged)]` instead of `#[serde(other)]`, so an unrecognized value round-trips instead of causing a hard failure. - **`rate_limit.rs`** — parses `RateLimit-*`/`Retry-After` response headers; attached to `Error::Api` so callers can back off correctly. - **`spec/spec3.json`** — a vendored copy of Outline's OpenAPI spec. `tests/spec_coverage.rs` asserts every implemented RPC method name still exists as a path in it, catching typos and upstream renames without hitting the network. Feature flags of note: `stream` (pagination as a `futures_core::Stream`), `rustls-tls` / `native-tls` (mutually exclusive TLS backend choice), `uuid` (`Id::as_uuid()`), `tracing`, `multipart`. ## Architecture (`cli/`) - **`cli.rs`** — clap `Parser`/`Subcommand` definitions only; no logic. - **`commands/`** — one module per subcommand, dispatched from `commands/mod.rs`. - **`config.rs`** — non-secret settings (currently just `base_url`) in `~/.config/outline-cli/config.toml`. Precedence for the effective base URL: `--base-url`/`$OUTLINE_URL` > saved config > Outline Cloud default. - **`secret.rs`** — the *only* module that talks to the FreeDesktop Secret Service (GNOME Keyring/KWallet) for the API key. The API key is never written to the config file. Swapping the secret backend later means rewriting only this file. - Rendered documents are cached in `~/.cache/outline-cli/` (mode `0600`) rather than `/tmp`, since documents may be confidential. ## Testing conventions - `tests/common/mod.rs::client_for` spins up a `wiremock::MockServer` and returns a `Client` pointed at it with a dummy API key — the standard fixture for every non-live integration test. - `tests/fixtures/` holds canned JSON response bodies. - `tests/live.rs` is the only suite that hits the real API; it's `#[ignore]`d and requires `OUTLINE_API_KEY`. - Unit tests for pure logic (e.g. base-URL normalization in `builder.rs`, config precedence in `cli/src/config.rs`) live inline in `#[cfg(test)] mod tests` next to the code they test, not under `tests/`.