CLI

Partners — un agente para cada uno de tus clientes

Con ghosty partner le das un asistente a cada negocio que usa tu producto — un agente compartido que sólo usa tu MCP, memoria por cliente y credenciales para tu servidor.

Actualizado 2026-10-07

ghosty partner es para cuando tu producto le quiere dar un asistente a cada uno de sus clientes. Tú tienes un espacio de partner; cada negocio que usa tu producto es un tenant (lo identificas con tu propio externalId) y cada persona que le escribe a ese negocio es un usuario final (groupId).

  • Hay un solo agente compartido, en modo solo-MCP: no tiene máquina ni disco; sus únicas herramientas son las de tus servidores MCP por HTTPS.
  • Cada turno lleva un token de corta vida que Ghosty le pasa a tu MCP. Nunca va al prompt, a la memoria ni a los registros.
  • La memoria es por tenant y usuario final: un negocio nunca ve la de otro.

Tu espacio de partner

Estos comandos los corre la dueña del espacio, con su sesión (ghosty login).

bash
ghosty partner ls                                  # tus espacios de partnerghosty partner enable <espacio>                    # convierte uno de tus espacios en espacio de partnerghosty partner credentials ls                      # credenciales del servidor (sin secretos)ghosty partner credentials create --label produccion   # emite una: el secreto sale UNA vezghosty partner credentials create --label produccion --out-env .env.partner   # sin pasar por la terminal (archivo 600)ghosty partner credentials create --label produccion --format env | fly secrets importghosty partner credentials rotate gpk_… --grace 60     # secreto nuevo, mismo keyIdghosty partner credentials revoke gpk_…            # la revoca (idempotente)ghosty partner audit                               # quién hizo qué y cuándo
  • create imprime GHOSTY_PARTNER_KEY=gpk_… y GHOSTY_PARTNER_KEY_SECRET=gps_…. Guárdalas en tu servidor en ese momento: el secreto no se vuelve a enseñar. Para que no salga en pantalla: --out-env RUTA (lo escribe con permisos 600) o --format env (sólo las dos líneas, para mandarlas a otro comando). rotate acepta lo mismo.

--out-env antes se llamaba --env-file; Node 22 se come ese nombre como flag suyo (node: ./.env.partner: not found). El viejo sigue como alias.

  • rotate --grace MIN deja válido el secreto anterior esos minutos (default 60) para que cambies tu servidor sin cortes.
  • creds es alias de credentials; revoke también se escribe rm.
  • Si tienes más de un espacio de partner, elige cuál con --workspace <slug> en cualquier comando.

El agente compartido

bash
ghosty partner agent get                           # la plantilla y su versiónghosty partner agent apply --file agent.json       # la crea o la actualiza (idempotente)ghosty partner agent apply --file agent.json --prompt-file prompt.md   # el prompt en su propio archivo (gana al del JSON; tope 60,000 caracteres)ghosty partner agent apply --file admin.json --agent nik-admin         # OTRO agente del mismo partner (slug a-z, 0-9, -)ghosty partner agent get --agent nik-admin

Un partner puede tener varios agentes: el de siempre (sin --agent) y otros con slug (por ejemplo uno para la dueña del negocio y otro público). Cada uno tiene su plantilla y sus versiones; con slug, apply publica directo. Por API: PUT /api/v2/partner/agent con "slug" en el body, GET …/agent?slug=, y "agent": "<slug>" en POST /api/v2/partner/turns. La memoria y el consumo de un negocio cruzan todos sus agentes (tenants rm borra la de todos).

maxSteps (1-12) es cuántas vueltas modelo↔herramientas da un turno; si en la última el modelo sigue pidiendo herramientas, se le pide una respuesta final sin ellas. Si tu MCP manda instructions en su initialize (p. ej. cómo usar un catálogo con discover_tools/run_tool), entran al prompt (hasta 6,000 caracteres).

