Skip to content

Escribir complementos ​

Un complemento es un módulo ES cuya exportación por defecto es un objeto Plugin. El contrato está en @alisio/sdk: tipos más dos utilidades (definePlugin, textResult), sin dependencias de runtime ni SDKs de proveedores.

ts
import { definePlugin, textResult } from "@alisio/sdk";

export default definePlugin({
  id: "acme.hello",
  name: "Acme Hello",
  description: "Adds a friendly greeting tool",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    api.tools.register({
      name: "hello",
      description: "Greets the user.",
      effect: "read",
      inputSchema: { type: "object", properties: {}, additionalProperties: false },
      async execute() {
        return textResult("Hello!");
      },
    });
  },
});

Los complementos son código de confianza

Los complementos se ejecutan en el mismo proceso con todos los privilegios del proceso de Alisio. Ni un manifiesto de complemento ni un subproceso son un sandbox, y el campo effect no aísla nada. Carga solo código en el que confíes.

El objeto Plugin ​

MiembroDescripción
idID único que cumple ^[a-z0-9][a-z0-9.-]{0,63}$, por ejemplo acme.hello
versionSemVer, por ejemplo 0.1.0 o 0.1.0-beta.1
apiVersionSiempre 1
name, descriptionTexto opcional y legible para el catálogo de /plugins
categoriesCategorías de catálogo opcionales (consulta la sección siguiente, Categorías de complementos). El host también deriva model-provider de los registros de proveedores
setup(api)Registra todo; puede ser asíncrono. Si falla, se revierten los registros parciales
extensionsOpcional; proveedores declarativos para puntos de extensión, registrados con prioridad 0
dispose()Opcional; libera recursos cuando Alisio se cierra. Se ejecuta solo al cerrar, nunca al deshabilitar un complemento ni con /reload, y lo limita pluginHooks.disposeTimeoutMs (consulta Cierre)

definePlugin es una función de identidad que solo añade tipado. Se rechazan IDs de complemento duplicados.

Categorías de complementos ​

categories es una lista de agrupaciones de catálogo. La TUI agrupa las filas de /plugins por la primera categoría declarada y usa "General" cuando el complemento no declara ninguna; la vista de detalle lista todas las categorías. Los autores de complementos pueden declarar cualquier valor aceptado; el host solo deriva model-provider por su cuenta, a partir de los registros de proveedores. Las categorías describen qué hace el complemento, así que un mismo complemento puede declarar varias.

CategoríaSignificadoUsada por los integrados
model-providerRegistra proveedores de modelosopenai-compatible
methodology-harnessAgrupa un flujo de trabajo de metodología de desarrollo—
memoryMemoria persistente, recuerdo y resúmenes de sesiónmemory
subagentsDelegación, sesiones hijas y gestión de agentessubagents
searchProveedores de búsqueda web o vectorial—
toolsColecciones de herramientas de propósito general—
securityHerramientas de auditoría, sandbox o permisos—
analyticsInstrumentación de uso/métricas (estadísticas de sesión, costo)—
mcpGestión de servidores MCP o utilidades para empaquetarlos—
storageBackends de almacenamiento durable más allá del SQLite por defecto—
uiProveedores de presentación TUI (pantallas de inicio, mascotas, paneles)—
decisionsProveedores de Decision Intelligence (api.decisions.registerProvider)—

El único proveedor de modelos integrado es openai-compatible. Los proveedores dedicados (DeepSeek, OpenCode Console (Zen), OpenCode Go) son complementos independientes publicados desde el monorepo alisio-plugins e instalables con alisio install npm:@alisio/plugin-{deepseek,opencode,opencode-go}; una vez instalados se registran como complementos model-provider y aparecen en /connect y /model.

Gestionar complementos en la TUI ​

Ejecuta /plugins para abrir el catálogo filtrable. [x], [ ], [!] y [*] significan activo, inactivo, fallido y reinicio necesario; las filas también distinguen los complementos integrados de etiquetas de origen externas seguras. Selecciona una fila para ver la descripción completa, la categoría y la acción disponible.

Los cambios actualizan atómicamente .alisio/config.json del proyecto actual y conservan los campos JSON no relacionados. Se aplican deliberadamente al iniciar Alisio de nuevo: los registros, hooks, almacenamiento y clientes de proveedor vivos no se eliminan parcialmente. Devolver un cambio al estado original del runtime elimina restart-required. Los complementos externos requieren un proyecto de confianza y confirmación explícita porque se ejecutan con todos los privilegios del proceso. Alisio impide desactivar un complemento de proveedor mientras lo retenga el proveedor activo o cualquier sesión enrutada viva, y también bloquea complementos con recursos de la sesión actual. Cambia de proveedor y cierra las sesiones retenidas, o reinicia Alisio, primero. Los perfiles y credenciales siguen siendo ajustes globales y nunca se copian a la configuración de complementos.

Referencia de PluginAPI ​

Cada método register/on devuelve una función para anular el registro. Todo lo que registra un complemento se elimina automáticamente cuando se descarga.

MiembroDescripción
tools.register(tool)Registra una herramienta (ToolDefinition)
commands.register(name, handler, options?)Registra un comando; handler(args: string, context?: { sessionId? }) => Promise<string> devuelve el texto que se muestra al usuario (sessionId es la sesión actual de la interfaz interactiva, cuando se conoce). options: { description?, argumentHint? }, mostrados en /help y en el autocompletado
events.on(handler)Observa los eventos versionados de ejecución (RunEvent: schemaVersion, runId, sessionId, seq, type, timestamp, data, más los opcionales eventId y correlationId). KnownRunEvent tipa el data de cada evento que emite el núcleo
context.register(provider)() => Promise<string>; añade texto al contexto del modelo
resources.skills(path)Añade una raíz de Agent Skills (relativa al archivo del complemento)
resources.agents(path)Añade un directorio de definiciones de agentes para complementos de delegación (consulta Subagentes)
resources.list(kind)Directorios registrados por todos los complementos para skills, prompts o agents, con el ID del complemento
resources.prompts(path)Añade un directorio de plantillas de prompts (*.md, relativo al archivo del complemento)
state.get(key) / state.set(key, value)Estado JSON pequeño por complemento, persistido en la base de datos de sesiones
storage.sqlite(path)Abre un archivo SQLite privado (0600), creando los directorios padre (0700). Devuelve el puerto de almacenamiento SqlDatabase
views.register(view)Registra una vista de datos con nombre y de solo lectura que los hosts, como la interfaz web, pueden leer. Ausente en un núcleo anterior: usa api.views?.register(...)
apps.register(app)Registra una aplicación de complemento de página completa junto al panel de proyectos. Ausente en un núcleo anterior: usa api.apps?.register(...)
proof.registerVerifier(verifier)Registra un verificador de Proof ({ id, describe, run(input, ctx) }) al que se enlazan los criterios como <pluginId>:<id>. Ausente en un núcleo anterior a Proof: usa api.proof?.registerVerifier(...)
decisions.registerProvider(provider)Registra un proveedor de Decision Intelligence (no lo activa); también available(), activeProvider(), tryDecide(request, options?) y el opcional activate(providerId, options?) (options.recommend preselecciona Yes tras una acción explícita del usuario). Ausente en un núcleo anterior: usa api.decisions?.registerProvider(...)
paths{ state, config, cache }: directorios por complemento resueltos por el host y creados con modo 0700 en la primera lectura. Ausente en un núcleo anterior
optionspluginOverrides[id].options de solo lectura (o {}), congelado cuando se ejecuta setup. Ausente en un núcleo anterior
compaction.register({ beforeCompact, afterCompact })Hooks de compactación, ver más abajo
session.onStart(handler)El texto devuelto se inyecta una vez al comienzo de una sesión nueva y vacía (persistido en la sesión)
session.onEnd(handler)Se llama cuando termina una sesión interactiva (/clear, /exit, salida)
model.complete(request)Completado de texto agnóstico del proveedor: { system, messages, maxTokens?, model?, signal? } → Promise<string>
models.list() / models.resolve(reference)Lista modelos configurados sin credenciales y resuelve proveedor/modelo o un ID único sin proveedor
providers.register(provider)Añade metadatos, campos de configuración/autenticación, capacidades y una factoría. Los registros coexisten; /connect selecciona uno
extensions.register(point, provider, options?)Proporciona una implementación para un punto de extensión (mascot, startup-screen); options: { priority? }
ui.status(key, text, detail?)Texto breve en la barra de estado de la TUI; detail aparece en /stats; text: undefined lo elimina
ui.panel(id, provider)Un panel en árbol plegable (PanelProvider), solo en interfaces interactivas
ui.select(request)Pide al usuario que elija (SelectRequest: title, options con value, label, description?); resuelve undefined sin interfaz interactiva
ui.open(sessionId)Abre una sesión en una vista de solo lectura; devuelve false sin interfaz interactiva
ui.interactive()true cuando una interfaz interactiva puede responder a select
sessions.*Sesiones hijas, ver más abajo

