Metadata-Version: 2.4
Name: sonnet-term
Version: 0.1.10
Summary: Sonnet persona as a terminal CLI, powered by Hermes Agent.
Author: Sonnet Desktop contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/sonnet-cli/sonnet
Project-URL: Repository, https://github.com/sonnet-cli/sonnet
Project-URL: Issues, https://github.com/sonnet-cli/sonnet/issues
Keywords: cli,agent,hermes,terminal,ai,chat
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pygments>=2.17
Requires-Dist: cryptography>=41.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: pillow>=10.0

# sonnet — CLI dari project Sonnet Desktop

Versi terminal dari Sonnet (persona KINJI, agent Hermes, skill pack dari
project aslinya). Folder ini hasil extract dari `SonnetSetup (2).exe` +
tambahan lapisan CLI.

## Struktur

```
sonnet              launcher bash (pakai .venv)
sonnet.py           entry point: argparse, REPL, oneshot
sonnet_json.py      engine: JsonSession (hermes --format stream-json) + Renderer
agent_core.py       dari project Sonnet (dipakai utuh: boot/setup)
stream_filter.py    dari project Sonnet (dipakai utuh)
prompts_enc/        dari project Sonnet (AES-GCM, system/bridge/personality)
skills_pack.zip     dari project Sonnet (962 skill)
hermes_config_template.yaml  dari project Sonnet
.venv/              python + cryptography + pyyaml
```

## Arsitektur prompt (UTUH, sama kayak desktop)

Boot flow:

1. `JsonSession.boot()` manggil `agent_core.HermesSession`:
   - `_ensure_soul()` — decrypt `prompts_enc/*.bin` (AES-GCM, key dari
     `_derive_key()`), tulis `SOUL.md` ke `HERMES_HOME`. Isi = system_prompt
     (46620 chars) + bridge_prompt (8135 chars).
   - `_ensure_skills()` — extract `skills_pack.zip` (962 SKILL.md) ke
     `HERMES_HOME/skills`.
   - `_ensure_hermes_config()` — generate `.hermes/config.yaml` dari
     `hermes_config_template.yaml`, isi `api_key`/model/reasoning_effort/
     disabled_toolsets.
   - `_ensure_hermes_auth()` — sync token OAuth kalo ada.
2. Hermes CLI auto-load `SOUL.md` dari `HERMES_HOME` (identity slot #1,
   always included) → persona KINJI aktif tanpa perlu injection manual.

Verifikasi: `test_sonnet_json.py` cek `SOUL.md` hasil boot beneran
mengandung system_prompt + bridge_prompt (bukan template lain).

## Kenapa stream-json, bukan PTY scraping

Desktop app/WebView2 scrape output PTY (`agent_core._spawn_tmux` +
`stream_filter.HermesStreamFilter`). Itu jalan di WebView2 karena ada
renderer sendiri yang nge-handle re-render. Di terminal, output PTY
hermes berantakan: tiap frame full-screen (status bar `☤ model │ ctx │ 0% │
21s │ ⚠ YOLO`, spinner, `┌─ box ┐`) nge-overwrite baris, dan yang masuk
pipe cuma delta — hasil nyata: status bar bocor + baris nyambung nyambung
(`"Find and fix a failing test────⚠️ Attempt 1/3, not retryable failed: …"`).

hermes punya mode resmi: `hermes chat -q … --oneshot --format stream-json`
→ JSONL per event (`system`/`reasoning`/`tool_call`/`tool_result`/
`assistant`/`result`), deterministik, plus `--resume SESSION_ID` buat
kelanjutan konteks. CLI pakai itu. PTY scraping tetap ada di `agent_core`
buat kalau lo mau mode interaktif penuh (TUI) — belum di-wire ke CLI.

## Pakai

Sudah di-symlink ke `/usr/local/bin/sonnet`, jadi ga perlu `./`:

```bash
sonnet                        # REPL (mode interaktif)
sonnet "cek disk"             # oneshot, langsung keluar
sonnet -C /path/proyek "fix bug"   # agent cwd = /path/proyek
sonnet -m deepseek-v4-flash "hi"
sonnet -t terminal,file "list repo"
sonnet -s obsidian "catatan kemarin"
sonnet --resume <session_id> "lanjut"   # lanjut konteks
sonnet --sessions             # list sesi
sonnet --models               # katalog model live
sonnet --doctor               # cek config + prompt + deps
sonnet --version              # versi CLI + path engine
sonnet help                   # help lengkap (juga /help di REPL)
```

### Slash command di REPL

| command | fungsi |
|---|---|
| teks apa aja | kirim ke agent |
| `/model` | **picker model interaktif** (ketik filter, enter pilih) |
| `/model <nama>` | set model langsung |
| `/key` | status key semua provider |
| `/key <provider> <key>` | simpan api key provider itu |
| `/new [nama]` | sesi baru, konteks bersih |
| `/sessions` | list sesi tersimpan, `*` = aktif |
| `/resume <sid>` | lanjut sesi |
| `/reasoning <lv>` | none/low/medium/high/max |
| `/tools <list>` | pilih toolsets |
| `/stop` | interrupt turn |
| `/clear` | bersihkan layar |
| `/doctor` | cek config |
| `/quit` `/q` | keluar |
| `/help` | bantuan |

Ctrl-C membatalkan turn, Ctrl-D keluar dari REPL.

### Fokus folder (`-C` / `/cd`)

```bash
sonnet -C ~/proj/proyek            # agent kerja di folder itu
sonnet                            # ingat folder terakhir
```

di REPL: `/cd ~/proj/proyek`, `/cd` (balik ke home), `/cd` lagi buat cek.
`-C` kesimpen di config, jadi `sonnet` berikutnya inget.

`HERMES_HOME` (SOUL.md, 973 skill, config, auth) **selalu** di project
sonnet — bukan di folder target. Waktu dulu `workdir` dipakai buat dua
hal sekaligus, jadi `sonnet -C ~/proj` nulis 460MB `.hermes` ke dalam
repo lo. Sekarang dipisah: `workdir` = state, `agent_cwd` = folder kerja
agent. Yang ngaruh ke tool (file/terminal/search) cuma `agent_cwd`, lewat
`TERMINAL_CWD` + `terminal.cwd` di config.yaml.

### Shim `hermes` global

`~/.hermes/hermes-agent/.hermes/bin/hermes` di-rewrite tiap kali hermes
dijalankan — baris `exec` diisi path absolut ke `<HERMES_HOME>/tools/python-*`,
dan HERMES_HOME diturunkan dari **PWD**, bukan env. Jalankan `hermes` dari
folder yang `.hermes/tools`-nya ga ada, shim ditulis ulang nunjukin path
itu; panggilan berikutnya → `python3: not found`, exit 127, semua hermes
mati.

CLI memanggil `repair_hermes_shim()` di `main()` sebelum nge-hermes,
jadi tiap `sonnet` /= nyelametin shim global. Test suiteounty ngecek
repair + idempotenya.

### Sama kayak Claude CLI?

Mirip secara interaksi, beda di implementation:

| | Claude CLI | sonnet |
|---|---|---|
| REPL + slash command | ya | ya, subset di atas |
| oneshot | `claude -p "..."` | `sonnet "..."` |
| ganti model saat jalan | `/model` | `/model` (picker) |
| `-C` working dir | ya | ya |
| resume sesi | `--resume` | `--resume` + `/sessions` |
| ENGINE | Claude | **hermes** |
| prompt/SOUL | prompt Claude | **SOUL.md project lo** (dari `prompts_enc`) |
| skills | Claude skills | **skills_pack desktop, 973 SKILL.md** |
| provider | hanya Anthropic | 10 provider + portal Nous |

Jadi ini Claude-CLI-shaped, tapi isi otaknya project lo.

## Pilih model

`/model` buka picker interaktif ala Claude Code: kursor `↑↓`, filter
ketik, `Enter` = pilih + simpan jadi default, `s` = sesi ini saja, `Esc`
batal. Nomor juga masih jalan (ketik angka) buat muscle memory lama.

