El Último CLI: Reconstruyendo la Base 51 de las Herramientas que Fui Construido Contra
Cada modelo buscó un conjunto diferente de bibliotecas, así que construí una base y convertí cada proyecto en una rama superior dentro de ella. Luego le pregunté a Claude Fable a qué CLI la humanidad aún podría estar usando en una década, y Opus 5 y yo construimos lo que regresó — 46 cláusulas, 92 pruebas, cero dependencias.
Developed by Robert E. Beckner III (Merlin) | rbeckner.com
Tengo 51 herramientas de línea de comandos en esta máquina. No planeé ese número. Ocurrió porque un CLI es la distancia más corta entre una idea y algo que realmente puedo ejecutar, y porque durante los últimos años he tenido ayuda para escribirlos más rápido de lo que podría haber hecho solo.
Esa ayuda vino con un hábito que noté temprano, cuando GPT-3.5 y los primeros modelos Claude eran los que usaba. Pide a 3 modelos diferentes que esbocen un CLI y obtienes 3 opiniones diferentes sobre qué es un CLI. Uno busca a Commander. Uno busca a Inquirer para la generación de prompts. Uno busca a Chalk porque la salida debe ser coloreada. Cada respuesta es defensible. Juntos son un impuesto, porque ahora poseo 3 bases de código que discrepan sobre el análisis de argumentos, sobre cómo se ve un fallo, y sobre cuál de esas bibliotecas ahora soy responsable de vigilar.
Cada modelo buscó un conjunto diferente de bibliotecas, y yo fui el que tuvo que vivir con todas ellas#
El problema no son las bibliotecas. Es que las mejoras dejan de viajar.
Cuando 3 CLIs no están de acuerdo sobre cómo un comando reporta fallo, una corrección en uno es una corrección en uno. No hay nada a lo que subirlo. El trabajo no se compone, y después de la décima herramienta no estás construyendo apalancamiento, estás manteniendo un portafolio de casi fallos.
Ya había escrito sobre querer lo opuesto a eso. Todo el argumento en How to Turn AI Gains Into Compounding Infrastructure es que una ganancia se vuelve duradera cuando cada proyecto dependiente la hereda. Una superficie de capacidad compartida. Una regla de promoción. Un lugar donde una mejora llega y se difunde.
Construí esa capa para la capacidad de IA, para el flujo de trabajo, para las operaciones. No la construí para la cosa que realmente hago más a menudo.
Así que construí una base y hice que cada proyecto CLI incorporara sus mejoras en ella#
La regla era simple y era mía para hacerla cumplir: cuando un CLI en mi patrimonio necesitaba algo mejor — una forma más limpia de registrar servicios, un camino de error mejor, un ayudante de pruebas que hiciera una suite legible — esa mejora no se quedaba en el proyecto. Entró en la base, y la base salió a los demás.
Ese es todo el diseño. La base es pequeña a propósito. No tiene opinión sobre lo que tu herramienta hace. Tiene una opinión fuerte sobre lo que es un comando es: algo que toma argumentos, hace trabajo, informa lo que sucedió y deja.
La carpeta se creó el 6 de julio de 2025, y 2 de mis herramientas dependían de la versión 1.0.0 de ella el mismo día. Eso es la señal: no se construyó especulativamente y luego adoptado. Se extrajo de un trabajo que ya existía, en el punto donde copiar la misma estructura entre proyectos dejó de ser razonable.
Se difundió rápido, porque difundir era toda la idea. 8 repositorios estaban en ella dentro de 25 días. 10 dentro de 11 semanas.
Repositories Adopting the Base in 2025Chart data
repositories
Jul 6
2
Jul 8
4
Jul 17
5
Jul 23
7
Jul 30
8
Sep 20
10
Git llegó después de todo eso. El repositorio se inicializó el 12 de noviembre de 2025, 4 meses después, y se publicó al día siguiente — por eso el historial de versiones y el historial real no coinciden, y por eso revisé el sistema de archivos en lugar de confiar en el registro de commits cuando me senté a escribir esto.
Esos 10 herramientas hacen administración de Cloudflare. Local DNS y gestión de nginx. Despliegue contra Coolify. Automatización de navegador. Reporte de costos entre proveedores de modelos. La mayoría son privados, por eso los describo por lo que hacen en lugar de por nombre. Los públicos son aia, que consultan varios modelos en paralelo, y la base misma. vssh, mi herramienta de ejecución remota protegida, también es pública y surgió del mismo instinto — construir la superficie del operador una vez, correctamente, y dejar de reconstruirla.
Los dividendos fueron reales y aburridos, que es la forma correcta para los dividendos de infraestructura. Un endurecimiento en una herramienta apareció en todas ellas. Cuando descubrí que un comando podía imprimir un mensaje de error rojo y aun así salir 0 — diciendo al humano que falló y al shell que funcionó — la corrección no entró en los 16 lugares en una herramienta donde había ocurrido. Entró en la base, y cada herramienta lo heredó.
Después de 13 meses quería que se reconstruyera, no que se parcheara#
Para agosto de 2026 la base estaba funcionando y todavía quería que se fuera.
No porque estuviera rota. Porque había acumulado. Porque la regla de exit-code de la que más orgulloso estaba había sido adaptada en lugar de diseñada desde el principio. Porque el mundo para el que se escribió había cambiado debajo de ella: la mayoría de las invocaciones de mis CLIs ya no las escribo yo. Se emiten por agentes, leyendo stdout, stderr y $? como sus únicos sentidos.
Así que en lugar de parchear, cambié los términos. Le di a Claude Fable una sola instrucción, y la hice deliberadamente grande:
Si esto fuera el último marco de CLI que la humanidad construyó — el que todavía está en servicio
en una década — ahora tienes la oportunidad de hacerlo.
Diseñalo desde allí. No esperaba un documento de vuelta. Esperaba un plan.
Fable regresó con un tratado, y la restricción era que las promesas tuvieran que ser pocas#
Lo que llegó no fue una lista de características. Estaba estructurado como un tratado, dividido por la mitad por un muro sólido.
Una mitad era un contrato: lo que cada CLI construido sobre esta base garantiza a cada observador, escrito como cláusulas numeradas en el lenguaje RFC-2119 — DEBE, NO DEBE, DEBERÍA, PUEDA. Doce familias de ellos. Códigos de salida. Disciplina de flujo. Salida de la máquina. Auto-descripción. Gramática. Entorno. Cancelación. Determinismo. Presupuestos de rendimiento. Compatibilidad.
La otra mitad era la superficie de autoría, que estaba permitida crecer, y que existía solo para hacer que cumplir el contrato fuera el camino de menor resistencia.
La razonamiento debajo de él fue la parte que me resultó convincente. Un diseño pensado para durar una década no puede apostar por la moda, porque la moda es lo que expira. No puede apostar por la ingeniosidad, porque la ingeniosidad es lo que no puedes predecir en el año 8. Solo puede apostar por las interfaces que no han cambiado desde los 1970s: vectores de argumentos, flujos 3, un código de salida de 8-bits, variables de entorno. Y notó el único hecho genuinamente nuevo — que el lector mayoritario de esas interfaces ahora es una máquina que no puede hacer una pregunta de seguimiento.
La cláusula que terminó organizando todo lo demás fue la que abrió con:
Un resultado, muchas representaciones. Un comando calcula un solo resultado. La salida
código, el texto humano, el documento JSON y las líneas de streaming son todas
proyecciones de ese único valor. No pueden contradecirse, porque
solo hay una fuente.
Esa es la frase en la que gira toda la reconstrucción.
Diagram source
graph LR
A["execute() devuelve
un valor"] --> B["código de salida"]
A --> C["texto renderizado
stdout"]
A --> D["JSON sobrecapa
--json"]
A --> E["flujo NDJSON
--ndjson"]
F["logger.error()
ctx.emit()"] -.-> B
F -.-> G["eventos
stderr"]
Opus 5 y encontré que la especificación estaba correcta sobre la tesis y equivocada sobre 3 cosas#
Aquí es donde el trabajo se volvió nuestro en lugar de mío.
Llevé la especificación a Opus 5 y la construimos en un día. No fue un día limpio. Las partes útiles son los lugares donde el documento cumplió con la propiedad y perdió.
La especificación quería que ctx.args se convirtiera en un registro de argumentos nombrados. Es el
mejor diseño en aislamiento. También habría roto cada comando en cada uno
de las 10 herramientas, porque todas leen ctx.args como un arreglo. Mantenemos el arreglo
y colocamos los argumentos tipados en ctx.namedArgs junto a él. La regla que decidió
ya estaba escrito en el contrato, una cláusula arriba: nunca rompas un
el consumidor supera cualquier otro valor en el repositorio, incluyendo el del contrato
propia completitud.
La especificación quería que un grupo de comandos sin verbo fuera un error de uso. Ejecutar un
el comando padre sin subcomando saldría 2. Defendible, y habría
cambiado el comportamiento de cada script que ejecuta un comando de agrupación sin subcomando para ver
su ayuda. Seguimos imprimiendo ayuda y saliendo 0.
La especificación asumió que el streaming y el único documento JSON eran lo mismo
característica.** No lo son. Transmitir un millón de ítems en memoria constante es el
punto de uno e imposible en el otro, porque un llamador que pidió un
documento único pidió que fuera único. Dividimos el comportamiento y escribimos
qué cláusula rige cuál.
También encontramos cosas que la especificación no podría haber sabido, porque solo eran visibles desde el artefacto. Un archivo de prueba que ejecutó 0 pruebas y reportó éxito, habiendo matado al ejecutor a mitad de camino. Manejo de señales que salió 0 al Ctrl-C — un comando interrumpido que reportó que había tenido éxito. Dos ayudantes de salida que unieron sus líneas con una barra invertida-n literal, así cada tabla volvió en una sola línea. Un ayudante de color que, una vez que reemplazamos la dependencia que había envuelto, redujo silenciosamente su propia firma de tipo y rompió código que no había cambiado un carácter.
Ese último vale la pena sentarse con él. No fue capturado por ninguna prueba que ninguno de nosotros escribiera. Apareció en la verificación de tipos de un consumidor durante la migración, que es el único lugar donde podría haber.
El contrato solo cuenta porque la compilación falla cuando una cláusula no tiene prueba#
Una promesa que nada verifica es un comentario.
Así que el conjunto de conformidad analiza el archivo de contrato, encuentra cada cláusula que contiene la palabra MUST, y falla la compilación si una de ellas no tiene prueba registrada. No puedes agregar una promesa a este proyecto sin agregar la cosa que la pruebe, en el mismo commit.
Conformance Tests by Contract FamilyChart data
Value
Grammar
20
Exit codes (truth)
12
Machine output
11
Self-description
10
Environment
8
Prompt safety
6
Streams
5
Cancellation
5
Determinism
5
46 cláusulas normativas. 92 pruebas mapeadas a ellas. 184 pruebas en total.
Y ninguna de esas pruebas de conformidad se ejecuta contra el código fuente. Construyen el paquete con su propio script de compilación, ejecutan npm pack, descomprimen el tarball, escriben CLIs de prueba que importan el punto de entrada descomprimido, y los generan bajo Node, Bun y Deno — afirmando sobre el estado de salida y los bytes exactamente como un shell los vería.
Esa forma no fue una elección estética. Este paquete una vez envió un 65 stub de KB. Una sola bandera "sideEffects": false dejó que el empaquetador eliminara el enrutador y el módulo de código de salida del artefacto mientras sus nombres permanecían en la lista de exportación. La compilación salió con 0. El conjunto de origen permaneció verde todo el tiempo. Solo el artefacto era evidencia, y nadie estaba mirando el artefacto.
La migración de 7 herramientas encontró 3 puertas que nadie sabía que existían#
Migramos 7 de los 10 CLIs el mismo día, y la migración es donde el diseño obtuvo su calificación real.
El dividendo se registró inmediatamente y no costó nada: porque los comandos en la versión antigua ya devolvían valores — el marco los usaba solo para derivar un código de salida, luego los descartaba — cada uno de esos valores de retorno se convirtió en un JSON payload el día de la actualización. 7 herramientas obtuvieron salida legible por máquina sin que se reescribiera un solo comando.
Lo que no esperábamos era el mismo defecto en 3 herramientas diferentes, ninguna de las cuales sabía de la otra. Cada una tenía una puerta frente al enrutador: una lista mantenida a mano de nombres de comandos válidos, o un paso de inicio que exigía credenciales antes de que se ejecutara cualquier otra cosa. En cada caso el nuevo comando manifest — el que describe toda la superficie de la herramienta en una sola llamada, para que un agente pueda aprenderlo sin leer el código fuente — respondía con "comando desconocido" o "token faltante."
Una de ellas mantenía una segunda copia de su lista de comandos y una pantalla de ayuda escrita a mano, ambas las cuales se habían desviado de lo que la herramienta realmente hacía. Eliminar ambas sacó su conjunto de 52 pasando con 3 fallando a 57 pasando con 0. La herramienta más grande del conjunto tiene 364 pruebas, y pasaron antes y después de la actualización sin un cambio de fuente.
El patrón se generalizó lo suficiente como para convertirse en un procedimiento escrito, enviado dentro del paquete mismo. Es 9 pasos, y los 2 pasos que consumen el tiempo son los 2 que nadie anticipa.
Cero dependencias es el único número que no necesita monitoreo#
La base tenía 2 dependencias de tiempo de ejecución. Ahora no tiene ninguna.
Eso fue parcialmente estético y mayormente aritmético. El 8 de septiembre de 2025, un atacante phishing el account de npm de Josh Junon, mantenedor de algunos de los paquetes más dependientes en JavaScript, usando un dominio falso y un código de un solo uso en vivo. 18 paquetes fueron publicados con versiones maliciosas, incluyendo chalk y debug — paquetes que suman algo en el orden de 2.6 mil millones de descargas a la semana entre ellos. El payload era un crypto-clipper. Los mantenedores lo detectaron y revertieron en aproximadamente 2 horas, y las versiones comprometidas todavía se descargaron alrededor de 2.6 millones de veces en esa ventana.
Chalk es una de las 3 bibliotecas a las que los modelos siguieron recurriendo cuando les pedí un CLI.
La base no se vio afectada — nunca dependía de chalk — y quiero ser preciso en lugar de dramático al respecto, porque fue creada 2 meses después del incidente. La relevancia no es que evitamos algo. Es que el incidente describe la clase de riesgo exactamente: cada dependencia es una década de decisiones de lanzamiento de otra persona, y confías en una cuenta que no controlas. El manejo de color que reemplazó una dependencia trata sobre 60 líneas. La indicación que reemplazó la otra trata sobre 120. Cero es el único número que no necesita monitoreo.
La versión que se lanzó es de 85 KB, sin minificar, sin dependencias de tiempo de ejecución, funcionando en Node, Bun y Deno. Cada comando construido sobre ella obtiene, sin código por comando:
Garantía
Lo que significa en la práctica
Códigos de salida honestos
Un error reportado a un humano se reporta al shell
--json y --ndjson
El valor que devuelve tu comando, en una forma que una máquina pueda analizar
manifest
La herramienta completa descrita en 1 llamada determinista, sin cargar nada
Disciplina de flujo
stdout es la carga útil; cada línea de registro está en stderr
Errores de uso
Salir 2 por "me llamaste mal", distinto de 1 por "intenté y fallé"
Seguridad del prompt
Un prompt sin terminal falla en milisegundos en lugar de colgarse para siempre
Cancelación
Ctrl-C aborta la señal del comando, luego sale 130
Lo que sigo regresando es que no es ningún elemento único en esa lista. Es que la lista ahora es verificable. El propio ejemplo del README se ejecuta como una prueba contra el tarball publicado, y los números citados en su prosa se mantienen a los números que produce el conjunto — una regla que atrapó su primer error dentro de un minuto de haber sido escrito, donde la página decía 87 KB y el artefacto era 85.
La base es de código abierto en github.com/light-merlin-dark/merlin-cli, y el contrato es un archivo en el repositorio en lugar de una reclamación en un sitio web.
Hace cuatro años el problema era que cada modelo tenía una opinión diferente sobre qué debería ser un CLI. La respuesta nunca fue discutir las opiniones. Era poseer la base sobre la que todos construyen, y escribir las promesas en algún lugar donde una construcción pueda fallar.