Los complementos de proveedores dependen solo de @alisio/sdk. Su factoría recibe por separado el profile sin secretos, las credentials secretas y valores heredados opcionales. El núcleo controla la persistencia y la activación. La limpieza del registro participa en el rollback del complemento; si falla la creación de un proveedor, el proveedor activo permanece intacto.

Herramientas ​

ts
interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: JsonSchema;
  effect?: "read" | "write" | "process" | "external" | "internal";
  paths?: (input: Record<string, unknown>) => string[];
  concurrent?: boolean;
  execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
}

concurrent: true permite que una llamada se ejecute a la vez que otras llamadas de lectura o concurrentes del mismo turno (las herramientas de delegación lo usan). ToolContext proporciona signal, workspace, emit(data) y session. Devuelve textResult(text, isError?). El efecto controla los permisos (Herramientas y permisos): las operaciones desconocidas o de complementos usan external por defecto; declara read solo para herramientas sin efectos secundarios.

Resultados enriquecidos (bloques ui) ​

Un resultado de herramienta puede añadir partes { type: "ui", block } junto a su texto. Un UiBlock son solo datos; los complementos nunca envían código de renderizado. Conserva siempre también una parte de texto: los proveedores, la compactación y la salida headless solo ven la proyección de texto, y el bloque es una indicación de presentación.

ts
return {
  content: [
    { type: "text", text: "2 passed, 1 failed" },
    {
      type: "ui",
      block: {
        kind: "test-results",
        framework: "vitest",
        suites: [{ name: "math", cases: [{ name: "adds", status: "passed" }] }],
      },
    },
  ],
};
KindCamposRenderizado en la TUI
tablecolumns, rows, caption?Columnas alineadas
key-valueentries, caption?Dos columnas
treenodes (label, children?, meta?)Glifos de rama
codecode, lang?, caption?Código resaltado
markdowntextMarkdown
diffpatch? o before?/after?, path?, lang?, caption?Parche unificado como código diff; si no, before/after etiquetados
terminaloutput, command?, cwd?, exitCode?, durationMs?, truncated?Salida sin ANSI más exit <código> · <tiempo>
mermaidsource, title?Fuente como código mermaid
mathlatex, display?LaTeX literal
jsonvalue, collapsedDepth?, caption?JSON indentado, truncado
test-resultssuites (name, file?, cases), framework?, durationMs?Tabla de suite, caso, estado y tiempo, más los fallos
progresssteps (label, status, detail?), title?Una línea por paso con un glifo de estado

UI_BLOCK_KINDS lista todos los kinds. Acota los tamaños (unos 200 KB para un diff y 256 KB para la salida de terminal o el JSON serializado). Un kind que la versión de Alisio en ejecución no conoce, o un bloque mal formado, se muestra como JSON etiquetado en lugar de fallar, de modo que versiones anteriores pueden reproducir transcripciones más nuevas.

Reglas de nombres y prefijos ​

ElementoComplemento externoComplemento integrado
Nombre de herramientap_<hash>_<name> (10 caracteres hexadecimales del SHA-256 del ID del complemento), para evitar colisiones y cumplir los límites de nombres del proveedorSin prefijo
Comando<plugin id>:<name>, invocado como /command acme.hello:name args o /acme.hello:name; también desde alisio run "/acme.hello:name args", que imprime el resultado sin llamar al modelo (--json imprime una línea command_result), y desde la paleta webSin prefijo (por ejemplo /memory)
Efecto internalSe degrada a externalPermitido

Hooks de compactación ​

ts
export default definePlugin({
  id: "my.notes",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    api.compaction.register({
      async beforeCompact() {
        return { outputFields: { notes: "array of short strings worth keeping" } };
      },
      async afterCompact({ extracted }) {
        const notes = Array.isArray(extracted.notes) ? extracted.notes : [];
        return { report: { summary: `notes: ${notes.length}` } };
      },
    });
  },
});
  • beforeCompact(input) recibe sessionId, reason (manual o auto), los messages que van a reemplazarse, focus y signal. Puede devolver instructions (añadidas a las instrucciones del resumidor) y outputFields (campos JSON adicionales de primer nivel, nombre → descripción, solicitados en la misma llamada al resumidor).
  • afterCompact(result) recibe además model, replaced, structured, checkpoint, checkpointText y extracted (los campos de este complemento, sin validar: valídalos tú mismo). Puede devolver injectContext (texto añadido tras el checkpoint; mantenlo dentro de un presupuesto) y report (summary se muestra literalmente).

Hooks de sesión y completados del modelo ​

ts
api.session.onStart(async (info) => `Project notes for ${info.workspace}`);
api.session.onEnd(async (info) => {
  const summary = await api.model.complete({
    system: "Summarize the session in one sentence.",
    messages: [{ role: "user", text: `${info.messages.length} messages` }],
    maxTokens: 200,
    signal: info.signal,
  });
  api.state.set("lastSummary", summary);
});

SessionInfo contiene sessionId, model, workspace, reason (start, clear o exit), messages y signal. model.complete usa el proveedor configurado (el modelo de la sesión cuando el complemento lo pasa) y no descuenta del presupuesto limits.maxTokens.

Puerto de almacenamiento ​

ts
const db = api.storage.sqlite("/path/to/notes.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)");
db.transaction(() => db.prepare("INSERT INTO notes (text) VALUES (?)").run("hello"));
const rows = db.prepare("SELECT * FROM notes").all();

SqlDatabase es síncrono: exec(sql), prepare(sql) (las sentencias se cachean por texto SQL y ofrecen run, get, all), transaction(fn) (transacción inmediata; las llamadas anidadas se unen a la exterior) y close(). FTS5 está disponible. El host proporciona el driver, por lo que los complementos nunca dependen de un runtime concreto.

Vistas de datos ​

Una vista de datos es una función con nombre que devuelve JSON para una sesión y que los hosts leen sin ejecutar una herramienta. alisio serve las expone a la interfaz web (el complemento de memoria integrado alimenta así la pestaña Memoria).

ts
import { definePlugin } from "@alisio/sdk";

export default definePlugin({
  id: "notes",
  version: "1.0.0",
  apiVersion: 1,
  setup(api) {
    // `views` is absent on an older core: feature-detect it and keep working without it.
    api.views?.register({
      id: "recent",
      description: "Recent notes of the session",
      params: {
        type: "object",
        properties: { limit: { type: "integer", minimum: 1, maximum: 50, default: 20 } },
      },
      // `params` is already validated and coerced; `loadNotes` is your own storage code.
      handler: (params, { sessionId }) => ({ items: loadNotes(sessionId, Number(params.limit)) }),
    });
  },
});
  • id son letras minúsculas, dígitos y guiones (hasta 40 caracteres) y es único dentro del complemento; description es obligatoria. params es un objeto JSON Schema de primitivos (string, integer, number, boolean; sin $ref) cuyas claves desconocidas se rechazan. Las cadenas de consulta se convierten a los tipos declarados y se aplican los valores por defecto antes de ejecutar handler(params, { sessionId, workspace, signal }). ViewParamsError convierte un parámetro que el esquema no puede expresar en un 400.
  • Por HTTP una vista es GET /api/sessions/:sid/views/:plugin/:view?<params>, con la misma cookie de sesión y las mismas reglas de Host y Origin que el resto de rutas. Solo responden los complementos habilitados, la sesión debe existir y su workspace también, y una vista de otro complemento o de uno deshabilitado es un 404. Se aceptan como máximo 16 parámetros de consulta de 512 caracteres.
  • La respuesta es JSON de como máximo 1 MiB (view_too_large, 502) y el handler tiene 5 segundos (view_timeout, 504; signal aborta). Cualquier otro fallo es un view_failed genérico (502): su mensaje y los parámetros nunca se envían ni se registran, porque pueden contener datos del proyecto.
  • Las vistas son de solo lectura por contrato, no por aislamiento. El host solo controla el método, los parámetros validados, el tiempo y el tamaño; no puede impedir que el código de un complemento escriba, y un handler que bloquee el bucle de eventos no se interrumpe con el timeout. Un complemento no es un sandbox.

