grokbot_api 1.2.0

Unofficial async client for the (undocumented) Grok Bot gateway API
Documentation
# Transcripts

[← README](README.md)

## The newest page

```rust,no_run
use grokbot_api::{Client, sanitize_text};

async fn show(client: &Client, bot: &str) -> grokbot_api::Result<()> {
    let page = client.transcript_tail(bot, 50, None).await?;
    for e in &page.entries {
        let who = if e.is_from_user() { "you" } else { "bot" };
        println!("[{who}] {}", sanitize_text(&e.text));
    }
    if let Some(older) = page.next_before_seq {
        let _previous_page = client.transcript_tail(bot, 50, Some(older)).await?;
    }
    Ok(())
}
```

`transcript_tail(id, limit, before_seq)` calls `getAgentTranscriptTail`:

- `limit` is clamped to 1..=200.
- With `before_seq: None` you get the newest page. Pass the page's `next_before_seq` to get
  the page before it (the desktop app pages the same way).
- Entries in a page are oldest first.

## The whole history

```rust,no_run
use grokbot_api::{Client, Transcript};

async fn everything(client: &Client, bot: &str) -> grokbot_api::Result<Transcript> {
    client.transcript_history(bot, 200, 1_000).await // up to the newest 1,000 entries
}
```

`transcript_history(id, page_size, max_entries)` walks pages from newest to oldest:

- It stops at an empty page, at a page without `nextBeforeSeq`, or once it has
  `max_entries`. At the cap it keeps the **newest** entries.
- The result is oldest to newest. Entries are de-duplicated by id; entries without an id
  are kept.
- `next_before_seq` is where the walk stopped. `raw` is `{"pages": [...]}`.

## Threads

`thread(id, root_id)` calls `getAgentThread` and returns the replies under one entry. To
reply in a thread, set `SendPrompt::reply_to_id`.

## Entries

The entry format is undocumented, so `Entry` keeps the raw record and derives a few
convenience fields.

- **`id`**: the entry id, if it's a string.
- **`role`**: the first non-empty `role`, `kind`, `sender` or `type`, else `"msg"`.
  - User prompts use kinds like `user-prompt`.
  - Bot replies use `send-message`.
- **`text`**: the first match of:
  1. `text`, `prompt` or `message`, if it's a string
  2. `content` as a string
  3. `content` as an array of strings or `{text}` / `{content}` parts, joined with newlines
  4. `content.text`, or the `content` object as compact JSON
  5. `message.content` (bot replies)
  6. `preview`
- **`raw`**: the full record.

`is_from_user()` is a heuristic: the role contains `user`, `human` or `prompt`.
`Entry::from_value` parses a raw entry and never fails. `approval()` finds approval cards;
see [APPROVALS.md](APPROVALS.md).

## Displaying text safely

Transcript text comes from a server and from LLM output. `sanitize_text` removes ANSI
escape sequences (CSI and OSC, including hyperlinks) and control characters except `\n`
and `\t`, so a message can't move the cursor, retitle the terminal or hide text:

```rust
assert_eq!(grokbot_api::sanitize_text("\u{1b}[2J\u{1b}]0;pwned\u{7}hi\r"), "hi");
```