# mcp_nostr_explorer

Webowy GUI nad pod-agentem `nostr` z JOU. FastAPI + WebSocket; każda karta przeglądarki
dostaje własną sesję z własną instancją agenta po stronie serwera.

Komendę z konsoli (po prawej) wykonuje agent, a tabelaryczne wyniki `run_sql` lądują jako
draggable karty w panelu po lewej. Agent widzi tylko schemat + tag JOU — pełne dane są w GUI
i w magazynie JOU dostępnym przez `jou_sprawdź` / `jou_odczytaj`.

## Uruchomienie

```bash
./run.sh                # pierwszy wolny port w 5001..5020
./run.sh 5005           # konkretny port
```

Skrypt zakłada venv w `.venv/`, instaluje `requirements.txt` i odpala `main.py`.
Otwórz `http://127.0.0.1:<port>/` w przeglądarce.

## Wymagania

- działający `mcp_gateway` (URL w `conf/agent0.json` → `gateway_url`)
- pod-agent `nostr` w `jou/agents/nostr/{config.json, system_prompt.md}`
- `conf/agent0.json` z provider/api_key/model — patrz `jou/README.md`

Bez gateway-a backend jest oznaczony jako `down`. WS akceptuje połączenie, ale każde
`run` zwróci błąd. Po wstaniu gateway-a `KlientMCP` sam się reconnectuje (1s→30s exp);
explorer nie wymaga restartu.

## Endpointy

| Metoda | Ścieżka | Opis |
|--------|---------|------|
| `GET` | `/` | `index.html` (topbar + split: tabele / konsola) |
| `GET` | `/api/health` | live probe — `{"backend": "real"\|"down", "sessions": N}` |
| `GET` | `/api/models` | lista modeli PPQ z tool-call support, cache 5 min |
| `POST` | `/api/topup/confirm` | **STUB** — `{sid, sats}` dolicza saty (brak realnego LN) |
| `WS` | `/api/ws?sid=...` | pojedynczy bidirekcyjny kanał (komendy + eventy) |
| `DELETE` | `/api/session/{sid}` | Explicit teardown (browser onbeforeunload) |

`/api/health` woła `gateway.lista_narzędzi()` z 2-sekundowym timeoutem — `real` znaczy
że gateway *teraz* odpowiada, nie tylko że kiedyś wstał.

## Protokół WebSocket

JSON w obie strony.

**Klient → serwer:**
```json
{"type": "run",       "command": "..."}
{"type": "set_model", "model":   "claude-haiku-4.5"}
{"type": "ping"}
```

**Serwer → klient:**
```json
{"type": "hello",          "sid": "...", "backend": "real"|"down",
                           "model": "...", "balance_sat": 10000}
{"type": "started",        "command": "..."}
{"type": "table",          "table": {"columns": [...], "rows": [[...], ...]}}
{"type": "assistant_text", "text": "..."}
{"type": "error",          "message": "..."}
{"type": "done",           "cost_usd": 0.001, "took_ms": 12345}
{"type": "balance",        "sat": 9988, "last_charge_sat": 12, "last_charge_usd": 0.001}
{"type": "model",          "model": "claude-haiku-4.5"}
{"type": "pong"}
```

Klient wysyła `ping` co 25 s, serwer odpowiada `pong` — pomaga wykryć martwy socket
zanim TCP keepalive zauważy. Po close klient sam reconnectuje (exp backoff 1s→30s);
sesja na serwerze żyje dalej do 30 min bezczynności, więc reconnect odbiera tę samą
historię tury.

## Sesje

- **Lazy:** tworzone przy pierwszym `/api/ws` z nowym `sid`.
- **GC:** zamyka sesję po **30 min** bezczynności (`SESSION_IDLE_TIMEOUT_S`).
- **Hard cap:** **50** aktywnych sesji (`MAX_SESSIONS`) — powyżej WS dostaje close 1011.
- **Wielu klientów na jeden sid:** dozwolone — każdy WS dostaje swoją kopię stream-u eventów.

## Saldo i wybór modelu (symulacja)

W nagłówku jest pasek z saldem w sat (`⚡ N sat`) i dropdown modelu.
Saldo startuje na **10 000 sat** per sesja — udajemy że user „już zapłacił".

- Po każdej turze adapter `KlientPayPerQ` wyciąga z response `usage.cost`
  (a gdy klucz to BYOK i `cost==0` — fallback na
  `usage.cost_details.upstream_inference_cost`).
- `billing.usd_to_sat()` przelicza po aktualnym kursie BTC/USD (CoinGecko, cache 60 s).
- Saldo schodzi, WS broadcastuje `{"type":"balance", ...}`, UI animuje subtelny pulse.
- `+ topup` w nagłówku to symulacja — strzela `POST /api/topup/confirm` które
  dolicza saty bez weryfikacji. Realnej integracji LND/CLN na razie nie ma.

Wybór modelu pobierany z `GET /api/models` (proxy `https://api.ppq.ai/v1/models`,
cache 5 min, filtr: tool-call support + text output, ~250 modeli).
Zmiana wysyła `{"type":"set_model", "model":"..."}` przez WS — sticky w sesji.

## Logi

`explorer.log` w katalogu projektu — append, DEBUG (konsola: INFO).

## Testy

```bash
./.venv/bin/python -m unittest discover tests
```

Pokrycie: PayPerQ cost extraction, USD→SAT konwersja, Session billing/model,
models_catalog filtr+cache.

## Pliki

```
main.py             entrypoint (logging, port discovery, uvicorn)
server.py           FastAPI app — /, /api/health, /api/models, /api/topup/confirm,
                    /api/ws, /api/session/{sid}
agent_runtime.py    SharedRuntime, Session (fan-out + billing + model),
                    SessionsManager + wrapper run_sql
billing.py          usd_to_sat, get_btc_usd_rate (CoinGecko cache 60s)
models_catalog.py   fetch_chat_models (PPQ cache 5min, filtrowanie)
static/             index.html (topbar+split), app.js (WS+model+balance), style.css
tests/              testy_payperq, testy_billing, testy_session, testy_models_catalog
run.sh              venv + run
requirements.txt    fastapi, uvicorn, websockets, httpx, anthropic, openai, mcp
```