Aplicaciones de complementos ​

Un complemento puede aportar una aplicación de página completa: una herramienta pequeña o una interfaz completa de varias páginas que vive junto al panel de proyectos y es independiente del chat. Regístrala durante setup; el host la elimina cuando el complemento se cierra o cuando setup falla.

ts
import { definePlugin } from "@alisio/sdk";

export default definePlugin({
  id: "acme.notes",
  version: "1.0.0",
  apiVersion: 1,
  setup(api) {
    // `apps` no existe en un núcleo anterior: avise en lugar de fallar en silencio.
    if (!api.apps) throw new Error("This plugin needs an Alisio host with plugin apps");
    api.apps.register({
      id: "notes",
      label: { en: "Notes", es: "Notas" },
      html: () => PAGE, // HTML autocontenido, como máximo 8 MiB
      actions: {
        save: {
          effect: "write",
          inputSchema: { type: "object", properties: { text: { type: "string" } } },
          handler: async (input, context) => saveNote(context.sessionId, input),
        },
      },
      busy: () => hasBackgroundJobs(), // evita la expulsión por inactividad del workspace
      dispose: () => cancelOwnedJobs(), // síncrono e idempotente
    });
  },
});
  • Ids y etiquetas. id sigue las reglas de las vistas (^[a-z][a-z0-9-]{0,39}$) y es único por complemento; un duplicado lanza un error. label es un texto o { en, es }; la web usa el idioma activo. Los nombres de acción cumplen ^[A-Za-z][A-Za-z0-9_.-]{0,63}$.
  • Dónde aparece. La web lista las aplicaciones de los complementos habilitados del proyecto abierto en un grupo Aplicaciones justo encima de Proyectos (el grupo no se muestra si no hay ninguna) y refresca la lista cada 15 segundos. Una lista larga muestra cinco aplicaciones y un botón Mostrar todas (N). Abrir una aplicación sustituye toda el área del chat; el panel de proyectos permanece y Volver al chat devuelve la conversación, su borrador y los adjuntos pendientes. El frame sigue montado al alternar, así que la edición sin guardar se conserva. Otro proyecto es otra instancia. La aplicación usa la sesión actual del proyecto (o la más reciente, o una nueva) y la conserva.
  • El frame. GET /api/sessions/:sid/apps/:plugin/:app/frame sirve la página en un iframe con sandbox (allow-scripts allow-downloads allow-modals allow-popups, sin allow-same-origin: no tiene cookies ni acceso al DOM del host). Cada <script> en línea debe llevar nonce="__ALISIO_NONCE__"; el servidor sustituye el marcador por un nonce nuevo en cada petición. La CSP es default-src 'none', scripts con nonce, estilos en línea, imágenes, fuentes y medios data:/blob:, connect-src blob:, worker-src blob:, base-uri 'none' y form-action 'none': la página no alcanza la red, solo el host.
  • Mensajes. Al cargar, el host transfiere un MessagePort al frame con { type: "alisio.app.connect", locale, visible } (acepta el primer canal que venga de window.parent). Por el puerto el frame envía { type: "request", id, action, input } y recibe { type: "response", id, result } o { type: "response", id, error: <código> }; el host también envía { type: "locale", locale } y { type: "visibility", visible }. Los códigos son not_found, invalid_input, not_permitted, timeout, failed, too_large, too_many_requests, unauthorized y network; nunca llevan entradas ni secretos. Se ejecutan como máximo 8 peticiones a la vez por frame. Los tipos (PluginAppConnectMessage, PluginAppRequestMessage, PluginAppHostMessage, PluginAppErrorCode) están en @alisio/sdk.
  • Acciones. Cada acción declara un effect (read, write o process) y un inputSchema (esquema de objeto; se rechazan $ref, $id y $async). Por HTTP una acción es POST /api/sessions/:sid/apps/:plugin/:app/actions/:action con { input, idempotencyKey? }. El host valida la entrada y aplica la misma puerta de efectos que una llamada a herramienta con las capacidades efectivas de la sesión. Un efecto que la sesión ya concede se ejecuta. Cuando el chat lo preguntaría (una acción write o process en una sesión que no es de solo lectura), el host abre la misma aprobación que una llamada a herramienta del chat, con las mismas opciones (permitir una vez, permitir para la sesión, denegar): el aviso dice «Complemento · App quiere escribir archivos (acción)», se muestra sobre la app (el chat está oculto mientras hay una app abierta) y nunca incluye la entrada. «Permitir para la sesión» amplía la política de la sesión igual que el chat, así que las acciones de apps y las herramientas del chat con ese efecto dejan de preguntar. Denegar, no responder, no tener un cliente web abierto (la aprobación se deniega tras la gracia habitual) o un efecto que la sesión prohíbe de plano (sesión de solo lectura) se rechaza con app_action_denied (403). El timeout de 30 segundos de la acción empieza cuando arranca el handler, no mientras la aprobación está pendiente; la espera de la respuesta tiene el tope del timeout de aprobaciones del chat (10 minutos) y cerrar la petición cancela la aprobación pendiente. El rechazo nombra el efecto para que la app pueda decir qué activar: { type: "response", id, error: "not_permitted", effect: "write" | "process" } por el puerto y error.details.effect en el cuerpo del error HTTP. La sesión sale de la ruta, nunca de la entrada (context.sessionId, context.workspace). Se comprueba en cada llamada que el complemento esté habilitado. Límites: 37 MiB de petición, 90 MiB de resultado (app_result_too_large), 30 segundos (app_action_timeout, 504; context.signal aborta). El trabajo largo debe devolver un job enseguida en lugar de retener la petición. Los fallos son un app_action_failed genérico (502); las entradas y los resultados nunca se registran.
  • Modelos y agentes. Los handlers reutilizan api.models, api.model.complete y api.sessions; no necesitan un SDK de proveedor ni claves aparte.
  • Un sandbox de navegador no es un sandbox de Node. El frame está aislado en el navegador, pero el backend del complemento es código de confianza en proceso con los privilegios de Alisio, y el timeout de una promesa no detiene código que consume CPU. Deshabilitar un complemento bloquea de inmediato las acciones nuevas, pero un puerto ya abierto no se revoca al instante.

Consulta examples/plugins/hello-app para una aplicación mínima.

Decision Intelligence, rutas y opciones ​

api.decisions, api.paths y api.options son opcionales: un núcleo anterior los omite, así que detecta cada uno por presencia y sigue funcionando sin él.

