slate
by giveme11us
README.md
# Slate
Una whiteboard sull'iPad che gli agenti AI possono leggere e scrivere via MCP.
Disegni e scrivi a mano con la Pencil, aggiungi note e riquadri. Dall'altra parte
un agente (Claude Code o qualsiasi client MCP) legge la board come **struttura**
— elementi, testo, posizioni, frecce — e come **immagine**, così capisce anche
quello che è stato scritto a mano. E può rispondere scrivendo sulla board.
Tutto gira sul tuo Mac. Nessun servizio esterno, nessun dato che esce dalla rete.
> **Prima di adottarlo:** il codice di Slate è MIT, ma il canvas è l'SDK di
> tldraw, che **non** è open source e limita l'uso gratuito agli ambienti di
> sviluppo. Per un deployment di produzione serve una licenza tldraw. Dettagli
> nella [sezione Licenza](#licenza) — leggila prima di costruirci sopra.
## Come è fatto
```
iPad Safari (PWA tldraw) ─┐
├─ WebSocket sync ─► board-server (Node 22+)
Mac browser (stessa PWA) ─┘ • @tldraw/sync-core
• SQLite (un file per board)
• render PNG headless + OCR
▲
Claude Code / altri agent ── stdio MCP ──► slate-mcp ────┘
```
Lo stato vive sul server, non sull'iPad: la board resta leggibile anche a tablet
spento.
| Cartella | Cosa fa |
|---|---|
| `apps/board-server` | Sync tldraw, persistenza, API, render, OCR |
| `apps/board-web` | La PWA installabile sull'iPad |
| `apps/slate-mcp` | Il server MCP che parla agli agenti |
| `tools/ocr` | Binario Swift che usa Apple Vision per la scrittura a mano |
## Prerequisiti
- **Node ≥ 22.12** — richiesto da tldraw 5. Il `.node-version` punta a 26.
Se la tua shell è su Node 20 (default per altri progetti), usa
`/opt/homebrew/bin/node` o `fnm use 26`.
- **Google Chrome** — usato in headless per il render. Nessun browser da scaricare.
- **Xcode command line tools** — solo per compilare il binario OCR.
## Setup
```bash
npm install
npm run build
# OCR della scrittura a mano (opzionale ma consigliato)
cd tools/ocr && swiftc -O -o slate-ocr main.swift && cd -
```
## Avvio
```bash
npm run dev:server # in sviluppo
```
Oppure come servizio, così parte da solo al login:
```bash
./scripts/install-service.sh
```
All'avvio il server stampa i link di pairing:
```
[slate] pair a device: http://192.168.1.x:4501/?token=… ← LAN
[slate] pair a device: http://100.x.x.x:4501/?token=… ← Tailscale
```
## Collegare l'iPad
1. Apri uno dei link di pairing su Safari. Il token viene salvato e tolto dall'URL.
2. Condividi → **Aggiungi a Home**. Si apre a schermo intero, senza barre.
3. Fuori casa serve **Tailscale sull'iPad** (usa il link `100.x`). In LAN non serve.
### Il firewall di macOS blocca tutto
Se l'iPad non si collega ma da Mac funziona, è quasi certamente il firewall:
autorizza il binario `node` **reale** (non il symlink).
```bash
NODE_REAL=$(readlink -f /opt/homebrew/bin/node)
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$NODE_REAL"
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$NODE_REAL"
```
Va **rifatto dopo ogni aggiornamento di Node**: Homebrew installa in un path
versionato (`/opt/homebrew/Cellar/node/26.4.0/bin/node`) e l'autorizzazione è
legata a quel path esatto.
## Collegare l'agente
```bash
claude mcp add slate -- /opt/homebrew/bin/node ~/GitHub/mcp/slate/apps/slate-mcp/dist/index.js
```
L'MCP parla al server via loopback, che è sempre autorizzato: nessun token da
configurare finché server e agente stanno sulla stessa macchina. Se li separi,
passa `SLATE_SERVER` e `SLATE_TOKEN`.
### Tool disponibili
| Tool | Cosa fa |
|---|---|
| `list_boards` | Elenca le board, dalla più recente |
| `read_board` | Struttura: elementi, testo, posizioni, gruppi, frecce, + OCR della scrittura |
| `render_board` | Restituisce la board come immagine PNG |
| `search_board` | Cerca testo, incluso quello scritto a mano |
| `create_board` | Crea una board vuota |
| `add_note` | Aggiunge una sticky note (posizionata da sola per non sovrapporsi) |
| `add_image` | Carica un'immagine da file e la posiziona sulla board (PNG, JPEG, GIF, WebP) |
| `update_note` | Cambia il testo di un elemento mantenendo stile e posizione |
| `delete_shapes` | Elimina elementi |
Eliminare una board **non** è esposto come tool: si fa via
`DELETE /api/boards/:id`. Un agente può cancellare elementi, non board intere.
## Quanto è affidabile l'OCR
Poco, ed è per questo che `render_board` esiste. Su scrittura a mano reale Apple
Vision prende circa 3 parole su 4: abbastanza per **cercare** ("in che board avevo
scritto FLUSSO?"), non abbastanza per **fidarsi** del testo riconosciuto.
Per capire davvero cosa c'è su una board disegnata, l'agente deve guardare
l'immagine. `read_board` lo dice esplicitamente nel proprio output.
Misurato su una board di prova (`scripts/ocr-scale-test.mjs` confronta le varianti):
| Variante | Risultato |
|---|---|
| scale 1.5, corretto | `CIAO QUESTO E ON` / `TEST` / `ALUSSO 1` / `FLUSSO?` |
| scale 2, corretto (default) | `CIAO QUESTO E ON` / `TEST` / `FLUSSO 1` / `Autor` |
| scale 3, corretto | `CIAO QUEDO E ON` / `TEST` / `FLUSSO 1` / `{LUSSO?` |
| qualsiasi scala, `--raw` | sempre uguale o peggio |
Due cose controintuitive emerse dai dati:
- **Alzare la risoluzione non migliora linearmente.** A scale 3 la scrittura viene
letta peggio che a 2 in alcuni punti: i tratti spessi iniziano a fondersi.
Regola con `SLATE_OCR_SCALE`, non dare per scontato che più alto sia meglio.
- **La correzione linguistica conviene tenerla accesa**, anche se spinge verso
parole di dizionario (è lei a trasformare `FLUSSO2` in `Autor`). Disattivarla
con `--raw` peggiora tutto il resto. Se la tua board è fatta solo di sigle,
riprova la misura: il compromesso può ribaltarsi.
## Sicurezza
Un solo modello di accesso per tutte le superfici remote — API, sync e upload.
Loopback è sempre autorizzato (è così che entra l'MCP); tutto il resto richiede
il token, che viene generato al primo avvio e salvato in `~/.slate/token` (0600).
Non esiste una modalità "senza token": lasciare aperto il sync proteggendo solo
l'API sarebbe inutile, perché chi raggiunge il socket di sync ha già accesso
completo in lettura e scrittura a ogni board.
Le immagini sotto `/media/` si leggono senza token: tldraw le carica con `<img
src>`, che non può portare un header, e mettere il token nell'URL lo farebbe
finire dentro i documenti esportati. I nomi contengono un UUID, quindi non sono
indovinabili.
Per restringere l'ascolto alla sola tailnet:
```bash
SLATE_HOST=100.x.x.x npm run dev:server # il tuo indirizzo Tailscale
```
## Variabili d'ambiente
| Variabile | Default | Note |
|---|---|---|
| `SLATE_HOST` | `0.0.0.0` | Metti l'IP Tailscale per non esporre in LAN |
| `SLATE_PORT` | `4501` | |
| `SLATE_DATA_DIR` | `~/.slate` | Board, asset, token |
| `SLATE_TOKEN` | generato | Sovrascrive quello salvato |
| `SLATE_CHROME_PATH` | Chrome in /Applications | Per il render |
| `SLATE_OCR_BIN` | `tools/ocr/slate-ocr` | |
| `SLATE_OCR_SCALE` | `2` | Risoluzione del render passato a Vision |
| `SLATE_SERVER` | `http://127.0.0.1:4501` | Lato MCP |
## Trappole già incontrate
Cose che costano un pomeriggio se le scopri da solo.
- **`fontSizeAdjustment: 0` rende il testo invisibile.** La nota si salva, si
rilegge correttamente dai dati, e sull'iPad appare **vuota**. Il valore giusto
è `null` ("ricalcola"). Se scrivi record a mano, non copiare uno `0` da un
esempio vecchio.
- **`/assets` è di Vite.** Le immagini delle board stanno sotto `/media` perché
la build della PWA emette i suoi bundle in `/assets`: una rotta sovrapposta
se li mangia e l'app resta senza JavaScript, fallendo in modo silenzioso.
- **Il testo è ProseMirror, non stringa.** `props.richText` vuole un documento;
una stringa viene rifiutata dallo schema.
- **I tratti sono path base64.** tldraw 5 non espone un encoder, quindi non si
può fabbricare un tratto a mano libera da script — solo disegnandolo. Per lo
stesso motivo la **dimensione** di un tratto non è calcolabile dal record: la
sanno solo il renderer e l'endpoint OCR, che la misurano in un editor vivo.
- **Non elencare i tratti uno per uno.** Una pagina scritta a mano sono decine o
centinaia di `draw`: elencarli produce un muro di righe identiche che seppellisce
tutto il resto. `read_board` li conta e mostra il testo riconosciuto.
- **Le note non crescono da sole.** Una sticky è un riquadro fisso 200×200 e il
testo che eccede viene **tagliato nel render**, pur restando integro nei dati:
l'API lo rilegge intero mentre sull'iPad la nota appare mozzata. `add_note`
stima `growY` dalla lunghezza del testo; il client ricalcola l'altezza esatta
alla prima modifica.
- **Il posizionamento automatico ha bisogno delle misure vere.** Siccome i tratti
dichiarano dimensione zero, su una board disegnata il calcolo del bordo destro
cade dove *inizia* il primo tratto e la nota finisce sopra il disegno. Per
questo `add_note` interroga `/api/boards/:id/bounds` quando trova inchiostro.
- **I record scritti vengono validati.** Il server rifiuta con 400 quello che non
passa lo schema tldraw, invece di far crashare il canvas sull'iPad più tardi.
- **La licenza tldraw non è open source.** Vedi la sezione qui sotto: incide su
cosa puoi farci, non solo sul watermark.
## Licenza
Il codice di Slate è **MIT** (vedi `LICENSE`): usalo, modificalo, ridistribuiscilo
come vuoi.
**Ma il canvas no.** Slate è costruito sull'SDK di [tldraw](https://tldraw.dev),
che non è open source e ha una licenza propria. Vale la pena leggerla prima di
adottare Slate, perché è più restrittiva di quanto il watermark lasci intuire:
- L'uso gratuito è limitato ai **Development Environment**. La licenza definisce
Production come «any production deployment of the Software that operates on
servers, cloud platforms, web applications, or where the software is used to
provide functionality to end users».
- Per un deployment di produzione serve una licenza commerciale o un trial.
- La redistribuzione è ammessa solo **come componente di un'altra applicazione**,
mai standalone, e con copia verbatim della licenza tldraw.
In pratica: un'istanza personale self-hosted sul proprio Mac sta in una zona
grigia che solo tldraw può chiarire; qualsiasi cosa somigli a un servizio per
altri utenti quasi certamente richiede una licenza. Se il vincolo non ti va bene,
la strada è sostituire il canvas con un'alternativa realmente libera —
[Excalidraw](https://github.com/excalidraw/excalidraw) è MIT — tenendo il resto
di Slate, che dal canvas dipende solo attraverso il layer di sync e il renderer.
Questa è lettura del testo della licenza, non consulenza legale. Per un uso serio
chiedi a tldraw (sales@tldraw.com) o a un avvocato. Il quadro completo delle
dipendenze e delle loro licenze è in [`NOTICE.md`](NOTICE.md).
## Sviluppo
```bash
npm run dev:server # server con reload
npm run dev:web # PWA su :4500, proxy verso il server
node scripts/seed-demo.mjs demo # riempie una board di prova
node scripts/test-mcp.mjs demo # esercita l'MCP su stdio
node scripts/make-icons.mjs # rigenera le icone della PWA
```
This server cannot be deployed
Maintenance
ActivityNo data
ResponsivenessUnresponsive