Personae MCP
Personae permite que los agentes de IA usen MCP para controlar de forma segura una identidad de navegador específica ya abierta y aislada por cuenta, sin mezclar sesiones ni lanzar un navegador separado.
Documentación
Personae
Un navegador multi-identidad · cada identidad es una partición aislada, cada ventana es controlable por agentes
Incluye navegador de agente y un servidor MCP — no se necesita Node ni instalación de CLI
English · 中文
Un navegador de escritorio basado en Electron que combina dos cosas: aislamiento multi-cuenta y permitir que agentes de IA controlen el navegador.
- Cada identidad de navegador es su propia partición de Chromium
persist:— las cookies, localStorage y el estado de inicio de sesión están completamente aislados, por lo que puedes estar conectado al mismo sitio con varias cuentas a la vez. - Cada identidad es una ventana independiente con botones de atrás/adelante y una barra de direcciones. Se comporta como un navegador normal.
- El binario agent-browser está incluido, y esas ventanas se exponen a través de MCP, para que Codex / Claude Code puedan controlar la página de una identidad específica.
Los usuarios no necesitan preinstalar Node, Chrome for Testing, ni ningún CLI.
Registro MCP
Personae está publicado en el Registro MCP oficial
como io.github.leek-emperor/personae después de su primera versión habilitada para el Registro.
El paquete del Registro es un pequeño lanzador stdio
para una aplicación de escritorio Personae instalada por separado y en ejecución — no
reemplaza la aplicación del navegador ni crea un navegador en la nube.
Después de instalar y lanzar Personae, un cliente que admita paquetes MCP npm puede ejecutar:
npx -y @leek-emperor/personae-mcp
El lanzador descubre Personae a través de su puente local y mantiene cada llamada de herramienta limitada a la identidad nombrada en la solicitud. No usa ninguna clave API y no abre ningún listener de red público. El control Configure Codex de la aplicación sigue siendo la ruta de configuración recomendada porque usa el runtime de Electron incluido y no requiere instalación de Node.
El problema que resuelve
Las herramientas existentes de automatización de navegador asumen que el agente lanza el navegador. Si tu producto es un cliente de navegador, es al revés: las ventanas ya existen y pertenecen a diferentes identidades de cuenta. Lo que el agente necesita es conectarse sin cruzar identidades.
El enfoque aquí:
- La aplicación abre un puerto CDP al inicio (asignado por el sistema, solo loopback).
- Cuando se abre una ventana de identidad, el proceso principal resuelve el targetId autoritativo de su página de contenido mediante
Target.getTargetInfo. - Un puente local expone el mapeo
identity → targetId. - El servidor MCP usa ese targetId directamente como referencia de pestaña — un golpe de un solo paso.
Así que snapshot(identity: "Account A") y snapshot(identity: "Account B") siempre aterrizan en la ventana correcta, incluso cuando ambas tienen el mismo sitio abierto con títulos y URLs idénticos.
Inicio rápido
Requiere Node ≥ 20 y pnpm (solo para desarrollo; la aplicación empaquetada no depende de ninguno).
agent-browser debe ser ≥ 0.36. Por debajo de eso, tab list --json no emite targetId,
que es lo único que vincula una identidad a su ventana — cada herramienta fallaría al encontrar su
objetivo. La versión en package.json ya está fijada; no la degraden.
pnpm install
pnpm bundle:ab # copy the agent-browser binary and core skill into resources/
pnpm dev
Agrega una o dos identidades de navegador en la interfaz, luego haz clic en una identidad para abrir su ventana.
La interfaz está en inglés por defecto. Usa el interruptor EN / 中 en la esquina superior derecha para cambiar al chino; la elección se recuerda y también se aplica a la barra de herramientas dentro de cada ventana de identidad y al prompt del agente que copias.
Pruebas
Las decisiones complicadas y mayormente no documentadas viven en módulos puros pequeños para que puedan ser
aseguradas por pruebas. Se ejecutan en node:test sin dependencias adicionales:
pnpm test
Cubierto: la decisión de window.open / popup (window-open.ts), el análisis de entrada
del proxy (proxy.ts), la normalización de URL de la barra de direcciones (url-input.ts), el
empalme TOML de configuración de Codex (toml.ts) y la paleta de identidades (colors.ts).
Empaquetado:
pnpm build:mac # or build:win
El valor predeterminado es firma adhoc (identity: '-'), que no necesita certificado de Apple Developer y funciona bien en la máquina de compilación. Pero una aplicación con firma adhoc no se puede distribuir — Gatekeeper la bloqueará en la Mac de otra persona. Para distribución real, proporciona un certificado mediante variables de entorno y establece notarize a true:
CSC_LINK=/path/to/cert.p12 CSC_KEY_PASSWORD=... \
APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... \
pnpm build:mac
Conectar Codex / Claude Code
La ruta más rápida: deja que el agente se conecte solo
La interfaz tiene un bloque "Let the agent connect itself" que muestra un prompt listo para enviar. Cópialo, pégalo en Codex o Claude Code, y el agente escribirá su propia configuración MCP, reiniciará y verificará el enlace llamando a list_identities.
El prompt no es solo un fragmento de configuración — también le dice al agente qué hacen las 13 herramientas y qué errores evitar (referencias obsoletas, adivinar la sintaxis de agent-browser, intentar distinguir identidades por el título de la página). Se genera a partir de valores de runtime en vivo, por lo que las rutas en él siempre son correctas para la máquina en la que se ejecuta.
O haz clic en un botón
El mismo panel tiene una instalación de un clic que escribe el servidor MCP en ~/.codex/config.toml (idempotente, con una copia de seguridad automática primero). Las rutas se resuelven en tiempo de ejecución desde process.execPath, por lo que no pueden ser incorrectas.
O configúralo manualmente
En macOS:
[mcp_servers.personae]
command = "/Applications/Personae.app/Contents/MacOS/Personae"
args = ["/Applications/Personae.app/Contents/Resources/mcp-server.mjs"]
[mcp_servers.personae.env]
ELECTRON_RUN_AS_NODE = "1"
El ejecutable bajo Contents/MacOS/ sigue productName, por lo que es Personae. Ten en cuenta que electron-builder's executableName solo se aplica a Windows.
command apunta al binario de la propia aplicación; ELECTRON_RUN_AS_NODE=1 hace que se degrade a un runtime Node simple. Por eso no se requiere instalación de Node (verificado: con PATH establecido a /usr/bin:/bin, el flujo completo sigue funcionando).
Claude Code:
claude mcp add personae \
--env ELECTRON_RUN_AS_NODE=1 \
-- /Applications/Personae.app/Contents/MacOS/Personae \
/Applications/Personae.app/Contents/Resources/mcp-server.mjs
Herramientas MCP
Cada herramienta toma un argumento identity (nombre o id).
| Herramienta | Propósito |
|---|---|
load_skill | Obtener la sintaxis real de comandos de la versión incluida de agent-browser (devuelve un índice de secciones por defecto; pasa section para una específica) |
list_identities | Listar todas las identidades con estado abierto y URL actual |
open_identity | Abrir la ventana de una identidad |
snapshot | Instantánea del árbol de accesibilidad que devuelve referencias de elementos [ref=eN] |
navigate | Navegar a una URL |
click / fill / press | Interacción; click acepta una referencia o texto visible |
act | Ejecutar varios comandos dentro de un solo proceso agent-browser |
get_text / get_url | Leer contenido |
screenshot | Capturar una captura de pantalla |
eval_js | Evaluar JavaScript |
Cuando click / fill reciben una referencia @eN, automáticamente toman una instantánea primero, porque las referencias solo son válidas dentro de un solo proceso agent-browser — reutilizar una entre procesos siempre falla con Unknown ref. Para interacciones de varios pasos, colócalas en un lote act.
Controlar una ventana emergente hija. Cuando una página abre una ventana emergente (un inicio de sesión OAuth, un diálogo de compartir…), esa ventana emergente se convierte en una ventana hija que pertenece a la misma identidad (ver Popups y OAuth). Es un objetivo CDP separado, listado bajo la identidad en list_identities como children. Cada herramienta de acción/lectura toma un argumento opcional target — pasa el targetId de una hija (o su índice) para controlar la ventana emergente en lugar de la ventana principal; omítelo para controlar la ventana principal como antes.
Arquitectura
┌─────────────────────────────────────────────────────────┐
│ Electron main process │
│ │
│ ├─ CDP server (port 0 → system-assigned, 127.0.0.1 only)│
│ ├─ IdentityManager storage / window lifecycle / target │
│ └─ Agent Bridge local HTTP, exposes identity↔target │
└───────────┬─────────────────────────────────┬────────────┘
│ │
┌─────────▼──────────┐ ┌───────────▼───────────┐
│ Identity window │ │ Discovery file │
│ (one per identity) │ │ userData/ │
│ │ │ agent-bridge.json │
│ BrowserWindow shell│ │ (port changes every │
│ ├ WebContentsView │ │ launch; found via │
│ │ chrome.html │ │ a fixed path) │
│ └ WebContentsView │ └───────────┬───────────┘
│ content │ │
│ ↑ agent acts │ │
└────────────────────┘ │
│
┌─────────────────────────────────▼───────────┐
│ scripts/mcp-server.mjs (stdio JSON-RPC) │
│ read discovery → get targetId → run CLI │
└───────────────────┬─────────────────────────┘
│
┌───────────▼────────────┐
│ Codex / Claude Code │
└────────────────────────┘
Por qué cada ventana de identidad es "un shell más dos WebContentsViews"
El objetivo es envolver páginas de terceros en nuestra propia interfaz de navegación sin usar la etiqueta <webview>. Entonces:
- el shell
BrowserWindowes solo un contenedor y cargaabout:blank; - la barra superior es un
chrome.htmllocal con un preload dedicado que expone solo seis métodos de navegación; - el área de contenido es un
WebContentsViewdonde se aplica la partición, y deliberadamente no carga ningún preload, dejando las páginas de terceros intactas.
Los BrowserWindows anidados y BrowserView también funcionan, y el agente puede controlar los tres (todo verificado). WebContentsView se eligió porque BrowserView está marcado como @deprecated en las definiciones de tipos de Electron, y porque una ventana anidada es una ventana nativa separada en macOS — arrastrar, redimensionar y minimizar requerirían sincronización manual de límites, que nunca se siente como "una ventana de navegador".
Popups y OAuth
Una página puede abrir una ventana emergente con window.open(url, name, features) — inicios de sesión OAuth, diálogos de "compartir en…", etc. Estas necesitan ser ventanas reales: una ventana emergente OAuth depende de window.opener y postMessage/window.close() para devolver el resultado, por lo que no se puede aplanar en una navegación dentro de la página sin romper el flujo.
El enfoque aquí trata una ventana emergente como un hijo de primera clase de su identidad, sin intentar adivinar "¿es una ventana OAuth?":
- Misma ventana vs hijo. Un
<a target="_blank">simple o unwindow.opensin características permanece como una navegación dentro de la ventana (sin opener, sin nuevo objetivo) — eso es seguro y mantiene el invariante de "una identidad, un objetivo principal". Solo una apertura con disposiciónnew-window(es decir,window.opencon características de ventana) se convierte en una ventana hija real. - Pertenece a la identidad. El hijo se crea con
parentestablecido a la ventana shell de esa identidad y la misma partición, por lo que flota sobre la ventana correcta y sus cookies caen en la cuenta correcta.window.openerse conserva, por lo que todos los estilos OAuth funcionan.outlivesOpener: falsesignifica que cerrar la identidad cierra sus ventanas emergentes. - Bloqueador de popups (activado por defecto). Lo único que distingue "debería abrirse" de "no debería" es la activación del usuario — ¿hubo un clic o pulsación de tecla real en el último segundo? Esto es exactamente cómo funcionan los bloqueadores de popups de los navegadores principales: nunca inspecciona el dominio o el contenido. Los pop-unders impulsados por scripts (sin entrada previa) se bloquean y se muestran como un toast silencioso; puedes desactivar el bloqueador en la barra superior.
- Los agentes pueden controlarlo. Cada hijo es un objetivo CDP separado, resuelto a su
targetIdautoritativo y listado bajo la identidad comochildren. Cada herramienta MCP toma untargetopcional para dirigirse a una ventana emergente específica. El contrato de "un objetivo principal por identidad" para el mundo exterior no se rompe — se extiende honestamente a "un principal más hijos rastreados".
La decisión en sí (same-window / allow-child / ignore) vive en una función pura, src/main/window-open.ts, y está probada por unidades.
Proxy por identidad
Cada identidad puede enrutar a través de su propio proxy, algo útil cuando quieres que las cuentas del mismo sitio provengan de diferentes regiones o para probar el comportamiento geográfico. Tú aportas tu propio proxy (cómpralo en IPRoyal / Webshare / Bright Data / quien sea); Personae solo proporciona la configuración y lo conecta.
- Dónde se aplica. Un proxy se establece en la partición
persist:de la identidad mediantesession.setProxy, por lo que es genuinamente por identidad: la identidad A puede salir por una IP de EE. UU. mientras que la identidad B sale por Japón. Sin proxy configurado significa conexión directa. - Compatibilidad. HTTP, HTTPS y SOCKS5. Puedes rellenar los campos por separado o pegar una cadena completa como
socks5://user:pass@host:porten el campo de host. - Autenticación. Los proxies con nombre de usuario y contraseña funcionan. Las credenciales nunca van en la cadena de reglas del proxy (Chromium rechaza
user:pass@allí); la autenticación se responde a través del eventologinde Electron. - Las contraseñas están cifradas en reposo. La contraseña se almacena mediante
safeStorage(Keychain en macOS, DPAPI en Windows) enuserData/proxy-secrets.json, nunca enidentities.jsony nunca se envía de vuelta al renderizador. CuandosafeStorageno está disponible, se recurre a texto plano y la interfaz lo indica. - Probar conexión. Un botón consulta un servicio de eco de IP a través de la sesión de esa identidad y muestra la IP de salida real, para que puedas confirmar que el proxy está realmente activo.
- Protección contra fugas WebRTC. Para cualquier identidad con proxy, se aplica
setWebRTCIPHandlingPolicy('disable_non_proxied_udp')para que WebRTC no pueda eludir el proxy y filtrar tu IP real.
Esto no es un navegador anti-detección. Cambia tu salida de red, nada más. Más allá de la protección WebRTC mencionada, no realiza suplantación de huellas digitales (Canvas / WebGL / UA / zona horaria / fuentes…), por lo que varias identidades en la misma máquina siguen compartiendo la misma huella del navegador. Si una plataforma correlaciona cuentas por huella, un proxy por sí solo no ocultará eso.
El análisis del proxy (parseProxyInput / buildProxyRules) es una función pura en src/main/proxy.ts y está probado por unidades, incluido que las credenciales nunca se filtren en la cadena de reglas.
Cosas que nos mordieron
Se mantienen aquí porque la mayoría no están documentadas en ningún sitio.
El puerto CDP es por proceso del navegador. --remote-debugging-port es un interruptor a nivel de proceso y no tiene contraparte webPreferences, por lo que una aplicación Electron tiene exactamente un servidor CDP; no puedes dar a cada ventana su propio puerto. Usa el puerto 0 para que el sistema asigne uno y luego léelo de userData/DevToolsActivePort — ese archivo se escribe antes de que el enlace tenga éxito, así que también sondea /json/version para verificar que está activo.
--pin-tab simplemente falla en Electron. Activa Target.createTarget, que Electron informa como Not supported. Peor aún, el fijado es persistente: una vez que una sesión lo ha usado, el estado se guarda e incluso close --all no lo borra, rompiendo todos los comandos posteriores. Así que pasa --no-pin-tab explícitamente en cada llamada.
Un objetivo de página con título vacío cuelga agent-browser. Si la ventana del shell nunca llama a loadURL, aparece en CDP como un objetivo de página con url y título vacíos; adjuntarse a él cuelga para siempre (tab list no devuelve nada, ni siquiera un tiempo de espera). Por eso el shell debe cargar about:blank y establecer un título legible.
Usa about:blank en lugar de data:text/html,<title>... — los títulos no ASCII en este último se decodifican como latin-1 y se convierten en texto corrupto.
Establecer el título también tiene una condición de carrera: about:blank carga tan rápido que did-finish-load puede dispararse antes de que el listener esté adjunto, dejando el título atascado en about:blank cuando varias identidades se abren simultáneamente. Solución: llama a setTitle sincrónicamente una vez y luego de nuevo después de la carga.
Nunca identifiques una ventana por título, url o índice de pestaña. Los títulos y URLs son idénticos cuando varias identidades abren el mismo sitio, y el orden de objetivos CDP no coincide con el orden de índices de pestañas de agent-browser, por lo que los índices tampoco se pueden derivar. Solo targetId funciona.
El contenido de habilidades de agent-browser cambia entre versiones — no copies sintaxis de la web. Su propio SKILL.md establece explícitamente que no contiene sintaxis de comandos y requiere skills get para obtenerla desde la CLI. Este proyecto demostró el punto: tab --url "*settings*" y --pin-tab encontrados en línea no existen en versiones anteriores, mientras que tab list --json emitiendo targetId es una capacidad más nueva — y todo el esquema de direccionamiento de este proyecto se basa en ella.
agent-browser busca hacia arriba desde la ubicación del binario un directorio skills/, lo que puede chocar con el directorio de un proyecto no relacionado con el mismo nombre. Pasa AGENT_BROWSER_SKILLS_DIR explícitamente.
Un binario empaquetado no puede depender solo de asarUnpack. asarUnpack coloca archivos bajo app.asar.unpacked/resources/bin/, mientras que process.resourcesPath apunta a Contents/Resources — no es la misma ubicación, por lo que el binario no se encuentra después del empaquetado. Usa extraResources para alinear las rutas y mantén asarUnpack limitado a archivos realmente importados con ?asset, de lo contrario el mismo binario se envía dos veces.
En macOS no puedes simplemente "omitir la firma". Establecer identity: null hace que electron-builder omita la firma por completo; el bundle conserva la firma adhoc original de Electron, pero los recursos han sido modificados, por lo que la firma y el contenido no coinciden y la aplicación se niega a iniciar silenciosamente (sin error, sin registro; spctl informa code has no resources but signature indicates they must be present). El enfoque correcto es identity: '-' para la firma adhoc más com.apple.security.cs.disable-library-validation en los entitlements — establece tanto entitlements como entitlementsInherit, ya que el primero cubre el proceso principal.
El directorio userData sigue el package.json de name, mientras que el ejecutable sigue productName. Son dos campos diferentes, y executableName solo se aplica a Windows — así que si no coinciden, buscarás en el lugar equivocado. Este proyecto establece deliberadamente ambos en Personae para evitar eso. (Cuidado en Linux: su sistema de archivos distingue mayúsculas y minúsculas, por lo que un directorio creado con un nombre en minúsculas más antiguo no se encontrará.)
publish: generic con una URL de marcador de posición rompe el empaquetado en el último paso. La plantilla de electron-builder incluye provider: generic con url: https://example.com/auto-updates. Cambiar a provider: github hace que intente inferir un canal de lanzamiento al final del empaquetado, y sin contexto de propietario/repositorio lanza TypeError: Cannot read properties of null (reading 'channel') — la compilación ya está completa, pero sale como un fallo. Este proyecto publica mediante gh release upload en CI, así que publish: null es lo que se usa.
Un if a nivel de trabajo en GitHub Actions no puede acceder al contexto matrix. Escribir if: inputs.platforms == matrix.name para filtrar plataformas falla silenciosamente (actionlint marca context "matrix" is not allowed here, pero GitHub mismo no se queja), por lo que cada plataforma se compila independientemente de la entrada. Genera el JSON de la matriz en un trabajo anterior y pásalo a strategy.matrix con fromJSON.
Al renderizar SVG con Electron, suprime window-all-closed. El script de iconos itera "crear ventana → capturar → destruir → crear siguiente", y cada destroy() reduce el contador de ventanas a cero, lo que por defecto cierra toda la aplicación — el síntoma es un bloqueo después de solo el primer tamaño, con el hijo informando No rendezvous client, terminating process (parent died?). Parece un problema de sincronización, pero reintentar no ayuda. Registra un manejador window-all-closed vacío.
Dos más: en pantallas Retina capturePage genera a devicePixelRatio (pedir 512 produce 1024), así que fija zoomFactor / deviceScaleFactor; e incrustar un SVG moderadamente largo en data:text/html;base64,... excede el límite de longitud de URL y hace que loadURL falle con ERR_FAILED (-2), así que usa un archivo temporal con una referencia file://.
Limitaciones conocidas y notas de seguridad
- El aislamiento es una convención, no un límite arquitectónico. El puerto CDP no tiene control de acceso. Solo escucha en loopback con un puerto aleatorio, pero cualquier proceso local que se conecte puede controlar todas las identidades, cruzando los límites de partición. Ese es el costo directo de permitir que agentes externos se adjunten. No manejes cuentas sensibles en una máquina compartida.
- Navegación en la misma ventana para enlaces ordinarios.
setWindowOpenHandlerconvierte los enlaceswindow.openytarget="_blank"en navegación en la misma ventana para que una identidad siempre se mapee a un objetivo principal. Las ventanas emergentes genuinas (window.opencon características, por ejemplo OAuth) son la excepción — se abren como ventanas hijas rastreadas; ver Popups y OAuth. - Cada identidad ocupa al menos 3 objetivos de página CDP (shell + barra superior + contenido), más uno adicional por cada ventana emergente hija abierta, por lo que escala aproximadamente a 3× el número de identidades.
- La versión empaquetada de agent-browser está fijada en el momento de la compilación; las correcciones posteriores no se recogen automáticamente.
- El comportamiento en tiempo de ejecución solo se verifica en macOS (arm64), incluida la compilación empaquetada. El empaquetado de Windows tiene éxito en CI (macOS y Windows se compilan y producen instaladores), pero la aplicación nunca se ha ejecutado realmente en Windows, por lo que el comportamiento en tiempo de ejecución allí no está verificado. Linux no es un objetivo de compilación.
bundle:abempaqueta solo para la plataforma actual. - Las compilaciones con firma adhoc no son distribuibles: solo se ejecutan en la máquina de compilación y están bloqueadas por Gatekeeper en otros lugares. La distribución real necesita tu propio certificado y notarización.
- El lado de Codex no se ha verificado con un cliente real: el flujo MCP se probó con un script actuando como cliente (incluida la compilación empaquetada en un entorno sin Node), pero nunca contra una ejecución real de codex.
- El soporte de proxy solo cambia la salida de red, no la huella. Ver Proxy por identidad — no es un navegador anti-detección.
Estructura del proyecto
src/main/
cdp.ts CDP port setup/discovery, targetId resolution
identity.ts identity storage, window lifecycle, nav-bar IPC, proxy
window-open.ts pure decision for window.open / target=_blank (unit-tested)
proxy.ts proxy input parsing → proxyRules (unit-tested)
secret-store.ts safeStorage-backed proxy password storage
url-input.ts address-bar input → URL normalization (unit-tested)
toml.ts TOML section splice for the Codex config (unit-tested)
agent-bridge.ts local HTTP bridge + discovery file
mcp-setup.ts one-click Codex configuration
index.ts main entry and IPC registration
src/shared/
colors.ts identity palette — imported by BOTH main and renderer,
so a window's top-bar dot always matches its list entry
src/preload/
index.ts main-window API
chrome.ts top-bar preload (navigation methods only)
src/renderer/
chrome.html navigation-bar UI
src/App.tsx identity management and connection panel
src/ProxyPanel.tsx per-identity proxy settings UI
src/agent-prompt.ts builds the copy-and-paste prompt for agents
src/assets/fonts/ self-hosted latin subsets (the CSP blocks
external font CDNs; CJK falls back to the system)
scripts/
mcp-server.mjs MCP server (stdio JSON-RPC)
bundle-agent-browser.mjs bundles the binary and core skill
make-icons.mjs SVG → PNG / icns / ico
test/
*.test.ts node:test unit tests for the pure modules above
design/logo/
icon.svg icon source (edit this, then run pnpm icons)
concept*.svg alternative concepts from the design pass
.github/workflows/
release.yml build and publish a GitHub Release (manual)
check.yml lint / typecheck / packaging smoke test (manual)
resources/bin/ y resources/skills/ son generados por pnpm bundle:ab y no se confirman.
Icono
La fuente es design/logo/icon.svg — tres tarjetas de colores no superpuestas para tres identidades aisladas, con un puntero para el control del agente. Después de editar el SVG, regenera cada formato de plataforma:
pnpm icons
Esto produce build/icon.png (1024), build/icon.icns, build/icon.ico y resources/icon.png (512, el icono de ventana en tiempo de ejecución).
El renderizado pasa por el Chromium integrado de Electron, por lo que no se necesitan rsvg-convert / Inkscape / ImageMagick — esos suelen estar ausentes en un entorno limpio, mientras que Electron es una dependencia que este proyecto tiene de todos modos. .icns se ensambla con el iconutil del sistema; .ico se escribe byte a byte.
Publicación
Ambos flujos de trabajo son solo manuales (workflow_dispatch) y nunca se ejecutan en push o tag.
Ve a Actions en GitHub, elige un flujo de trabajo y luego Run workflow:
| Flujo de trabajo | Propósito | Entradas |
|---|---|---|
| Build & Release | Compilar macOS + Windows, crear una Release | versión (opcional), plataforma (all / macos / windows), crear release, prerelease |
| Check | lint + typecheck + prueba de empaquetado | si ejecutar el empaquetado |
La Release se crea como borrador; revisa los activos y luego pulsa Publish tú mismo. Si el tag ya existe, los activos se añaden a él (--clobber sobrescribe archivos con el mismo nombre), por lo que las re-ejecuciones no simplemente fallan.
La publicación no usa el paso de publicación propio de electron-builder (publish: null en electron-builder.yml); un solo trabajo de agregación sube todo con gh release upload en su lugar, porque tres plataformas ejecutándose en paralelo intentarían cada una crear la misma Release y se pisarían entre sí.
Cuando el flujo de trabajo Build & Release crea una versión, también publica
el lanzador stdio @leek-emperor/personae-mcp con la versión correspondiente en npm y luego
publica server.json con la CLI oficial mcp-publisher. Establece el
secreto NPM_TOKEN del repositorio (un token de automatización de npm con permiso para
publicar ese paquete público) antes de la primera versión; la autenticación del
Registro MCP usa OIDC de GitHub y no necesita un secreto separado.
Los artefactos de macOS de CI también están firmados ad-hoc. Para una firma real, agrega CSC_LINK / CSC_KEY_PASSWORD a los secretos del repositorio y establece notarize a true en electron-builder.yml.
Licencia
MIT — consulta LICENSE.
El agent-browser incluido tiene licencia Apache-2.0; los derechos de autor pertenecen a sus autores.