ts
setup(api) {
  api.decisions?.registerProvider(myProvider); // Registering never activates it.
  const cache = api.paths?.cache; // Per-plugin directory, created on first read.
  const device = api.options?.device ?? "cpu"; // From `pluginOverrides[id].options`.
}
  • api.decisions registra un proveedor de decisiones y permite que una herramienta de complemento pida decisiones. El usuario activa un proveedor con decisions.provider en la configuración global; instalar el complemento no basta. El contrato, los errores y el ciclo de vida están en Decision Intelligence. Las herramientas reciben el lado consumidor como context.decisions?.tryDecide(...), ligado a la ejecución actual. El miembro opcional api.decisions?.activate?.(providerId) permite que un complemento pida al host, desde un paso iniciado por el usuario, que un proveedor que registró pase a ser el activo: el usuario confirma una vez y el host guarda decisions.provider en la global (consulta pedir la activación).
  • api.paths ofrece state (<raíz de estado>/plugins/<id>), config (<config home>/plugins/<id>) y cache (<raíz de estado>/plugins/<id>/cache). La raíz de estado es la que usan el análisis y los artefactos, así que respeta --db. Úsalas en lugar de resolver tú mismo las rutas XDG o ALISIO_*.
  • api.options contiene el JSON que tú o el usuario pusieron en pluginOverrides[id].options de la configuración global (consulta pluginOverrides). Es solo global, de como máximo 8 KB, y una instantánea tomada en setup: cambiarlo requiere reiniciar. No guardes secretos ahí.

api.semantic y context.semantic son opcionales: un núcleo anterior los omite, así que detéctalos por presencia y sigue funcionando sin ellos. El núcleo define dos puertos independientes y un servicio que los compone; no incluye ningún motor (ni base de datos vectorial, ni runtime de inferencia, ni modelo). La búsqueda semántica existe solo cuando el usuario instala un complemento que la ofrece.

ts
// Consumer: index and search text without knowing the engine.
async execute(input, context) {
  const semantic = context.semantic;
  if (!semantic?.available()) return fallbackToFullTextSearch(input); // e.g. SQLite FTS5
  await semantic.index("notes", [{ id: "n1", text: "Quarterly budget review" }]);
  const matches = await semantic.trySearch("notes", input.query, { topK: 5, minScore: 0.3 });
  return matches ?? fallbackToFullTextSearch(input); // null: infrastructure problem
}

// Provider: register the ports (registering never activates them).
setup(api) {
  api.semantic?.registerEmbedding({
    id: "my-embed",
    name: "My embeddings",
    modelId: () => "my-org/my-model@q8", // changes whenever the vectors stop being comparable
    dimensions: () => 384,
    async embed(texts, { kind, signal }) {
      // `kind` is "query" or "document": apply the model's own prefixes here.
      return runModel(texts, kind, signal); // one Float32Array per text, same order
    },
  });
  api.semantic?.registerStore(myVectorStore); // a `VectorStoreProvider`
}
  • La activación es del usuario. semantic.embedding y semantic.store en la configuración global eligen los proveedores activos (consulta semantic); una configuración de proyecto no puede. Un proveedor puede ofrecer un paso de configuración iniciado por el usuario que llame a api.semantic?.activate?.({ embedding, store }): el host pregunta una vez, guarda los ids globalmente, nunca sobrescribe otro proveedor configurado y devuelve needs_confirmation cuando no hay a quién preguntar (headless).
  • Las colecciones tienen espacio de nombres por complemento. El host guarda <pluginId>/<collection> y collections() lista solo las suyas, sin el prefijo. Evita colisiones accidentales; no es aislamiento: los complementos son código de confianza en proceso.
  • Los modelos se vigilan. Cada colección recuerda el modelId y las dimensions con que se creó. Si el usuario cambia de modelo, index y search rechazan con model_mismatch o dimension_mismatch: borra la colección e indexa de nuevo desde tu propio texto fuente, porque solo tú lo conservas. trySearch devuelve null en ese caso para que puedas recurrir a otra vía.
  • Los límites se comprueban antes de llamar a ningún proveedor y fallan con invalid_request: 1..256 textos por embed (hasta 32768 caracteres cada uno), hasta 1000 documentos por index, topK 1..100, ids de registro ^[A-Za-z0-9._:-]{1,128}$, nombres de colección ^[a-z0-9][a-z0-9_-]{0,63}$, metadatos de hasta 8 KiB con valores escalares y filtros de igualdad sobre hasta 16 claves de primer nivel.
  • Las llamadas al proveedor están acotadas. Cada llamada recibe un AbortSignal y un tiempo límite (semantic.embedTimeoutMs, semantic.queryTimeoutMs); un agotamiento es timeout, el aborto del llamador se propaga como aborto y cualquier fallo del proveedor es un provider_error cuyo mensaje no contiene rutas ni texto del usuario.
  • minScore depende del modelo: las puntuaciones coseno no son comparables entre modelos.
  • /semantic (solo lectura, en todas las superficies) muestra los proveedores registrados y activos, el modelo, las dimensiones y el health() de cada proveedor (límite de 1 segundo).

Verificadores de Proof ​

Un complemento puede aportar un verificador para los criterios de aceptación de Proof. El miembro es opcional: detéctalo por presencia y el complemento sigue funcionando en un núcleo sin Proof.

ts
setup(api) {
  api.proof?.registerVerifier({
    id: "links",
    describe: "Checks that the docs have no broken links",
    async run(input, ctx) {
      const { callId, result } = await ctx.callTool("read_file", { path: "docs/index.md" });
      const text = result.content.map((p) => (p.type === "text" ? p.text : "")).join("");
      return { ok: !text.includes("](broken)"), outputHash: sha256(text), callId };
    },
  });
}

Un usuario enlaza un criterio con { "kind": "plugin", "verifierId": "<pluginId>:links" }. El resultado debe llevar evidencia referenciable: outputHash (sha256 en hexadecimal de lo que respalda el veredicto) y, si hizo llamadas a herramientas del núcleo, el callId de la que lo respalda. Sin un outputHash válido, o con un callId que la sesión nunca hizo, el criterio pasa a manual_review. Si run lanza un error o tarda más de 60 s, el criterio queda como estaba. Un verificador no gana permisos: ctx.callTool ejecuta una herramienta del núcleo por la misma compuerta que el agente (una llamada rechazada vuelve con isError). Es código de confianza dentro del proceso, no un sandbox, y nunca crea ni edita criterios.

Sesiones hijas ​

api.sessions es un servicio genérico para trabajo delegado: una sesión hija es una conversación separada y persistida (contexto nuevo) con un vínculo a su padre, ejecutada por el mismo runner con permisos reducidos. Un hijo nunca puede obtener una capacidad, herramienta o aprobación que su padre no tenga, y abortar la ejecución de un padre aborta sus descendientes en ejecución. El núcleo no contiene lógica de agentes; el complemento de subagentes está construido sobre este servicio.

MiembroDescripción
spawn(spec)Crea una sesión hija (ChildSessionSpec) y devuelve ChildSessionInfo
create(spec)Resuelve spec.model, si existe, y crea un hijo vinculado al proveedor; úsalo para reemplazos de modelo
run(id, prompt, { signal? })Ejecuta un turno en el hijo; resuelve un ChildRunResult (status, text, usage, error?)
get(id), children(parentId)Información de la sesión (id, parentId, depth, agent, title, status, model, workspace, usage, capabilities, marcas de tiempo)
ancestors(id)IDs de los ancestros, del más cercano al más lejano
cancel(id)Cancela una ejecución y todos sus descendientes en ejecución; devuelve cuántos estaban en ejecución
enqueue(id, text)Encola un mensaje de usuario para el siguiente turno de la sesión (o la siguiente ejecución si está inactiva)
isRunning(id)Si la sesión está en ejecución
capabilities(id)write, process, approvals y readOnly efectivos
model(id), workspace(id)Modelo que heredaría un hijo nuevo; workspace de la sesión
setStatus(id, status)Establece un SessionStatus: queued, running, completed, failed, cancelled o interrupted

Campos de ChildSessionSpec: parentId, id?, title, agent (etiqueta que se muestra en las interfaces y las aprobaciones), instructions?, tools?: { allow?, deny? } (* permite todas las herramientas del padre), model?, readOnly?, permission?: { write?, process? } (allow, ask o deny), workspace? (por ejemplo, un worktree de git), maxTurns?, timeoutMs? y maxTokens?.

sessions.run resuelve con { id, status, text, usage, error? }. Un hijo que agota su tope de turnos NO es un fallo: resuelve con status: "completed" y turnsExceeded: true, y text contiene un informe parcial utilizable (solo los errores reales, las cancelaciones y los tiempos de espera producen un estado de fallo).