Katalog diambil live dari API portal (421 model, 9 free), dicache 10
menit. Model `:free` **atau** yang pricing-nya 0/0 otomatis trigger
provider `nous`.

```bash
sonnet --models                     # list model tanpa masuk REPL
sonnet -p nous -m 'stealth/space-bunny-alpha'   # set di flag juga bisa
```

Tombol `s` (session-only) nulis ke `session_override.json`, bukan ke
`config.json` — jadi restart balik ke model default. Pilih `Enter` lagi
buat promotedin jadi default permanen.

## Login portal dari CLI

`sonnet --auth <aksi>` — wrapper atas `hermes auth nous`:

```bash
sonnet --auth login     # device flow: prints URL + kode
sonnet --auth status    # nous: logged in
sonnet --auth list      # detail kredensial (rate limit / 401)
sonnet --auth refresh   # refresh token OAuth
sonnet --auth reset     # bersihin cooldown
sonnet --auth logout
```

## Test

```bash
.venv/bin/python test_sonnet_json.py   # engine + prompt arch + integrasi hermes
.venv/bin/python test_sonnet_cli.py    # presentation layer (mode lama)
```

## Yang BELUM dites

Integrasi end-to-end pake API key asli. Semua run test pake key palsu
(`sk-fake...`) → model menolak (401), jadi yang terbukti: pipeline JSONL,
capture session_id, rendering event, boot/prompt/skills/config. Balasan
model sungguhan belum pernah diuji.

## Dependency

- python 3.10+ (venv: `cryptography`, `pyyaml`)
- `hermes` CLI di PATH (`~/.local/bin/hermes`)

## Config

Default `SONNET_HOME=~/.sonnet`:
- `config.json` (chmod 600, berisi api_key)
- `history.jsonl` (riwayat prompt)
- `~/.hermes/` di cwd project → SOUL.md, skills, config.yaml, sessions

## Provider: DeepSeek (default) vs Nous Portal (free)

CLI ini support dua provider:

### DeepSeek (butuh API key bayar)

```bash
export DEEPSEEK_API_KEY=sk-...
./sonnet
./sonnet -m deepseek-v4-pro "cek disk"
```

### Nous Portal (OAuth, model free)

Portal pake token OAuth dari `~/.hermes/auth.json`, bukan api key. CLI
sync `auth.json` itu ke `HERMES_HOME` folder project tiap boot (token
di-refresh per jam, jadi mtime dicek).

```bash
sonnet -p nous -m 'stealth/space-bunny-alpha' "halo"
./sonnet -p nous -m 'meituan/longcat-2.0:free' --reasoning none "halo"
```

Cara login manual di server (kalau `auth.json` belum ada / token mati):

```bash
hermes auth upgrade --no-browser
```

`--no-browser` mencetak URL + kode device flow. Buka URL-nya di browser
lo, login, paste kode balik ke terminal. Setelah itu `hermes auth status nous`
harus bilang `nous: logged in`.

Cek status kredensial:

```bash
hermes auth list nous      # detail per kredensial (rate limit, 401, dll)
hermes auth reset nous     # bersihin status cooldown/exhausted
```

### Model gratis di portal — cek `pricing`, bukan suffix

Ambil katalog live (9 dari 421 model gratis):

```bash
sonnet --models
```

**Gratis itu dua bentuk.** `stealth/space-bunny-alpha` jalan dgn saldo 0
padahal TIDAK berakhiran `:free` — di katalog entry-nya:

```json
"pricing": { "prompt": "0.0000000000", "completion": "0.0000000000" }
```

Jadi filter gratis = suffix `:free` **ATAU** `pricing.prompt == 0 and
pricing.completion == 0`. 8 dari 9 model gratis berakhiran `:free`, jadi
andalkan suffix doang bakal kehilangan model gratis terbaik. `portal_models()`
di CLI sekarang cek keduanya.

Status per model (hasil probe langsung ke inference API, 28 Sep 2026):

