SDK
Apps del Desk: declarar, ajustes, el supervisor y el sandbox
Convierte un programa en una app del Desk: declare, el esquema de ajustes, settings.get, request y onChange, la rotación, el supervisor y el sandbox de la comunidad.
Qué es una app
Una app es un programa que le dice a Miblo su nombre y los ajustes que necesita. La persona completa esos ajustes en Miblo Hub (pestaña Apps del Desk) o en la terminal, nunca en un archivo ni en una variable de entorno, y Miblo se encarga de ejecutar el programa. Las apps oficiales (miblo-apps) usan exactamente las rutas que usarás tú: no hay ninguna puerta privada.
Una app tiene tres partes:
declarar la app (nombre, categoría, promesa, ajustes, una vista previa);
leer sus ajustes y reaccionar cuando la persona cambia uno;
dibujar su pantalla con una página web, la forma estándar desde Miblo 1.45 (apps del Desk de página), o con
screen.card()yscreen.layer(), la forma antigua y obsoleta (la pantalla de apps), o solo enviar alertas (apps solo de alertas).
Las apps necesitan Miblo 1.30 o más nuevo en el computador y en el Miblo Desk (el SDK 2.x); leer las sesiones de IA de la persona (sessions) necesita Miblo 1.34.
Una app del Desk de página
En manifest.json, una app del Desk de página dice "api": 5, "display": "page" y "page": "page.html" (un archivo .html de la carpeta). run() devuelve { data } (JSON, de hasta 16 KB) en vez de una tarjeta, y Miblo le entrega esos datos a la página. Necesita Miblo 1.45 (1.46 para api 5 y los perfiles de render) y no puede ser solo de alertas (screen: false). La guía completa, con lo que una página no puede hacer, es Apps del Desk de página.
app()
const app = miblo.app({ id: 'weather', tool: 'weather' });app = miblo.app("weather", "weather")id: el id de la app, letras minúsculas, dígitos y-, hasta 48 caracteres. Usa el mismo quemiblo apps add --id.tool: el nombre de tu programa (las reglas deltoolde una sesión). Los ajustes de una app pertenecen al programa que la declaró, por ese nombre: otro programa recibenot_your_app. Una app instalada desde la tienda de apps del Desk pertenece al nombre del paquete.
declare()
await app.declare({
name: { pt: 'Clima', en: 'Weather' },
category: 'daily',
promise: { pt: 'Vai chover?', en: 'Will it rain?' },
settings: [
{ key: 'city', label: { pt: 'Cidade', en: 'City' }, type: 'text', required: true, max: 40 },
{ key: 'units', label: { pt: 'Unidade', en: 'Units' }, type: 'select', options: ['°C', '°F'], default: '°C' },
{ key: 'peek', label: { pt: 'Avisar antes da chuva', en: 'Heads-up before rain' }, type: 'toggle', default: true },
],
preview: { v: 1, title: 'Weather', icon: 'cloud', items: [{ t: 'big', value: '24°', label: 'Recife' }] },
});app.declare(
{"pt": "Clima", "en": "Weather"},
settings=[
{"key": "city", "label": {"pt": "Cidade", "en": "City"}, "type": "text", "required": True, "max": 40},
{"key": "units", "label": {"pt": "Unidade", "en": "Units"}, "type": "select", "options": ["°C", "°F"], "default": "°C"},
{"key": "peek", "label": {"pt": "Avisar antes da chuva", "en": "Heads-up before rain"}, "type": "toggle", "default": True},
],
category="daily",
promise={"pt": "Vai chover?", "en": "Will it rain?"},
preview={"v": 1, "title": "Weather", "icon": "cloud", "items": [{"t": "big", "value": "24°", "label": "Recife"}]},
)| Campo | Reglas |
|---|---|
name | Una cadena o { pt, en }, cortada a 24 caracteres. Obligatorio. |
settings | El esquema de ajustes (abajo), hasta 12. |
category | Un nombre corto para la pestaña Apps (a-z, 0-9, -, hasta 24). La tienda de apps del Desk usa computer, money, daily, news-dev, sport, delights y other. |
promise | Una línea, { pt, en } o una cadena, cortada a 100 caracteres: la pregunta que responde la app. |
preview | Una tarjeta con datos de ejemplo, que se muestra en la pestaña Apps antes de que la app corra. |
dwell | Los segundos que sugiere en la pantalla de apps (5 a 120); gana la elección de la persona. |
screen | false: una app solo de alertas, sin pantalla. |
alerts | De qué tratan sus alertas (una cadena o { pt, en }, 40 caracteres, que se muestra cuando se le pregunta a la persona), o sus tipos de alerta (apps solo de alertas). |
sessions | true: lee las sesiones de IA de la persona con app.sessions(), una vez que lo permite (Miblo 1.34). |
Devuelve { ok, id }. Declarar de nuevo reemplaza el esquema y conserva todos los valores guardados que sigan siendo válidos con él: declara en cada inicio. El texto más largo que su límite se corta, no se rechaza. Como máximo 64 apps por computador (too_many_apps).
El esquema de ajustes
Cada ajuste es { key, label, type, default?, required? } más los campos de su tipo.
type | Campos extra | Valor |
|---|---|---|
text | max (1 a 200, por defecto 100), suggest?, coords? | Una línea de texto |
number | min?, max? | Un número |
select | options: de 1 a 2000 opciones, cada una una cadena o { value, label }; max?; dependsOn? | Una de las opciones (una lista con max mayor que 1) |
toggle | (ninguno; por defecto false) | true o false |
url | (ninguno) | Una dirección http:// o https:// de hasta 512 caracteres |
time | (ninguno) | Una hora HH:MM (24 h); acepta 22:00, 7:30 o 22h30 y siempre guarda HH:MM. Miblo Hub muestra un selector de hora |
connect | provider, clientId, scopes, clientSecret?, help? | La cuenta conectada (ver Conectar cuentas) |
key: empieza con una letra minúscula, luego letras, dígitos y_, hasta 32 caracteres, única.label: una cadena o{ pt, en }, cortada a 40 caracteres.required:truehace que la app aparezca como "necesita configuración" mientras no tenga valor.default: debe ser en sí mismo un valor válido del tipo.group:"advanced"pliega el ajuste bajo "Avançado" (Avanzado) en Miblo Hub, para los que casi nadie cambia.Opciones de un
select: unvaluetiene hasta 64 caracteres (sin repetidos), unlabel(una cadena o{ pt, en }) se corta a 60; como máximo 6000 opciones en una app. Unmax(1 a 20) mayor que 1 lo convierte en elección múltiple: el valor es una lista de como máximomaxopciones distintas.dependsOn: '<key>'nombra un select de elección única anterior, y entoncesoptionses un mapa de sus valores a listas ({ "bra.1": ["flamengo", ...] }).Autocompletado (
text):suggest: 'city'o'free'recibe sugerencias que tu programa en ejecución responde conapp.settings.onSuggest(key, async (q) => [{ value, label }])(Pythonon_suggest);coords: true(solo consuggest: 'city') también acepta"lat,lon"desde "Usar mi ubicación" de Miblo Hub.placeholder(text,number,url,time): un ejemplo de qué escribir, una cadena o{ pt, en }de hasta 40 caracteres, que se muestra en el campo vacío. Donde exista una lista, declara unselect(carga la lista desde su fuente y guárdala 7 días); texto libre solo donde no pueda existir una lista, y entonces con el ejemplo.seed: un valor que tu programa trae de los ajustes de una versión anterior (un texto viejo dividido en ajustes nuevos). Miblo lo guarda una vez, solo donde la persona aún no tiene valor, nunca lo conserva en el esquema, e ignora uno que no sea válido. Un texto guardado cuyas partes son todas opciones de unselectde elección múltiple que lo reemplazó se traslada por sí solo.Sin secretos. Una app nunca le pide a la persona una clave, un token o una contraseña: no existe un tipo "password", y una clave o etiqueta que parezca un secreto (
token,password,senha,api_key,secret,chave,cookie,pin...) se rechaza (bad_request). Las apps leen fuentes públicas, sin cuenta; para la cuenta propia de la persona, usa un ajusteconnect(Conectar cuentas): Miblo la hace iniciar sesión con OAuth y tu app nunca ve una contraseña.
Leer los ajustes
settings.get()
const { values, missing, rev } = await app.settings.get();s = app.settings.get() # {"id", "values", "missing", "rev"}values: los valores por defecto combinados con lo que la persona guardó; un ajuste sin ninguno de los dos esnull.missing: los ajustes obligatorios que aún no tienen valor.rev: cambia cuando cambian los valores.
settings.request()
Le pregunta a la persona ahora, con un motivo (cortado a 80 caracteres).
if (missing.length) await app.settings.request(missing, 'To show the forecast');if s["missing"]:
app.settings.request(s["missing"], "To show the forecast")Lo que pasa: Miblo Hub muestra una tarjeta de configuración con el formulario y el motivo, el celular dice "Configure o app Weather no computador", y la pantalla de la app en el Miblo muestra "Configure no Miblo Hub" en el lugar de su tarjeta. La solicitud se mantiene hasta que los valores cambian. Devuelve { ok, needsSetup }. El motivo no puede empezar como uno de los avisos de Miblo. Comparte el límite de las acciones del Miblo Desk (say, focus, timer: 3 a la vez, luego 1 cada 10 s, para todos los programas del computador juntos): pregunta una vez, no en un bucle.
settings.onChange() y settings.wait()
const stop = app.settings.onChange((values) => redraw(values)); // the person saved: redraw now
// ...
stop();stop = app.settings.on_change(lambda values: redraw(values)) # on a background thread
# ...
stop()onChange(cb, { onError, retryMs })llama acb(values, answer)en cada cambio (no para los valores actuales) y devuelvestop(). Los errores van aonErrory sigue intentando cadaretryMs(por defecto 5000). En Python:on_change(cb, on_error=None, retry=5.0), concb(values).wait(rev?)es la pieza de debajo: espera a que los valores cambien respecto derev, como máximo 20 s, y devuelve{ values, missing, rev }. Como máximo 16 esperas a la vez (busy).
Los valores se quedan en este computador (<datos de Miblo>/apps/<id>.json, legible solo por tu usuario). Desde la terminal: miblo apps config <id> key=value.
Cuándo se muestra la pantalla de la app
La rotación: con varias apps, la pantalla de apps muestra una a la vez, cada una por su propio tiempo:
miblo apps dwell <id> <seconds>(5 a 120; el valor por defecto es lo que la app sugiere cuando se declara,dwell, si no 15 s). Un tiempo por app; no hay uno global.Cuándo:
miblo apps when idle(por defecto: solo cuando ninguna sesión de IA ha estado corriendo, esperando ni necesitando a la persona durante--idle-aftersegundos, 60 por defecto, 10 a 3600),rotation(siempre en la rotación),request(solo cuando una app pide la pantalla conscreen.request(), por su propio tiempo, mientras nada necesita a la persona) omanual(solo fijada o en un vistazo). La persona elige; una app no lo cambia.Fijar y echar un vistazo siguen funcionando con cualquier elección; pedir la pantalla funciona con todas menos
manual. Las alertas de Miblo siempre van primero.Los límites son por computador, compartidos por todos los programas y las apps oficiales activadas: tarjetas 2 por segundo (ráfagas de 5),
show1 cada 10 s, vistazos 1 cada 10 s para todos (y 1 cada 10 minutos por programa). Mientras un programa tiene su pantalla fijada (screen.show(true)), elanimatede otro programa recibeanim_takenconretryAfter.
El supervisor
miblo apps add "<command>" registra tu programa y miblo apps enable <id> lo mantiene corriendo: Miblo lo inicia con el token, lo reinicia cuando termina y guarda su salida (miblo apps logs <id>). El paso a paso está en Instalación.
| Comando | Qué hace |
|---|---|
miblo apps list [--json] | Todas las apps, encendidas o apagadas, y cuáles necesitan configuración |
miblo apps enable / disable <id> | La enciende o la apaga |
miblo apps show <id> | Estado, reinicios, comando o hosts |
miblo apps logs <id> | La salida del programa (y lo que bloqueó el sandbox) |
miblo apps config <id> [key=value ...] | Muestra o cambia los ajustes |
miblo apps dwell [<id>|sessions|pet [<seconds>|default]] | El tiempo de cada app en la pantalla (y el turno de las sesiones y de la mascota en la rotación) |
miblo apps order [<id>|sessions|pet <position>] | El orden de la rotación; --sessions-between on|off pone las sesiones entre dos apps |
miblo apps when [rotation|idle|request|manual] [--idle-after <seconds>] | Cuándo se muestran las apps |
miblo apps pin [<id> [always|idle|after <minutes>|window <HH-HH>]] / apps unpin | Mantiene una app en la pantalla (siempre, en reposo, tras N minutos sin sesiones de IA, o entre dos horas) |
miblo apps peek <id> | Su pantalla en el Miblo ahora, por 10 s |
miblo apps screen <id> on|off | Muestra su pantalla o no (apagada: corre solo para sus alertas) |
miblo apps alerts <id> [on|off|later] | Permite sus alertas, las rechaza, o vuelve a preguntar en 7 días (apps solo de alertas) |
miblo apps sessions <id> [on|off|later] | Le permite ver las sesiones de IA (o no) |
miblo apps connect|disconnect <id> <google|microsoft> | Conecta u olvida una cuenta |
miblo apps search [<words>] [--category <c>] | Busca en la tienda de apps del Desk |
miblo apps install <id> | Instala una app de la tienda de apps del Desk (firmada) |
miblo apps start | Inicia el puente y las apps que están encendidas (Miblo Hub lo hace al abrirse) |
miblo apps remove <id> | La apaga, le quita la pantalla y elimina sus ajustes |
miblo screen status / clear | Muestra o limpia la pantalla de apps |
Solo tú, en tu propia terminal: miblo apps add, permitir las alertas de una app (apps alerts <id> on) o sus sesiones (apps sessions <id> on) se niegan a correr dentro de un agente de IA (Claude Code, Codex...). Escríbelos tú mismo, o usa Miblo Hub.
Conectar cuentas
Una app puede leer los datos propios de Google o Microsoft de la persona (la Agenda oficial lee su calendario así) sin ver nunca una contraseña ni un refresh token: el puente de Miblo hace iniciar sesión a la persona, con OAuth 2.0 (Authorization Code + PKCE, como una aplicación de escritorio), y tu programa solo recibe tokens de acceso de corta vida.
await app.declare({ name: 'Agenda', settings: [
{ key: 'google', label: { pt: 'Conectar Google', en: 'Connect Google' }, type: 'connect', provider: 'google',
clientId: '<your desktop client id>.apps.googleusercontent.com', clientSecret: '<Google\'s installed-app secret>',
scopes: ['https://www.googleapis.com/auth/calendar.readonly'],
help: { pt: 'Os eventos aparecem no Miblo em até 30 s', en: 'Events show on Miblo within 30 s' } },
] });
const { url } = await app.connect.start('google'); // the sign-in address (the Miblo app has the button)
const { accessToken, expiresAt, account } = await app.connect.token('google');
await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', { headers: { authorization: `Bearer ${accessToken}` } });
await app.connect.off('google'); // disconnecturl = app.connect.start("google")["url"]
token = app.connect.token("google")["accessToken"] # MibloError "not_connected" until the person connects
app.connect.off("google")El ajuste
connectdice dónde iniciar sesión:provider(googleomicrosoft, un ajuste cada uno),clientId(tu cliente OAuth: un cliente "Desktop app" de Google o un cliente público de Azure),scopes(1 a 8), elclientSecretde Google cuando tiene uno (Google lo emite para clientes de escritorio y lo documenta como no confidencial; nunca para Microsoft) y una líneahelpopcional (80 caracteres, bajo el botón). Nunca es obligatorio y no tiene valor por defecto; su valor es la cuenta conectada (el correo),''hasta entonces, y solo lo escribe el puente. Asísettings.wait()despierta a tu programa en el momento en que la persona conecta o desconecta.connect.start(provider)devuelve{ provider, url }, la dirección de inicio de sesión. El puente espera exactamente una redirección dentro de 5 minutos en un puerto loopback propio, verifica sustate, intercambia el código con el verificador PKCE y verifica el id_token. El navegador dice "Conectado como ...". Como máximo 4 inicios de sesión a la vez (busy).connect.token(provider)devuelve{ provider, accessToken, expiresAt, account }, un token con al menos un minuto de vida, renovado cuando hace falta.not_connectedhasta que la persona conecta (y de nuevo si el proveedor revoca el acceso);unavailableconretryAftercuando no se puede llegar al proveedor: inténtalo más tarde, sigue conectada.Sin ajuste
connectpara ese proveedor:start,tokenyoffrespondenno_connect_setting(404).connect.startyconnect.offcomparten el límite de las acciones del Miblo Desk (3, luego 1 cada 10 s);tokenes una lectura.connect.off(provider)olvida la cuenta en este computador (miblo apps removetambién).Quién puede: solo el programa que declaró la app (
not_your_app), como con los ajustes.Dónde viven los tokens: el refresh token está sellado (AES-256-GCM, bajo una clave derivada de la clave del puente y ligada a la app y al proveedor) en
<datos de Miblo>/apps/<id>/connect-<provider>.json; los tokens de acceso quedan solo en la memoria del puente. Nada va a miblo.ai.Para la persona: la pestaña Apps del Desk de Miblo Hub muestra un botón "Conectar Google", luego "Conectado como ... · Desconectar". Desde una terminal:
miblo apps connect <id> googleymiblo apps disconnect <id> google.
Receta: suscriptores de YouTube
El número de suscriptores de un canal de YouTube, leído con la cuenta de Google de la propia persona (sin clave de API en la app):
En la consola de Google Cloud, crea un proyecto, activa la YouTube Data API v3, configura la pantalla de consentimiento de OAuth y crea un cliente OAuth de tipo Desktop app. Copia su client id y su client secret (Google documenta el secreto de un cliente de escritorio como no confidencial).
Declara un ajuste
connectcon el scope de solo lectura, y lee el conteo con el token de acceso que te entrega Miblo:
import { MibloStatus } from '@miblo/status';
const miblo = new MibloStatus();
const app = miblo.app({ id: 'youtube-subs', tool: 'YouTube' });
await app.declare({ name: 'YouTube', settings: [
{ key: 'youtube', label: { pt: 'Conectar YouTube', en: 'Connect YouTube' }, type: 'connect', provider: 'google',
clientId: '<your client id>.apps.googleusercontent.com', clientSecret: '<your client secret>',
scopes: ['https://www.googleapis.com/auth/youtube.readonly'] },
] });
const screen = miblo.screen({ tool: 'YouTube' });
for (;;) {
try {
const { accessToken } = await app.connect.token('google');
const r = await fetch('https://www.googleapis.com/youtube/v3/channels?part=statistics&mine=true',
{ headers: { authorization: `Bearer ${accessToken}` } });
if (!r.ok) throw new Error(`YouTube answered ${r.status}`);
const subs = Number((await r.json()).items?.[0]?.statistics?.subscriberCount ?? 0);
await screen.card({ v: 1, title: 'YouTube', items: [{ t: 'big', value: subs.toLocaleString('en-US'), label: 'subscribers' }] });
} catch (e) {
if (e.code === 'not_connected') { await app.settings.wait(); continue; } // wakes when the person connects (20 s at most)
console.error(e.code ?? e.message);
}
await new Promise((r) => setTimeout(r, 10 * 60_000)); // every 10 minutes
}Luego la persona presiona Conectar YouTube en la pestaña Apps del Desk de Miblo Hub (o ejecuta miblo apps connect youtube-subs google).
Cuota: cada proyecto de Google Cloud recibe 10.000 unidades al día.
channels.listyvideos.listcuestan 1 unidad por llamada;search.listcuesta 100, así que evítala. Una lectura cada 10 minutos son 144 unidades al día.
El sandbox de las apps de la comunidad
Una app de la tienda de apps del Desk puede llevar código escrito por otra persona. Llega a un computador solo después del análisis automático, la revisión de IA y la aprobación de una persona (Publicar en la tienda de apps del Desk), y corre en un sandbox:
Archivos: lee solo su propio paquete, su propia carpeta de estado y el runtime de Miblo; escribe solo en su carpeta de estado. Usa el modelo de permisos de Node (
--permission).Sin procesos ni código nativo: sin procesos hijos, worker threads, addons nativos ni WASI.
Módulos bloqueados:
http,https,http2,net,tls,dgram,dns,child_process,cluster,worker_threads,vm,module,wasi,inspector,repl,v8,trace_eventsysqlite. La app importa solo archivos de su propio paquete y los demás módulos integrados de Node; el código que escribe en su carpeta de estado no se puede importar.Red solo a través del
fetchde Miblo, y solo a los hosts declarados ennetwork: https en el puerto por defecto, las redirecciones se siguen bajo la misma regla, 10 s por llamada, 256 KB por respuesta, 30 llamadas por minuto.WebSocket,EventSourceyXMLHttpRequestya no están.El token del SDK sale del entorno antes de que la app cargue; solo el cliente lo conserva.
Todo lo bloqueado se vuelve una línea
blocked: ...enmiblo apps logs <id>, incluso cuando la app captura el error.La tienda de apps del Desk tiene la última palabra: en cada inicio y cada hora se verifica el paquete de nuevo; una app que la tienda de apps del Desk ya no sirve o que ocultó se apaga.
Actualizaciones (1.37.0): una versión más nueva cuyo acceso (hosts, sesiones, alertas) es el mismo se instala sola; una que pide más muestra "Actualización disponible" y espera a que la persona acepte (Miblo Hub, o
miblo apps install <id>en una terminal).Memoria y CPU (1.37.0): el heap de JavaScript de la app está limitado a 128 MB, y una que mantiene un núcleo de CPU ocupado durante 2 minutos se detiene (macOS y Linux); ambos cuentan como una falla y se inicia de nuevo más tarde.
Necesita Node 22.15+, 23.5+ o 24+. Miblo elige uno por sí solo: el propio Node del puente, si no
MIBLO_NODE(la ruta a unnode), si no uno en elPATHo en los lugares habituales (Homebrew, nvm, fnm, Volta...). Sin ninguno, la app no corre (estadounsupported, con el motivo en su log).
Encabezados: el
fetchde Miblo envía lo que la app le da; los helpersctx.fetchJson/fetchTextaceptan soloauthorization,accept,accept-language,if-none-matcheif-modified-since(Publicar).Errores y reintentos: cada falla es una línea en el log con el motivo (el campo de una tarjeta rechazada, el archivo y la línea del error, el estado HTTP con un extracto sin secretos), y la app lo intenta de nuevo en 5 s, 10, 20..., nunca más allá de su propio
intervalMs(al menos 10 s después de un 4xx), para no gastar la cuota de una API.
Lo que el sandbox no garantiza: la red la sostienen el fetch reemplazado y el control de módulos, no el sistema operativo, y una app puede usar CPU y memoria y enviar a sus hosts lo que Node le da a cualquier programa (la hora, la configuración regional, el nombre del computador). Por eso una persona revisa primero cada versión.