ts
api.tools.register({
  name: "second_opinion",
  description: "Ask a read-only child session to review a file.",
  effect: "external",
  concurrent: true,
  inputSchema: {
    type: "object",
    properties: { path: { type: "string" } },
    required: ["path"],
    additionalProperties: false,
  },
  async execute(input, ctx) {
    if (!ctx.session) return textResult("needs a session", true);
    const child = api.sessions.spawn({
      parentId: ctx.session,
      title: `Review ${String(input.path)}`,
      agent: "reviewer",
      instructions: "Review the file and report bugs only.",
      tools: { allow: ["read_file", "search_text"] },
      readOnly: true,
      maxTurns: 10,
    });
    const result = await api.sessions.run(child.id, `Review ${String(input.path)}`, {
      signal: ctx.signal,
    });
    return textResult(result.text || result.error || result.status, result.status !== "completed");
  },
});

PanelProvider tiene un title, nodes({ sessionId }), que devuelve PanelNodes (id, parentId?, label, color?, status, startedAt?, endedAt?, tokens?, detail?, sessionId?), y un action("cancel" | "background", nodeId, { sessionId }) opcional.

Timeouts de hooks y aislamiento de fallos ​

  • Los hooks de compactación y de inicio de sesión se ejecutan con pluginHooks.timeoutMs (por defecto 15 000 ms); los de fin de sesión, con pluginHooks.sessionEndTimeoutMs (por defecto 10 000 ms).
  • Cada hook recibe un AbortSignal que se aborta cuando vence el tiempo límite.
  • Un fallo o timeout se registra como evento plugin_hook_failed y el núcleo continúa sin la contribución de ese complemento.
  • Los hooks se ejecutan en el mismo proceso: el timeout detiene la espera y señala el AbortSignal, pero no puede detener código síncrono bloqueante.

Cierre ​

  • dispose() se ejecuta cuando Alisio se cierra: salir de la interfaz de terminal, SIGINT, SIGTERM y SIGHUP (interfaz de terminal, alisio serve y alisio run) y el final de una ejecución. No se ejecuta al deshabilitar un complemento ni con /reload, porque ambos requieren reiniciar.
  • Los complementos se liberan en paralelo y cada uno está limitado por pluginHooks.disposeTimeoutMs (2000 ms por defecto, de 100 a 10000); un dispose() lento o colgado no retrasa a los demás, y su fallo se informa sin detenerlos. Termina con holgura dentro de ese límite.
  • Una muerte súbita (SIGKILL, una caída) no puede cubrirse: un complemento que posee un proceso del sistema operativo debe limpiar por sí mismo los que queden huérfanos.

Puntos de extensión ​

Los puntos de extensión permiten que un complemento reemplace una parte del host con su propio proveedor. Están tipados en ExtensionPoints; los puntos nuevos se añaden ahí sin romper los existentes.

PuntoTipo de proveedorPor defecto
mascotMascotProvider: { id, render(ctx: MascotContext) } que devuelve un string o string[]DefaultAlisioMascot (alisio.default)
startup-screenStartupScreenProvider: { id, render(ctx: StartupContext): string[] }DefaultStartupScreen (alisio.default)
websearchSearchProvider: { id, search(query, options?: { signal? }): Promise<SearchResult[]> }, SearchResult = { title, url, snippet }La cadena integrada: un websearch.provider configurado, o si no una instancia pública de SearXNG (consulta Herramientas y permisos)

Registra un proveedor de forma imperativa, o decláralo en el objeto del complemento:

ts
import { definePlugin } from "@alisio/sdk";
import { kiteMascot } from "./kite.js";

// Imperative, with a priority
export default definePlugin({
  id: "kite-mascot",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    const dispose = api.extensions.register("mascot", kiteMascot, { priority: 10 });
    // dispose() unregisters it; everything is also removed when the plugin unloads.
  },
});

// Declarative, at priority 0
export const declarative = definePlugin({
  id: "kite-mascot-declarative",
  version: "0.1.0",
  apiVersion: 1,
  extensions: { mascot: kiteMascot },
  setup() {},
});
  • api.extensions.register(point, provider, { priority }) devuelve una función que anula el registro. priority es un número finito, 0 por defecto; un proveedor necesita un id (lo único que comparten todos los puntos); un punto renderizable como mascot además necesita un render que funcione, comprobado cuando el host lo llama de verdad, igual que hoy un render que lanza excepción ya recurre al valor por defecto.
  • El campo declarativo Plugin.extensions es azúcar sintáctico para api.extensions.register(point, provider) con prioridad 0, aplicado antes de ejecutar setup.

Resolución ​

Para cada punto, el host elige exactamente un proveedor, de forma determinista:

  1. la mayor priority;
  2. después, el id del complemento en orden lexicográfico (los complementos se identifican por id, no por el nombre del paquete);
  3. después, el orden de registro dentro del mismo complemento.

El orden de carga entre complementos nunca influye. Los valores por defecto integrados solo se usan cuando no hay nada registrado, o como reemplazo de un proveedor que falla. Cuando varios proveedores empatan en la prioridad ganadora, el host informa un diagnóstico extension_conflict con point, winner y losers (como IDs plugin/provider). Los conflictos y los proveedores resueltos se muestran en /stats y en alisio plugins doctor:

sh
alisio plugins doctor --plugin ./dist/index.js

Contextos tipados ​

TipoCampos
TerminalCapabilitiescolor (ANSI SGR permitido), unicode (no ASCII permitido), columns, interactive
MascotContextterminal, version
PluginMetadataid, version, builtin
StartupFactlabel, value
StartupContextversion, cwd (ya abreviado), model?, provider? (solo el host, nunca credenciales), userName?, terminal, plugins (PluginMetadata[]), mascot (la mascota resuelta y validada), tips, facts? (datos del host como el acceso y el estado de la memoria)

StartupContext.mascot siempre puede llamarse con seguridad: una pantalla personalizada puede reutilizar la mascota ganadora.

Reglas de seguridad ​

  • Los proveedores reciben solo su contexto: no deben leer globales, variables de entorno ni el sistema de archivos. Respeta terminal.color, terminal.unicode y terminal.columns.
  • Un proveedor que lanza una excepción, devuelve algo distinto de un string o un array de strings, devuelve demasiadas líneas (12 para una mascota, 60 para una pantalla) o tarda más de 250 ms se reemplaza por el valor por defecto, y se registra un diagnóstico plugin_hook_failed. El renderizado es síncrono, así que un proveedor lento no puede interrumpirse: su salida se descarta después.
  • La salida se sanea: solo se conservan las secuencias de color SGR, y solo cuando color es true; se eliminan las demás secuencias de escape y los caracteres de control; los tabuladores se convierten en espacios; los caracteres no ASCII se convierten en ? cuando unicode es false. Cada línea se recorta a columns.
  • La pantalla por defecto se construye con funciones de sección reutilizables que exporta @alisio/core: titleSection, welcomeSection, infoSection, pluginsSection, tipsSection, mascotSection, composeSideBySide, shortenPath y startupTips (determinista para una semilla dada). Muestra la mascota junto a la información cuando la terminal es lo bastante ancha, y apiladas en caso contrario.

Ejemplo: mascota cometa ​

El repositorio incluye un paquete de ejemplo en examples/plugins/custom-mascot (alisio-plugin-kite-mascot, id de complemento kite-mascot). Solo depende de @alisio/sdk (dependencia peer) y distribuye JavaScript.

ts
import { definePlugin, type MascotProvider, type StartupScreenProvider } from "@alisio/sdk";

/** A kite riding the trade winds. Honors unicode/color/columns from the context only. */
export const kiteMascot: MascotProvider = {
  id: "kite",
  render({ terminal }) {
    if (terminal.columns < 40) return terminal.unicode ? "◇~ kite" : "<>~ kite";
    const art = terminal.unicode
      ? ["   ◢◣", "  ◢██◣", "  ◥██◤", "   ◥◤", "    ╲", "     ∿∿"]
      : ["   /\\", "  /  \\", "  \\  /", "   \\/", "    \\", "     ~~"];
    return terminal.color ? art.map((line) => `\u001b[35m${line}\u001b[0m`) : art;
  },
};