maxTokens (256-8000, default 2000) es el tope de cada respuesta del modelo y thinking (true/false, default false) prende el razonamiento de DeepSeek: más lento y caro, útil para un asistente de varios pasos.

Con "engine": "claude" (p. ej. "model": "claude-sonnet-5-5") el agente corre con caja (claude-worker) en vez de solo-MCP: el mismo contrato de turns y el mismo SSE, pero cada conversación va encerrada (un negocio no ve archivos ni historial de otro), no tiene las herramientas de la cuenta del espacio y el token del turno llega a tu MCP por un proxy de Ghosty (nunca entra a la caja). Cada servidor MCP debe llevar ${turn.token} en una cabecera. La llave de Claude es la del espacio (o la de casa si el espacio la tiene encendida).

Robustez del turno. Si tu MCP no responde al arrancar, se reintenta una vez y, si sigue caído, el turno termina con error (no contesta sin herramientas). Un 429/5xx del proveedor se reintenta antes del primer chunk. Si una herramienta contesta 401 a media vuelta (token vencido) y el turno nació en Ghosty (WhatsApp) o trae agent, se canjea un token nuevo en /api/nik/turn-token y la llamada se repite una vez. Las herramientas de lectura (get_, list_, search_, find_) de un mismo paso corren a la vez, y los ids que vio cada turno quedan en su memoria para el siguiente.

Topes. Además del tope por partner hay uno por negocio (8 turnos a la vez) y uno por conversación (20 mensajes por minuto); al pasarse, turns contesta 429.

agent.json lleva sólo lo que quieras cambiar:

json
{  "name": "Nik",  "engine": "deepseek",  "prompt": "Eres el asistente de {negocio}…",  "mcp": [    { "name": "agenda", "url": "https://api.tu-producto.com/mcp", "headers": { "Authorization": "Bearer ${turn.token}" } }  ]}

Sólo servidores MCP por HTTPS. En los headers no van llaves fijas: sólo los placeholders ${turn.token} y ${tenant.externalId}. Si nada cambió, no se crea una versión nueva.

Tenants

bash
ghosty partner tenants lsghosty partner tenants get org_123ghosty partner tenants set org_123 --config '{"name":"Spa Luna","timezone":"America/Mexico_City"}'ghosty partner tenants rm org_123                  # borra su configuración Y su memoria

Un tenant es sólo configuración (name, tone, timezone, locale, notes): no tiene máquina propia. El primer turno de un externalId nuevo también lo da de alta.

Consumo por negocio

bash
ghosty partner usage                                   # últimos 30 días, todos los negociosghosty partner usage --tenant org_123 --from 2026-10-01 --to 2026-10-07

Por API, firmado con la credencial (GET con cuerpo vacío): GET /api/v2/partner/usage?externalId=&from=&to= → por negocio turns, tokens, costUsd, el desglose por día (días de CDMX) y errors por razón (mcp_down, provider_429, token, timeout, cancelado, reinicio…). from/to en YYYY-MM-DD o ISO; máximo 92 días.

Probar un turno

bash
ghosty partner try org_123 "¿qué citas tengo hoy?"ghosty partner try org_123 "¿y mañana?" --group cliente-42 --turn-token <token-de-corta-vida>
  • --agent <slug> prueba otro agente del partner (default: el de siempre).
  • --group es el usuario final (default cli); cada uno tiene su propia conversación.
  • --turn-token es el token que Ghosty le pasará a tu MCP como ${turn.token}. Aquí --token también funciona y significa lo mismo (no es tu sesión).
  • --verbose enseña los argumentos de cada tool (sin llaves con cara de secreto) y cuánto tardó: ↳ denik__buscar_citas {"fecha":"2026-10-01"} · ✓ denik__buscar_citas 340 ms.
  • Con --json sale una línea por evento del turno (chunk, tool con args y ms, error…).

Probar como tu servidor

Con la credencial en el entorno, el CLI firma cada petición igual que lo hará tu backend (HMAC de ${ts}.${keyId}.${body}):