| model | status | ctx | catatan |
|---|---|---|---|
| `stealth/space-bunny-alpha` | hidup | 1M | pricing 0/0, multimodal, reasoning adjustable |
| `meituan/longcat-2.5-preview:free` | hidup | 1M | |
| `upstage/solar-pro4:free` | hidup | 512k | |
| `poolside/laguna-s-2.1:free` | hidup | 256k | |
| `poolside/laguna-xs-2.1:free` | hidup | 256k | |
| `inclusionai/ling-3.0-flash-sante:free` | hidup | 256k | |
| `stepfun/step-3.7-flash:free` | 429 | 256k | fair-share limit, per model |
| `meituan/longcat-2.0:free` | 404 | 1M | "no longer free" |
| `inclusionai/ling-3.0-flash-fin:free` | 404 | 256k | entri katalog, endpoint mati |

Katalog `/models` nampilin semua slug termasuk yg mati, jadi **katalog bukan
sumber kebenaran** — probe yang jujur.

Model paid tetap gabisa dipakai dgn saldo 0:
> Your Nous Portal account has no active subscription or usable credits
> (usable $0.00, subscription $0.00, purchased $0.00)

### Live tool status

Baris status tool ditulis in-place: spinner braille berputar selama tool
jalan, lalu diganti `✓`/`✗` + durasi pas selesai. Di non-TTY (pipe ke log)
jatuh ke mode statis, jadi `\r` + spinner di log cuma jadi sampah.

```
❯ list 3 file python di cwd

  ⠋ search_files /root/sonnet-cli  0.1s
  ✓ terminal wc -l sonnet.py sonnet_json.py  0.2s

3 file python di cwd: ...
  ── 588 in · 343 out
```

Spinner di-overwrite di satu baris, bukan nambah baris per frame.

## Install

### Unix (macOS / Linux / WSL)

```bash
curl -fsSL https://sonnet-cli.dev/install.sh | bash
```

### Windows (PowerShell)

```powershell
iex (irm https://sonnet-cli.dev/install.ps1)
```

Installer cek Python 3.10+, cek/install Hermes (engine), download sonnet
ke `~/.sonnet/app`, bikin venv, taruh launcher di `~/.local/bin/sonnet`,
lalu jalanin wizard provider sekali aja. Setelah itu user tinggal
`sonnet`.

### Update

```bash
sonnet --update              # update sonnet + hermes
sonnet --update --no-hermes  # sonnet saja
```

### Dari sumber (developer)

```bash
git clone https://github.com/sonnet-cli/sonnet
cd sonnet
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
ln -s "$PWD/sonnet" ~/.local/bin/sonnet
```

## Install

### pip (recommended)

```bash
pip install sonnet-term
sonnet
```

**LIVE di PyPI** (v0.1.0). Binary tetap `sonnet`.

Package name: `sonnet-term` — `sonnet` dan `sonnet-cli` sudah dipakai di
PyPI (library lain: graph library dan Typer/rich building blocks).
Binary-nya tetap bernama `sonnet`, jadi user selalu tulis `sonnet`.

`pip install` **tidak** menarik Hermes Agent: engine itu ~100MB dan
user mungkin udah punya versi sendiri. Setelah install, `sonnet` akan
deteksi Hermes; kalau belum ada, wizard nunjukin command install-nya.
Set `SONNET_SKIP_HERMES=1` untuk lewati pengecekan.

### curl (unix)

```bash
curl -fsSL https://sonnet-cli.dev/install.sh | bash
```

### PowerShell (windows)

```powershell
iex (irm https://sonnet-cli.dev/install.ps1)
```

Kedua installer cek Python 3.10+, cek/install Hermes, taruh sonnet di
`~/.sonnet/app`, bikin venv, symlink (unix) atau `sonnet.cmd` (windows),
lalu jalanin wizard provider sekali aja.

### Dari sumber (developer)

```bash
git clone https://github.com/sonnet-cli/sonnet
cd sonnet
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
./sonnet
```

### Update

```bash
sonnet --update              # update sonnet + hermes
sonnet --update --no-hermes  # sonnet saja
```

### Di mana state/config