/** Optional compact screen that reuses whichever mascot won resolution. */
export const compactScreen: StartupScreenProvider = {
  id: "kite.compact",
  render(ctx) {
    const mascot = [ctx.mascot.render({ terminal: ctx.terminal, version: ctx.version })].flat();
    return [
      ...mascot,
      `Alisio ${ctx.version} · ${ctx.model ?? "no model"} · ${ctx.plugins.length} plugin(s)`,
      ...ctx.tips.slice(0, 1),
    ];
  },
};

export default definePlugin({
  id: "kite-mascot",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    // Priority 10 beats plugins registering at the default priority 0.
    api.extensions.register("mascot", kiteMascot, { priority: 10 });
    // Uncomment to also replace the whole startup screen:
    // api.extensions.register("startup-screen", compactScreen);
  },
});
sh
npm run build
alisio --plugin ./examples/plugins/custom-mascot/dist/index.js

Inicio de sesión del proveedor (auth) ​

Una registración de proveedor puede declarar auth: ProviderAuthMethod[] para que el usuario inicie sesión con una cuenta en lugar de pegar una clave. El host ejecuta el flujo, guarda la sesión, la renueva y la redacta; el complemento solo habla con su propio servicio.

ts
api.providers.register({
  id: "my-service",
  name: "My service",
  fields: [], // sin campo secreto obligatorio: el proveedor es solo de inicio de sesión
  auth: [
    {
      id: "browser", // ^[a-z0-9_-]{1,40}$, único dentro de la registración
      label: "Cuenta de My service",
      kind: "oauth",
      description: "Abre la página de inicio de sesión de My service.",
      async start(ctx) {
        const loopback = await ctx.loopback?.({ port: 0, path: "/callback", state }).catch(() => undefined);
        ctx.show({ type: "auth_url", url: authorizationUrl, instructions: "Aprueba el acceso." });
        const code = loopback ? (await loopback.wait()).code : await ctx.prompt({ message: "Pega la dirección a la que te llevó", secret: true });
        const tokens = await exchange(code, ctx.signal);
        return { type: "oauth", access: tokens.access, refresh: tokens.refresh, expires: tokens.expiresAt };
      },
      async refresh(credential, signal) {
        /* devuelve la credencial renovada; lanza un error con retryable = true si el fallo es temporal */
      },
    },
  ],
  create: (request) => makeClient(() => request.oauth?.get()), // request.oauth existe solo si se declara `auth`
});
  • start(ctx) resuelve con la credencial o rechaza; ctx.signal se aborta al cancelar, reemplazar o expirar (15 minutos). ctx.show acepta pasos auth_url, device_code y progress; ctx.prompt pide un valor al usuario y resuelve undefined donde nadie puede responder (alisio run). ctx.loopback existe solo cuando el host puede suponer que el navegador está en la misma máquina (Alisio Desktop, o un servidor enlazado a loopback); si no, usa prompt. Un túnel puede romper esa suposición, así que ofrece también pegar la dirección.
  • Las URL de los pasos deben ser https: (o http: en localhost/127.0.0.1) sin credenciales; si no, el flujo termina con unsafe_url.
  • Marca unofficial: true cuando el proveedor no haya autorizado oficialmente el método: la interfaz web muestra un aviso de riesgo fijo y pide confirmación. No lo marques en un método oficial.
  • label, description, instructions y prompt.message se muestran como texto plano. Pon en ellos cualquier texto propio del proveedor: el host solo añade botones y estados genéricos.
  • Sin refresh la sesión no se renueva: al caducar, el perfil necesita iniciar sesión de nuevo. Un rechazo con retryable === true es un fallo temporal; cualquier otro es permanente. La metadata de la renovación se fusiona sobre la guardada.
  • request.oauth.get() devuelve una credencial válida (renovando antes) e invalidate("sign_in_required") avisa al host de que el proveedor rechazó la sesión.
  • Un complemento es código de confianza que corre en el proceso del host: puede leer la sesión guardada. El host reduce la superficie (el asa existe solo con auth y está ligada al perfil), pero un complemento nunca es un sandbox.

Ejemplo: proveedor de búsqueda personalizado ​

examples/plugins/custom-websearch (alisio-plugin-brave-websearch, id de complemento brave-websearch) envuelve Brave Search con una clave de API proporcionada por el usuario mediante el punto de extensión websearch — una referencia copiable para "trae tu propio motor de búsqueda":

ts
import { definePlugin, type SearchProvider, type SearchResult } from "@alisio/sdk";

export function braveSearchProvider(apiKey: string): SearchProvider {
  return {
    id: "brave-example",
    async search(query, options): Promise<SearchResult[]> {
      const url = new URL("https://api.search.brave.com/res/v1/web/search");
      url.searchParams.set("q", query);
      const res = await fetch(url, {
        headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
        signal: options?.signal,
      });
      if (!res.ok) throw new Error(`Brave Search request failed: HTTP ${res.status}`);
      const body = await res.json();
      return (body.web?.results ?? []).map((r: { title?: string; url?: string; description?: string }) => ({
        title: r.title ?? "",
        url: r.url ?? "",
        snippet: r.description ?? "",
      }));
    },
  };
}

export default definePlugin({
  id: "brave-websearch",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    const apiKey = process.env.BRAVE_SEARCH_API_KEY;
    if (!apiKey) throw new Error("brave-websearch: set BRAVE_SEARCH_API_KEY");
    api.extensions.register("websearch", braveSearchProvider(apiKey), { priority: 10 });
  },
});
sh
npm run build
export BRAVE_SEARCH_API_KEY=...
alisio --plugin ./examples/plugins/custom-websearch/dist/index.js --allow-external

Cargar complementos ​

Los complementos solo se cargan desde orígenes de confianza explícita. Alisio instala un complemento solo cuando se lo pides: el comando alisio install, los botones de instalación de la interfaz web y de la aplicación de escritorio, o la herramienta plugin_install tras su aprobación.

OrigenConfianza
--plugin <path>, --plugin <package>Confianza explícita en esa entrada y sus dependencias
plugins en el archivo de configuraciónEl propio archivo es de confianza (configuración global, --config o --trust-project)
<config home>/plugins/ (archivos o directorios)Complementos globales: código personal de confianza
<workspace>/.alisio/plugins/Solo con --trust-project
sh
alisio --plugin ./my-plugin.js
alisio --plugin alisio-plugin-foo
json
{ "plugins": ["alisio-plugin-foo"] }

--read-only desactiva los complementos ejecutables (externos). Los complementos integrados no se ven afectados.

Resolución. Una entrada con forma de ruta (empieza por ., es absoluta, contiene \, termina en una extensión JS/TS, o contiene / sin empezar por @) es un archivo o directorio. Cualquier otra cosa es un nombre de paquete npm, que se busca en node_modules desde el directorio del proyecto hacia arriba y después en las raíces globales: las entradas de NODE_PATH y el prefijo global del Node/npm en ejecución.

La palabra clave alisio-plugin es obligatoria. Un paquete sin "keywords": ["alisio-plugin"] en su package.json se rechaza, para que un error tipográfico no cargue un paquete no relacionado. La entrada se toma de exports["."] (condiciones import, node o default), después de main y, por último, de ./index.js.

Icono del complemento (opcional). La interfaz web y la app de escritorio muestran un icono junto a cada complemento. Uno del catálogo usa el icon del catálogo. Cualquier otro complemento de npm puede incluir el suyo declarando en package.json una ruta relativa al paquete, y listando el archivo en files: "alisio": { "icon": "assets/icon.svg" }. El archivo debe ser SVG, PNG, WebP o JPEG de 256 KB como máximo y quedarse dentro del paquete (se ignora una ruta que salga de él, una ruta absoluta o un enlace simbólico hacia fuera); el servidor lo entrega solo como imagen, nunca como página. Sin icono (o si no carga) se muestra el icono predeterminado. Los complementos integrados, de proyecto y de archivo usan el predeterminado salvo que sean paquetes de npm que declaren uno.