bash
export GHOSTY_PARTNER_KEY=gpk_…export GHOSTY_PARTNER_KEY_SECRET=gps_…ghosty partner agent getghosty partner try org_123 "hola"

Grupos de WhatsApp de un agente

Un agente tuyo (p. ej. nik-admin) puede tener su propio número de WhatsApp (pareado con ghosty channels wa …). Cada grupo donde está se liga a un negocio y desde ahí contesta todo con las herramientas de ese negocio:

bash
ghosty partner wa groups ls --agent nik-admin                       # grupos, a qué negocio va cada uno, prendido/apagadoghosty partner wa groups link 1203…@g.us --tenant org_123 --agent nik-admin   # liga un grupo donde ya está el número y lo prendeghosty partner wa groups create --tenant org_123 --name "Nik · Spa Luna" --agent nik-admin   # grupo nuevo + enlace de invitación (máx. 10 por hora)ghosty partner wa groups unlink 1203…@g.us                          # lo desliga y lo apaga; el número sigue en el grupo

Por API: GET|POST /api/v2/partner/wa/groups y PUT|DELETE /api/v2/partner/wa/groups/{jid}. En cada turno Ghosty pide el token a tu servidor con {"externalId","aud":"<slug del agente>","groupId":"<jid>"}; tu respuesta puede traer además appendSystemPrompt (≤ 4,000) y config (name, timezone…). Si contestas 403/404, ese mensaje no se responde (queda en ghosty turns ls <id> --skipped). El nombre de quien escribe va en el texto (María: …); los adjuntos llegan como URL firmada y las notas de voz transcritas.

WhatsApp Business de un negocio

El número de WhatsApp Business de uno de tus negocios lo puede contestar un agente tuyo (p. ej. nik-public). Si tú das de alta el número en Formmy, pide el link a Ghosty y dáselo a Formmy; no viaja ninguna llave tuya:

bash
ghosty partner waba links create --tenant org_123 --agent nik-public   # imprime url y secreto (el secreto UNA vez); nace apagadoghosty partner waba links set <id> --integration <id-formmy> --phone 5215550001111 --on   # lo que devolvió Formmy, y prenderloghosty partner waba links ls --tenant org_123ghosty partner waba links rm <id>                                      # apagar; el historial queda

Formmy recibe url como externalAgentUrl y secret como externalAgentSecret, y postea cada mensaje a ${url}/message con Authorization: Bearer <secret>. Por API: GET|POST /api/v2/partner/waba/links y PUT|DELETE /api/v2/partner/waba/links/{id}.

Si el número ya está en Formmy y lo mueves tú desde Ghosty:

bash
ghosty partner agent get --agent nik-public        # su idghosty channels whatsapp link <id> --integration <id-formmy> --answer on --partner-tenant org_123 --partner-agent nik-public --dry-run

En cada mensaje Ghosty le pide a tu servidor un token de corta vida (POST /api/nik/turn-token firmado con tu credencial: X-Ghosty-Key, X-Ghosty-Ts, X-Ghosty-Sig = HMAC de ${ts}.${keyId}.${body}; body {"externalId","aud","groupId"}) y corre el turno con él. El groupId es wa:<teléfono>. Si tu servidor no da el token, el mensaje queda registrado sin respuesta (ghosty turns ls <id> --skipped).

GHOSTY_PARTNER_SECRET también funciona (es el nombre de antes), pero no lo confundas: el servidor de Ghosty usa ese mismo nombre para otro secreto suyo. Usa GHOSTY_PARTNER_KEY_SECRET.

Así funcionan agent, tenants y try. ls, enable, credentials y audit siempre usan tu sesión: una credencial no puede fabricar otras.

Un agente con caja para tus usuarios

Si en vez del agente compartido solo-MCP quieres un agente normal (Claude con caja, shell y archivos) que atienda a cada usuario de tu producto con SU llave:

bash
ghosty spaces create miempresa && ghosty partner enable miempresaghosty partner credentials create --label produccion    # gpk_ + gps_ghosty mcp set <agente> --file servers.json             # "Authorization": "Bearer ${turn.token}"

Cada turno: POST /api/v2/fleet-agents/<agente>/message-stream con {groupId, text, turnToken} (un groupId por usuario final; turnToken de 8 a 4096 caracteres, sin espacios) y las cabeceras X-Ghosty-Key: gpk_…, X-Ghosty-Ts (epoch en segundos) y X-Ghosty-Sig = HMAC-SHA256 en hex, con el gps_…, de ${ts}.${keyId}.${cuerpo}. La respuesta es SSE (chunk, tool, done, error). El token sólo llega a tu MCP: nunca entra a la caja ni se guarda. Sin turnToken, el turno falla con error. La credencial alcanza sólo los agentes normales de tu espacio.

Landings para tus negocios

Tu servidor puede pedirle a Ghosty la landing de un negocio con sus datos reales, sección por sección, para pintarla en tu propio editor. Misma firma que los turnos (X-Ghosty-Key, X-Ghosty-Ts, X-Ghosty-Sig). Nada se guarda ni se publica en Ghosty: el HTML es tuyo.

POST /api/v2/partner/pages/build (SSE):

json
{  "externalId": "org_123",  "brief": "Que se note que atendemos sin prisa",  "facts": {    "name": "Estética Luna", "description": "Faciales y depilación", "category": "estética",    "city": "Puebla", "phone": "222 123 4567", "whatsapp": "522221234567",    "address": "5 de Mayo 210, Centro", "hours": "Lun a sáb 10:00–19:00",    "services": [{ "name": "Facial profundo", "price": 650, "duration": "60 min", "url": "https://tu.app/luna/facial" }]  },  "photos": [{ "url": "https://cdn.tu.app/luna/fachada.jpg", "alt": "Fachada" }],  "theme": { "primary": "#be185d", "mode": "light" },  "sectionsWanted": 6,  "dialect": "denik",  "review": false}

Eventos, cada uno data: {…}:

typeQué trae
plantitle, theme (colores ya resueltos, fuentes, radio) y sections: [{id, kind, title}] en orden
sectionid, index, html: una por sección, conforme sale
section-updatela misma sección otra vez, ya con fotos e íconos (o corregida por la revisión)
reviewsólo con review: true: problems, fixes y skipped
donesections: [{id, kind, title, html}] finales, theme, usage {input, output}, seconds
errormessage
  • Datos reales: no se inventan cifras, años, reseñas, testimonios ni servicios. Sin servicios no hay sección de servicios; testimonios, cifras, equipo y paquetes nunca se piden. Cada servicio con url lleva su botón «Agendar» a esa liga.
  • Fotos: las tuyas llenan los huecos primero (en orden); las que falten salen de bancos libres, elegidas con visión. Las fotos CC llevan data-credit: pinta el crédito al pie.
  • dialect: gs (default) usa los tokens del constructor de Ghosty; denik deja clases que hornea bakeTailwindHtml y que siguen tu brandkit (sin surface-deep, fuentes como style, fotos https sin crossorigin, sin data-id). Las dos conservan data-animate.
  • Topes: 4 armadas a la vez por partner y 20 por negocio en 24 h (429). El gasto se registra en el consumo de tu agente, por externalId.

POST /api/v2/partner/pages/review con {html, viewport?, theme?, externalId?, dialect?} la abre en un navegador real (escritorio 1280 y celular 390) y devuelve {problems: [{sectionId, kind, detail, viewport, nodeId}], fixes, sections, skipped, seconds}; kind ∈ broken-image, overflow, clipped, overlap, contrast, contrast-unsure, dead-link. sections son sólo las que la revisión corrigió. Si no se pudo renderizar contesta 503, nunca una lista vacía.

Animación: copia (3 KB, CSS + JS) una vez antes de </body> de tu página publicada. Sin él la página se ve completa, sólo quieta.