| apa | unix | windows |
|---|---|---|
| config + history | `~/.sonnet/` | `%USERPROFILE%\.sonnet\` |
| Hermes state (SQLite, skills, SOUL.md) | `~/.sonnet/hermes/` | `%USERPROFILE%\.sonnet\hermes\` |
| install (curl/ps1) | `~/.sonnet/app/` | `%USERPROFILE%\.sonnet\app\` |
| install (pip) | site-packages | site-packages |

Untuk install `pip`, state otomatis pindah ke `~/.sonnet/hermes` karena
site-packages read-only. Override dengan `SONNET_HERMES_HOME`.
Install `curl`/repo memakai `<project>/.hermes` supaya state nyambung
sama source.

## Autocomplete input bar

Ketik `/` di baris input -> muncul preview semua slash command
lengkap dengan deskripsi. Ketik `@` -> preview file & folder di folder
kerja.

```
❯ /
 /help             daftar perintah
  /model [nama]     ganti model
  /quit             keluar dari sonnet
  /new              mulai sesi baru
  /clear            bersihkan layar
  /resume [id]      picker sesi interaktif
  ↓ 15 · Tab pilih · Esc batal
```

Cara pakai:

| tombol | fungsi |
|---|---|
| ketik | filter langsung (fuzzy: `mdl` -> `/model`) |
| ↑ / ↓ | pindah baris preview |
| PgUp / PgDn | geser satu halaman |
| Home / End | baris pertama / terakhir |
| Tab | isi baris dengan item terpilih |
| Enter | kirim apa adanya (tanpa autocomplete) |
| Esc / Delete | tutup preview |
| Ctrl+U | hapus baris + tutup preview |

Mulai ketik `/` tapi udah ada spasi (mis. `/model deepseek`) preview
tidak muncul - itu argumen, bukan nama command.

## Setup: langsung dari dalam chat

`sonnet` langsung buka welcome chat, tanpa wizard. Provider, key, model,
Tavily semuanya diset dari dalam chat:

```
/provider                # lihat provider sekarang + yang tersedia
/provider nous           # pindah ke Nous Portal (gratis, tinggal login)
/key deepseek sk-xxx     # set api key
/key                     # status key semua provider
/model                   # picker model
/tavily                  # status / set api key Tavily
```

Kalo provider aktif belum punya key, Sonnet tetap masuk REPL dan
nge-print cara set-nya, jadi user ga perlu keluar ke shell buat edit
env var dulu:

```
belum ada api key buat provider deepseek.
  set dari dalam chat: /key deepseek <api-key>
  atau di shell: export DEEPSEEK_API_KEY=...
  portal gratis (ga perlu key): /provider nous  lalu sonnet --auth login
```

Keys disimpan di `~/.sonnet/config.json` dengan permission 600.

Wizard lama masih ada, tapi cuma muncul kalo:
- config.json rusak (bukan JSON valid), atau
- wizard sebelumnya belum tuntas (`wizard_started` tanpa `wizard_done`), atau
- `SONNET_FORCE_WIZARD=1` dipaksa, atau
- ada flag (`-m`, `-p`, `--tavily`) atau env key — biar oneshot
  non-interaktif ga ketahan

## Welcome panel

Saat REPL buka, panel 2 kolom ala Claude Code:

```
 | sonnet v0.1.0                     Tips buat mulai                           |
 |                                   /init  bikin SONNET.md aturan project     |
 | stealth/space-bunny-alpha - nous  /add-dir  tambah folder lain              |
 |   /home/user/proj/api              /where  lihat folder kerja agent          |
 |   + /home/user/proj/shared         @file.py  sisipin isi file                |
 |   2 sesi - ketik /sessions         !ls  jalankan shell langsung              |
 |                                   /model  ganti model                       |
 |                                   Aktivitas terakhir                        |
 |                                   - benerin bug login                       |