Un directorio indicado como ruta debe contener un manifiesto alisio-plugin.json que declare su entrada (un paquete también puede incluirlo); la entrada debe permanecer dentro del directorio:

json
{ "apiVersion": 1, "entry": "./index.js" }

Comprueba lo que registra un complemento con:

sh
alisio plugins list
alisio plugins doctor --plugin ./my-plugin.js

Instalar complementos desde npm ​

La CLI puede instalar un paquete npm en el directorio GLOBAL de complementos de Alisio (<config home>/plugins, por ejemplo ~/.config/alisio/plugins) y registrar su NOMBRE npm en el array plugins de la configuración global — la misma entrada que hoy puedes escribir a mano:

sh
alisio install npm:plugin-openrouter          # última versión
alisio install npm:@scope/plugin-x@1.2.3      # una versión fijada
alisio install plugin-openrouter              # el nombre pelado es igual que npm:
alisio install npm:plugin-openrouter --update # actualiza un complemento instalado a @latest
alisio install --update                       # actualiza TODOS los complementos instalados en una sola llamada de npm

La especificación se valida antes de cualquier operación de red: npm:<paquete>[@<versión>], o un nombre de paquete pelado; solo se permiten letras, dígitos, ., _, -, más / para nombres scoped y @ para una versión. Los prefijos desconocidos (git:, file:, URLs registry:, ...) se rechazan con un error claro. Un fallo de instalación nunca vuelve a ejecutar npm en silencio y la salida de npm fallida se sanea (sin tokens/secretos); el error nombra el paquete y el comando de reintento exacto.

Dónde aterriza. El paquete se instala con npm install --prefix <config home>/plugins en <config home>/plugins/node_modules/<paquete> y su NOMBRE (nunca la ruta resuelta en el filesystem) se añade al array plugins de <config home>/config.json (escritura atómica, campos no relacionados conservados, sin duplicados). alisio plugins list muestra los paquetes instalados junto a los complementos de archivos/directorios.

AspectoComportamiento
Alcance globalUna instalación sirve a todos los proyectos desde los que ejecutes Alisio
CargaEl complemento se carga como código ejecutable en proceso dondequiera que carguen los complementos globales; debe declarar la keyword alisio-plugin para poder cargarse
Confianza del proyectoLa configuración y los complementos PROPIOS de un proyecto solo cargan en proyectos de confianza (--trust-project o el aviso de confianza de una sola vez)
--read-onlySe rechaza instalar y el complemento nunca carga (los complementos ejecutables están desactivados)
ReinstalaciónSi el paquete ya está instalado, el comando lo indica y sugiere --update en lugar de volver a ejecutar npm
Scriptsnpm install puede ejecutar scripts de ciclo de vida del paquete con tus privilegios — Alisio avisa y exige confirmación en terminal interactiva
Headless / --jsonNunca pregunta: sin un --yes explícito (o --trust-plugin) falla con un error accionable antes de ejecutar npm
ErroresSalida npm saneada, nombre del paquete y comando de reintento exacto en el mensaje final
--update sin specActualiza a @latest todos los complementos instalados en el directorio global de complementos con una sola llamada a npm install --prefix; mismas reglas de confirmación, --yes y --read-only; sin complementos instalados lo indica y termina con código 0

La instalación es una acción global del usuario: no concede nada a ningún proyecto. La carga sigue la política existente de complementos ejecutables — el aviso de confianza de una sola vez (o --trust-project) es lo que permite a un proyecto cargar su propia configuración y complementos, y --read-only desactiva los complementos ejecutables por completo.

Conflictos de dependencias peer (ERESOLVE) ​

Los complementos antiguos ya instalados pueden fijar un @alisio/sdk anterior mediante su rango peer, por lo que npm rechaza instalar un complemento nuevo que necesita un SDK más reciente (ERESOLVE ... Conflicting peer dependency). Alisio explica entonces la causa, lista los complementos instalados implicados y no muestra la línea genérica de reintento, porque el reintento fallaría igual. Actualiza todos los complementos instalados juntos (hacerlo uno a uno vuelve a chocar con el mismo conflicto) y luego repite la instalación original:

sh
alisio install --update
alisio install npm:@alisio/plugin-laya

Alisio nunca sugiere --force ni --legacy-peer-deps para esto.

En la interfaz web (Ajustes → Complementos → Complementos instalados) esto es automático. Actualizar todo ejecuta un único npm install combinado de todos los complementos del catálogo con actualización. Actualizar en un solo complemento prueba primero ese complemento solo; si npm informa ERESOLVE, Alisio reintenta exactamente una vez como una sola instalación de ese complemento más los demás complementos del catálogo instalados que tienen una versión más reciente y compatible (los complementos locales .tgz y los que ya están al día no se tocan) y lista los complementos que se actualizaron junto con él. Si el conflicto persiste, no se intenta nada más y el error nombra los complementos implicados.

El agente puede instalar un complemento por ti ​

En la TUI, el modelo puede instalar un complemento a petición mediante la herramienta del host plugin_install: valida la especificación, ejecuta la misma rutina de instalación que alisio install y responde con el nombre del paquete, la versión instalada, la entrada de configuración y la ruta, más la nota de confianza. La herramienta usa el efecto process, así que pasa por la compuerta de permisos habitual — el aviso de aprobación en la TUI, o --allow-process en headless — y nunca está disponible con --read-only. Consulta Herramientas y permisos.

text
Tú:    instala el complemento plugin-openrouter
Agente: (llama a plugin_install con spec "npm:plugin-openrouter", tú lo apruebas)
Agente: Instalado el complemento "plugin-openrouter" v1.2.3 en
        ~/.config/alisio/plugins/node_modules/plugin-openrouter y añadido al array
        "plugins" de ~/.config/alisio/config.json. El complemento carga como código
        personal de confianza; --read-only impide que cargue. Ejecuta `alisio plugins
        list` para inspeccionarlo.

Instalar desde la web y desde un archivo ​

La interfaz web y la aplicación de escritorio instalan, actualizan y eliminan complementos en Ajustes → Complementos, con la misma advertencia de seguridad que la CLI. Las pantallas están en Catálogo de complementos, instalación y actualizaciones.

  • Desde el catálogo. Un complemento del catálogo se instala con un clic, en la versión del catálogo (la última del registro cuando npm aún no sirve esa versión). Solo se pueden instalar así los paquetes listados en el catálogo, y --read-only lo rechaza.
  • Actualizar en el sitio. Instalar un paquete que ya está instalado lo reemplaza (actualización, reinstalación o versión anterior) y conserva una sola entrada en el array plugins. Una actualización fallida restaura la versión anterior.
  • Desde un archivo (complementos privados). Un complemento que nunca se publica se distribuye como tarball. Se genera desde la carpeta del complemento (o deja que el creador de complementos lo cree y lo empaquete por ti):
sh
pnpm pack
npm pack

Después elige el .tgz en Instalar desde archivo. Alisio lee solo package/package.json (un lector gzip y tar propio: no hace falta el binario tar) y comprueba que sea un complemento de Alisio: la keyword alisio-plugin, un nombre npm válido y una versión semver, y un punto de entrada (exports o main). El archivo puede pesar hasta 50 MB y package.json hasta 1 MB; una entrada cuya ruta pudiera salir de la carpeta del paquete rechaza el archivo. Un rango del peer @alisio/sdk que no coincide con el SDK instalado es una advertencia, y los scripts preinstall, install o postinstall declarados se destacan.

  • Dónde vive un complemento de archivo. El tarball se copia a <config home>/plugins/local-packages/<nombre>-<versión>.tgz y npm instala desde esa copia, así que la dependencia file:local-packages/... de <config home>/plugins/package.json nunca apunta a un archivo que pueda moverse. Actualizar reemplaza la copia guardada; desinstalar la elimina; un complemento instalado desde una ruta fuera de local-packages se migra en su siguiente actualización. La carga se guarda temporalmente en <state home>/tmp/plugin-uploads/ y se borra tras la instalación, un fallo o una cancelación; lo que quede con más de una hora se limpia. Una instalación fallida se revierte: la copia guardada, un paquete parcial y la entrada de configuración se restauran.
  • Reinicio tras una actualización. Node conserva el código de un complemento que ya cargó, así que la actualización de un complemento cargado solo se ejecuta tras un reinicio. El resultado lo indica; la aplicación de escritorio puede reiniciarse sola y alisio serve debes reiniciarlo tú.
  • npm. La CLI usa el npm de tu PATH. La aplicación de escritorio incluye npm (ALISIO_NPM_CLI lo señala), así que instala complementos sin Node.js en el sistema. Sin ninguno de los dos, el error dice: npm was not found: install Node.js, or use the desktop app, which bundles it.
  • Desinstalar. DELETE /api/plugins/package/:name (el botón Desinstalar) elimina un complemento de npm del directorio global de complementos y su nombre de la configuración; los complementos integrados y los de proyecto o locales no se pueden desinstalar desde la web.

