ocremote
v2.1.2
Published
Control opencode on your computer from the OpenCode Remote iOS app: supervisor, pairing QR, relay and tunnel transports
Downloads
733
Maintainers
Readme
oc-remote
Plataformas. El companion requiere Node >= 22.13 y soporta macOS, Linux y Windows:
npx ocremote --pairen cualquiera de los tres. El proceso se mantiene en primer plano y la terminal debe seguir abierta. En macOS, mDNS está activo por defecto (--no-mdnslo desactiva; en otros sistemas no se intenta). Los túneles (--tunnel cloudflare) descargancloudflaredpara la plataforma correcta. Windows está corregido en código (rutas,.exe,;en PATH, cloudflared .exe) pero no verificado en una máquina Windows real.
Primer arranque (2.1.1)
npm install -g opencode-ai
opencode auth login
npx ocremote --pair--pair selecciona el relay alojado por defecto y no necesita VPN ni descubrimiento
de red local. El proxy queda en loopback. El QR sólo se muestra cuando la invitación
está registrada: abre la app, escanéalo y conserva la terminal abierta mientras la
usas. Caduca en 15 minutos y es de un solo uso. Para mostrar otro QR de una instancia
en marcha: npx ocremote pair.
La nueva app comprueba la firma del ordenador contra la clave fijada por el QR. Es necesario actualizar app, companion y relay juntos y volver a emparejar las entradas antiguas que no tengan esa clave.
Las secciones de proxy/túneles de abajo describen también el modo directo opcional.
Con --relay o --pair, el valor predeterminado de --host es 127.0.0.1, el túnel
queda desactivado y Bonjour no se anuncia salvo petición explícita.
Proceso Node sin dependencias externas (solo APIs nativas de Node 22+) que convierte un Mac en un host de opencode accesible desde la app iOS OpenCode Remote, con autenticacion por token, pairing por QR y notificaciones push opcionales.
iOS ──token──► 0.0.0.0:4190 ┌──────────────┐
│ oc-remote │ inyecta Basic interno
│ (proxy+auth) │──────────────► 127.0.0.1:4191
└──────────────┘ opencode serve
QR / mDNS / ntfy (solo loopback)- El companion genera un token propio (32 hex) y lo guarda en
~/.config/oc-remote/config.json(0600). Solo rota con--new-token. - Arranca
opencode serveen127.0.0.1:<opencode-port>con un password interno aleatorio distinto (OPENCODE_SERVER_PASSWORD) que se regenera en cada arranque y nunca sale del Mac: el companion lo inyecta en cada peticion upstream. La red solo ve el token del companion. - Escucha en
0.0.0.0:<port>y hace proxy streaming (SSE sin buffering) ahttp://127.0.0.1:<opencode-port>.
Requisitos
- Node 22.13 o superior (
node --version). - El binario
opencode(por defecto~/.opencode/bin/opencode). - Opcional:
tailscale(acceso remoto),ntfy(push, no requiere instalar nada en el Mac),dns-sd(viene con macOS).
Arranque manual
node companion/oc-remote.mjs --dir /ruta/al/proyectoSalida (stderr): hostname, URLs candidatas (LAN, Tailscale, --public-url),
token, y el QR ASCII con el deep link de pairing (--print-qr esta activo
por defecto; --no-print-qr lo desactiva).
Flags:
| Flag | Default | Descripcion |
|---|---|---|
| --dir <path> | cwd | Directorio de proyecto que usa opencode (debe existir) |
| --port <n> | 4190 | Puerto publico del companion |
| --opencode-port <n> | 4191 | Puerto loopback del opencode serve interno |
| --host <ip> | 0.0.0.0 | Interfaz de escucha |
| --token <str> | — | Fija el token (se persiste en config.json) |
| --new-token | — | Rota el token y lo persiste |
| --tunnel <mode> | auto | auto | cloudflare | tailscale | none (ver Acceso remoto) |
| --ntfy <topic\|url> | — | Activa notificaciones push |
| --public-url <url> | — | URL publica a anunciar (tu propio tunel; salta --tunnel) |
| --no-auth | — | Desactiva auth (peligroso, avisa por stderr) |
| --opencode <path> | ~/.opencode/bin/opencode | Binario de opencode |
| --no-mdns | mDNS activo | No publicar _ocremote._tcp por Bonjour |
| --print-qr / --no-print-qr | on | Imprimir el QR al arrancar |
| --help | | Ayuda |
Variables de entorno: OCREMOTE_CONFIG_DIR (cambia ~/.config/oc-remote),
OPENCODE_BIN (alternativa a --opencode), OCREMOTE_CLOUDFLARED (ruta a un
binario cloudflared propio), OCREMOTE_TAILSCALE (ruta al CLI tailscale; útil
para tests).
Subcomandos
| Comando | Qué hace |
|---|---|
| oc-remote doctor | Verifica toda la cadena: binario de opencode, token, puerto, auth, upstream, LAN, Tailscale/Funnel, endpoint público y ntfy. Salida ok/FAIL con el arreglo de cada fallo |
| oc-remote status | Config, endpoint actual (de endpoint.json) y prueba de auth |
| oc-remote funnel on\|off\|status | Activa/desactiva el Funnel de Tailscale (la primera vez imprime el link de aprobación). El proceso en primer plano lo adopta automáticamente |
| oc-remote version | Versión |
Push (APNs) — /_ocremote/push
La app envía su token de APNs al companion con POST /_ocremote/push
({"token":"…","pairID":"…"}, autenticado con el token del companion). El
companion lo reenvía al relay por el canal de control, que es quien despierta el
teléfono cuando la app está cerrada. Si el relay no está configurado con APNs,
el endpoint sigue respondiendo 200 y no pasa nada más.
Beacons de endpoint (auto-reparación)
Con --ntfy <topic|url>, además de las notificaciones push el companion publica
un beacon con el endpoint actual: al arrancar, cada vez que cambia la URL
pública, y como heartbeat cada 5 minutos. Formato (mensaje ntfy con tag
ocremote-endpoint):
{"type":"endpoint","id":"1887884bfde3","name":"MacBook","version":"1.2.0",
"url":"https://mac.tailnet.ts.net","urls":[{"url":"...","label":"tunnel"}],
"provider":"funnel","auth":true,"port":4190,"ts":1789547880425}ides un hash del token (nunca el token): sirve para que la app ignore beacons de otros Macs.- La app guarda esa lista de
urlsy prueba los candidatos en orden cuando la URL actual falla; si todos fallan, consulta el último beacon del topic (/json?poll=1) y se actualiza sola. El QR incluye el topic (ntfy=...) y la lista inicial (urls=...), así que un solo escaneo lo configura todo. - El beacon también se escribe en
~/.config/oc-remote/endpoint.json(0600) paraoc-remote status/doctor.
Watchdog
opencode servese relanza solo si muere (backoff exponencial), y un chequeo cada 30 s vía/global/healthlo reinicia si queda colgado (3 fallos).cloudflaredse relanza solo; el Funnel se re-aplica si deja de servir.- El puerto del companion no cambia entre reinicios: la app solo necesita el endpoint actual, no un puerto nuevo.
- Con
auto, el companion se adapta en caliente: si aparece un Funnel activo pasa a la URL estable, y si desaparece vuelve al quick tunnel o a LAN/Tailscale.
Pairing
- QR del terminal: contiene el deep link
ocremote://pair?v=1&name=<hostname>&url=http%3A%2F%2F<ip>%3A<port>&token=<token>. - Pagina de pairing:
http://127.0.0.1:4190/_ocremote/pair(solo loopback sin token; desde otra maquina requiere?token=<token>). Muestra el QR como PNG, la URL, el token y botones de copiar. - Manual: URL
http://<ip>:<port>+ token.
La app envia Authorization: Basic base64("opencode:"+token) (llama "token" a
la password) y para SSE puede usar ?auth_token=<base64(opencode:token)>.
El companion acepta ademas Authorization: Bearer <token> y ?token=<token>.
token/auth_token se eliminan de la query antes de reenviar al upstream.
Payload del QR
ocremote://pair?v=1&name=<hostname>&url=http%3A%2F%2F<ip>%3A<port>&token=<token>| Parametro | Significado |
|---|---|
| v | Version del esquema de pairing (1) |
| name | os.hostname() del Mac, URL-encoded |
| url | Base URL a la que conectarse, URL-encoded |
| token | Token del companion, URL-encoded |
Endpoints propios
| Endpoint | Auth | Respuesta |
|---|---|---|
| GET /_ocremote/health | no | {"ok":true} (monitor local) |
| GET /_ocremote/status | token | {name, version, directory, uptime, port, opencodePort, clients, tunnel} |
| GET /_ocremote/pair | loopback o token | Pagina HTML con QR PNG, URL y token |
Todo lo demas se proxya a opencode. Los Upgrade/WebSocket responden
501 (PTY fuera de alcance). Metodos fuera de
GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS responden 405. Errores de upstream
responden 502 JSON.
Acceso remoto
--tunnel decide como se anuncia la URL (y cual va dentro del QR):
| Modo | Que hace |
|---|---|
| auto (default) | Si cloudflared ya esta instalado lo usa; si no, Tailscale si esta conectado; si no, solo LAN con un aviso |
| cloudflare | Quick tunnel: descarga cloudflared la primera vez (a ~/.config/oc-remote/bin/), arranca el tunel y publica la URL HTTPS |
| tailscale | Anuncia la IP de Tailscale (URL estable) |
| none | Solo LAN |
Cloudflare quick tunnel (cero configuracion)
oc-remote --dir ~/Projects/my-app --tunnel cloudflare
# [oc-remote] cloudflare quick tunnel ready: https://calm-river-1234.trycloudflare.comNo necesita cuenta. La URL cambia en cada reinicio: para una URL fija usa
Tailscale o un tunel con nombre propio. Si cloudflared muere, el companion lo
relanza con backoff; cuando hay URL nueva reimprime el QR.
Tailscale (URL estable)
Es una VPN mesh: el trafico va cifrado y solo tus dispositivos entran, sin abrir puertos ni exponer nada a Internet.
brew install --cask tailscale # o la app de la Mac App Store
tailscale up # login
tailscale ip -4 # 100.x.y.zCon --tunnel tailscale (o auto sin cloudflared) el companion imprime
http://100.x.y.z:4190. Desde el iPhone con Tailscale activo:
- por IP:
http://100.x.y.z:4190 - por MagicDNS (mas comodo):
http://<nombre-maquina>.<tailnet>.ts.net:4190
Opcional: --host <tailscale-ip> para escuchar solo en la VPN. La primera vez
macOS puede pedir permiso de firewall para node.
Tunel propio
cloudflared tunnel --url http://127.0.0.1:4190
# imprime una URL https://xxxx.trycloudflare.com
oc-remote --public-url https://xxxx.trycloudflare.comCon --public-url esa URL se usa en el QR, en los logs y en el click de
ntfy (y se salta --tunnel). El tunel expone el companion a Internet: el token
es la unica barrera, con rate limit de 20 intentos fallidos por minuto y IP
(respuesta 429 too_many_requests). Prefiere Tailscale para uso diario.
ntfy (opcional)
node companion/oc-remote.mjs --ntfy ocremote-raul-a8f3k2permission.asked→opencode: permission required(priority 4, tagswarning, click a la URL).session.status busyseguido desession.idle→opencode: task finished.- Se abre una conexion SSE propia a
/eventcon el Basic interno; si falla, se reintenta cada 5 s y nunca afecta al proxy. - Acepta un topic de ntfy.sh o una URL completa (self-hosted):
--ntfy https://ntfy.midominio.dev/mi-topic. - Los topics de ntfy.sh son publicos: usa uno largo y aleatorio.
Seguridad
- Token de 32 hex comparado con
crypto.timingSafeEqual; el config se escribe 0600 (directorio 0700). - El opencode interno escucha solo en
127.0.0.1con password aleatorio de 32 hex regenerado en cada arranque; la LAN no puede hablar con el directamente. - Sin TLS: no expongas el puerto a Internet directamente. Usa Tailscale (recomendado) o un tunel con HTTPS.
--no-authdeja el control de opencode al alcance de cualquiera en la red: usalo solo en loopback.GET /config/providersy/provider.envexponen API keys de OpenCode a quien tenga el token: tratalo como un secreto.- La pagina
/_ocremote/pairsolo se sirve sin token a clientes loopback.
Troubleshooting
port 4190 is already in use: cambia--port(o--opencode-port). Comprueba conlsof -nP -iTCP:4190 -sTCP:LISTEN.401: token distinto (rota con--new-tokeny vuelve a emparejar), o el cliente mandaBearer/Basic mal formado. Pruebacurl -H 'Authorization: Bearer <token>' http://127.0.0.1:4190/_ocremote/status.opencode binary not found: pasa--opencode /ruta/al/opencode.opencode did not become healthy within 30s: mira los logs con prefijo[opencode]en stderr; suele ser un puerto ocupado o falta de login.- El child muere: el companion lo relanza con backoff exponencial (1 s → 15 s).
- mDNS: anuncio Bonjour
_ocremote._tcp.localviadns-sd -R(el responder del sistema). Comprueba condns-sd -B _ocremote._tcp local; desactiva con--no-mdnssi no lo quieres. Sidns-sdno existe, solo avisa. node --test companion/test/: en Node ≤ 22.13 el runner no expande directorios, por esocompanion/test/package.jsonapunta aindex.mjs, que importa la suite. Tambien puedes usarnode --test companion/test/proxy.test.mjs.
Limitaciones
- Sin WebSocket/PTY (501), sin TLS, sin rate limiting, sin HTTP/2.
- Los clientes SSE se cuentan en
status.clientsmientras la conexion sigue abierta. - mDNS anuncia el servicio del companion (
_ocremote._tcp), no el de opencode: el--mdnsde opencode no se usa porque anunciaria el puerto interno.