```

Kiri: versi, model, folder kerja, extra dir, jumlah sesi. Kanan:
tips + aktivitas terakhir. Matikan: `"welcome_panel": false` di config.

## `/add-dir` - multi-root

Satu folder kerja default. Tambah folder lain yang boleh diakses
agent tanpa `cd`:

```
/add-dir                  # lihat daftar
/add-dir ~/proj/shared    # tambah
/add-dir --drop ~/proj/shared
```

Extra dir dikirim ke agent sebagai `extra_dirs` di config YAML, bukan
dipindah jadi cwd - jadi agent tetap tahu folder mana yang primary.
Tersimpan di `~/.sonnet/config.json`, kepulih tiap buka REPL.

## `/where` - cek folder kerja

```bash
sonnet --where -C ~/proj/api
```

```
agent cwd : /home/kin/proj/api
  d src
  - README.md
model     : stealth/space-bunny-alpha (nous)
```

Dalam chat: `/where` (alias `/pwd`, `/dir`) nampilin path + isi folder
sekaligus. Pindah pakai `/cd <path>`.

**Akses**: sonnet jalan dengan `--yolo`, jadi **nggak ada prompt
approval**. Agent bisa baca/tulis folder mana pun yang user punya.
Batasnya cuma permission OS — user biasa nggak bisa baca file root.

## Ringkasan hasil tool (ala Claude Code)

Di bawah `✓ ok 0.2s` sekarang ada ringkasan singkat hasil tool:

```
  ✓ read_file /tmp/x/a.py  0.1s  ⎿ 42 lines
  ✓ search_files *.py       0.1s  ⎿ 3 matches
  ✓ terminal python3 t.py   0.4s  ⎿ 2 lines · halo dunia
```

Ringkasan menyesuaikan tipe tool: jumlah baris (file), jumlah match
(search), atau jumlah baris + baris pertama (terminal).

## Chat bar (ala Claude Code)

Input REPL bukan `input()` polos lagi. Yang ada:

- prompt `❯` + **ghost hint** redup (`ketik pesan · /help · !shell · @file · ? shortcut`) yang lenyap pas lo ketik
- **multiline**: `Ctrl+J` atau `\` + Enter = baris baru, Enter doang = kirim
- **`!cmd`** shell mode — jalanin command, output-nya masuk ke konteks agent
- **`@path`** file mention — isi file disisipin otomatis, agent ga perlu baca sendiri
- **`?`** di prompt kosong = panel shortcut
- **Ctrl+A/E/W/U/K/L** editing, **↑↓** history, **←→** kursor
- **Ctrl+C** pertama bersihkan baris, kedua keluar. **Ctrl+D** keluar kalo prompt kosong

Semua otomatis jatuh ke `input()` biasa kalo bukan tty (pipe, test).

## Warna + diff (ala Claude Code)

Claude Code bedain dua hal secara visual, dan itu yang bikin chat panjang
tetap enak dibaca:

- **prompt user** = `❯` + teks **putih terang**
- **jawaban agent** = teks **redup**, jelas kelihatan lebih redup dari yang lo ketik

plus **diff inline** setiap agent ngedit file:

```
  ── /path/contoh.txt
     +1 -1
    baris dua
  - baris tiga
  + TIGA BARU
```

- merah = baris yang dihapus, hijau = yang ditambahkan
- Maximum 24 baris per diff, sisanya `… (dipotong)`
- Berlaku buat: `write_file`, `str_replace`, `patch`, `multiedit`, `edit_file`
- Toggle: `/diff on|off` (default on), atau `--no-diff`

### Syntax highlight code block

Claude Code mewarnain kode yang ditulis model. Sonnet sekarang begitu juga:

```
  ╭─ python
  def fibonacci(n):        ← keyword magenta
      a, b = 0, 1          ← angka oranye
      for _ in range(n):   ← builtin cyan
          hasil.append(a)
      return hasil
  ╰─
```

- pakai **pygments**, palet nyambung sama warna sonnet
- **tanpa frame secara default** (`none`). Kalau agent ngedit file, kode
  udah ditunjukin sama diff ijo/merah, jadi frame cuma ngeganda.
  Opsional: `/frame bar` (garis vertikal tipis) atau `/frame box`
  (`╭─` / `╰─` ala Claude Code)
- streaming aman: buffer per-baris, jadi fence ``` yang kepotong di
  tengah chunk tetap kedeteksi