Creador de complementos ​

El creador de complementos de la interfaz web construye complementos a partir de una petición en lenguaje natural. Es una herramienta para usuarios no técnicos; esta sección es la referencia de lo que genera y expone.

Proyectos y el agente creador ​

Los proyectos viven en <config home>/plugin-creator/projects/<id>/ (<id> son 12 caracteres hexadecimales); el registro de metadatos, el último paquete y la skill incluida se guardan junto a ellos en records/, builds/ y skills/. Cada proyecto es además el workspace de su sesión creadora, que ejecuta el agente plugin-creator con el preset de permisos ask y la skill incluida alisio-plugin-creator. El agente tiene dos herramientas acotadas en lugar de un shell: plugin_project (internal: validación estática, estadísticas y pasos de progreso) y plugin_project_run (process, así que pasa por la aprobación normal: la suite node --test del proyecto o la preparación completa, con entorno depurado y límite de tiempo). Las aprobaciones de una sesión creadora solo se muestran en su página mientras está abierta.

Estructura del complemento generado ​

text
package.json          name, version, type: module, alisio.pluginId / icon / cover
src/index.js          ESM simple + JSDoc, el export por defecto es el objeto Plugin
test/harness.js       un PluginAPI falso con una base node:sqlite real en memoria
test/*.test.js        suites de node:test (npm test = node --test test/)
assets/icon.svg       icono provisional de 64x64
assets/cover.svg      portada provisional de 1280x720 con el nombre del complemento
README.md

El contrato: JavaScript ESM simple, sin paso de compilación, sin dependencias en tiempo de ejecución, @alisio/sdk solo como peer opcional (nada lo importa en ejecución), la palabra clave alisio-plugin, un id de complemento con prefijo local. y la versión solo en package.json (src/index.js la lee). Los paquetes se llaman alisio-plugin-local-<slug>-<id> y son privados (UNLICENSED) hasta que los publiques.

Icono y portada ​

package.json apunta alisio.icon y alisio.cover a archivos dentro del paquete (y dentro de files). La plantilla escribe un icono SVG y una portada con el nombre visible escapado en XML, acortado a partir de 40 caracteres. El modelo puede reemplazarlos por un SVG optimizado (o un PNG, WebP o JPEG); la validación detecta el contenido, limita el tamaño (icono 256 KB, portada 512 KB) y rechaza SVG inseguro: scripts, atributos de manejadores de eventos, foreignObject, imágenes incrustadas, destinos externos en href o url(...), @import y entidades XML.

Validación y compatibilidad ​

Una preparación valida de forma estática, ejecuta las pruebas y una sonda que importa el complemento, y luego empaqueta un .tgz determinista. Los hallazgos tienen códigos estables (por ejemplo sdk_incompatible, api_member_unknown, forbidden_import, template_example, svg_unsafe, tests_missing). El rango de peerDependencies["@alisio/sdk"] debe aceptar el SDK instalado (los proyectos nuevos usan el rango con acento circunflejo de su minor, como ^0.11.0) y los miembros de PluginAPI que usan las fuentes se comprueban contra esa versión. Límites: 500 archivos, 5 MB por proyecto, 1 MB por archivo, 8000 caracteres por petición, 120 s por ejecución de pruebas. Preparar ejecuta el código del proyecto con los privilegios del usuario; no es un sandbox, y node --test tampoco lo es.

Rutas HTTP ​

Todas las rutas cuelgan de /api/plugin-creator/projects y toda mutación se rechaza con --read-only.

Método y rutaPropósito
GET / y POST /Lista los proyectos; crea uno desde request, con displayName y model opcionales
GET /:id y GET /:id/statusUn proyecto con su estado; estado de instalación y si hay una actualización o un paquete listo
DELETE /:idElimina (uninstall: true también desinstala el paquete)
POST /:id/sessionAbre o reanuda la sesión creadora, opcionalmente con una request y un model nuevos
GET /:id/files y GET /:id/files/*Lista de archivos y un archivo de texto (solo lectura, acotado)
GET /:id/iconEl icono del proyecto
POST /:id/buildValida, prueba y empaqueta; un fallo es 422 con la respuesta de la preparación
GET /:id/packageDescarga el .tgz (plugin_not_built si está desactualizado)
POST /:id/installVuelve a preparar y luego instala con el trabajo de instalación de archivo local; requiere confirmed: true

Complementos TypeScript ​

  • Los complementos publicados en npm deben distribuir JavaScript.
  • Los complementos .ts locales cargados con --plugin funcionan en Bun (y en el binario independiente) y en Node.js >= 22.18 mediante type stripping, que solo admite sintaxis borrable (sin enum, namespace ni parameter properties).
  • En versiones anteriores de Node.js, Alisio falla con un error claro que pide Bun, Node.js >= 22.18 o una compilación a JavaScript.

Paquete de complemento de ejemplo ​

text
alisio-plugin-hello/
├── package.json
├── tsconfig.json
└── src/
    └── index.ts

package.json:

json
{
  "name": "alisio-plugin-hello",
  "version": "0.1.0",
  "description": "Example Alisio plugin",
  "keywords": ["alisio-plugin"],
  "license": "MIT",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "@alisio/sdk": "^0.2.0"
  },
  "devDependencies": {
    "@alisio/sdk": "^0.2.0",
    "typescript": "^5.9.0"
  }
}

Usa la última versión publicada de @alisio/sdk al crear tu propio complemento — consulta npm view @alisio/sdk version para saber cuál es. Los complementos deben declarar @alisio/sdk como dependencia peer con ^0.2.0: mientras Alisio sea 0.x, un rango con circunflejo solo acepta su propia versión menor, porque una versión menor nueva puede incluir cambios incompatibles, así que adoptas cada versión menor de forma explícita. Un complemento que deba funcionar con ambas líneas puede declarar "^0.1.0 || ^0.2.0"; los complementos publicados para 0.1 (como @alisio/plugin-deepseek) declaran ^0.1.0 hasta que se actualicen.

tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "skipLibCheck": true
  },
  "include": ["src"]
}

src/index.ts:

ts
import { definePlugin, textResult } from "@alisio/sdk";

export default definePlugin({
  id: "acme.hello",
  version: "0.1.0",
  apiVersion: 1,
  setup(api) {
    let greetings = 0;

    api.tools.register({
      name: "hello",
      description: "Greets a person by name.",
      effect: "read",
      inputSchema: {
        type: "object",
        properties: { name: { type: "string" } },
        required: ["name"],
        additionalProperties: false,
      },
      async execute(input) {
        greetings += 1;
        api.ui.status("greetings", `hello ${greetings}`);
        return textResult(`Hello, ${String(input.name)}!`);
      },
    });

    api.commands.register("greetings", async () => `Greetings so far: ${greetings}`, {
      description: "Show how many greetings were sent",
    });
  },
});

Compílalo y pruébalo:

sh
npm install
npm run build
alisio --plugin ./dist/index.js         # local build, as an explicit path
npm publish                             # then: npm i -g alisio-plugin-hello
alisio --plugin alisio-plugin-hello     # or add it to "plugins" in the configuration

En la TUI el comando está disponible como /acme.hello:greetings, y el modelo ve la herramienta con su nombre con prefijo.

Publicado bajo la licencia MIT.