- fence yang belum ketutup (turn selesai duluan) tetap ditampilkan
- bahasa yang didukung: python, js/ts, bash, json, rust, go, sql, yaml, html,
  c/cpp, ruby, php, dll — apa pun yang pygments kenal
- toggle: `/hl on|off` atau `--no-highlight`
- butuh `pygments` di `.venv`: `pip install pygments`

### thinking interaktif

Thinking agent disembunyikan secara default, cuma muncul ringkasannya:

```
┌─ thinking (ctrl+t untuk expand)
 └─ (824 chars disembunyikan, ctrl+t untuk lihat)
```

- `/thinking on|off` buat nyalakan/mematikan permanen
- `--reasoning low|medium|high|off` atur effort

### Gaya tampilan user

```bash
sonnet            # default: ❯ rata kiri
```

| command | tampilan |
|---|---|
| `/style` | status sekarang |
| `/style claude` | `❯ teks` rata kiri, warna kontras (default) |
| `/style right` | bubble rata kanan dengan border, gaya Gemini/Cursor |
| `/style plain` | polos tanpa dekorasi |

## Screenshot CLI

`tools/pty_capture.py` + `tools/pty_drive.py` jalanin sonnet di pty dan
tangkap output mentah; `tools/ansi2png.py` emulasikan terminal (grid 2D:
CR, ESC[nK, ESC[A/B gerak kursor, ESC[7m reverse video) lalu render ke
PNG. Dua jebakan yang bikin screenshot ngaco:

- file harus dibaca dengan `newline=""` — universal newlines menelan CR
  jadi LF, jadi overwrite spinner kelihatan 3 baris padahal di terminal
  cuma 1 baris hidup;
- `emulate()` balikin `(baris, mask_reverse)`, bukan cuma baris. Tanpa
  mask, baris terpilih di picker kelihatan sama aja dengan yang lain.

```bash
.venv/bin/python tools/pty_capture.py out.txt "./sonnet -p nous -m 'x'"
.venv/bin/python tools/ansi2png.py shot.png "judul" out.txt
.venv/bin/python tools/pty_drive.py out.txt 1.5 "./sonnet" "/help" "/model"
```

### Rate limit portal — itu PER MODEL, bukan per akun

Dari log `screen -r hermes`:
> You've reached **this model's** current fair-share rate limit. It adapts
> to demand — retry after the indicated delay, or try an alternate model.

Kredensial lo bisa sehat total sementara satu model kena 429. Header
`retry-after` nunjukin sisa waktu (teramati 1246-6511 detik, turun tiap
probe). hermes retry 3x, dan karena delaynya ribuan detik, request
kelihatan "hang" sampe 90 detik.

CLI punya dua lapis cek:

1. `preflight()` — `hermes auth status` + `auth list` (kredensial sehat?)
2. `probe_portal_model()` — 1 request mini ke model yg aktif. Ini yg
   penting: auth check gak nyentuh rate limit per model.

Kalo kena, CLI lapor sisa cooldown + suggest model gratis lain yg
di-probe beneran hidup:
```
auth: model stepfun/step-3.7-flash:free kena rate limit (429) — coba lagi dalam ~9m32s
  model free lain yg lagi sehat: stealth/space-bunny-alpha
```

### Catatan config portal

- `base_url` & `api_key` dikosongin otomatis buat portal — hermes ambil
  `inference_base_url` + `agent_key` sendiri dari `auth.json`.
- `config.yaml` di-rewrite tiap boot: blok `model:` diganti ke
  `provider: nous` + model aktif, karena template Sonnet selalu nulis
  `provider: deepseek` (kalau dibiarin, hermes pilih deepseek → 401).
- Event stream dari portal bertipe `text` (bukan `assistant`) — renderer
  handle keduanya.
- `200 tapi jawaban kosong` (choices: []) diperlakukan gagal — model yg
  gabisa jawab jangan disarankan.
