New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@kikedealba/kit

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@kikedealba/kit

Shared contract for the @kikedealba tools: installed-tool registry, JSON envelope, event bus, agent integrations, credential store, profiles, settings, harness adapters and tool catalog.

latest
Source
npmnpm
Version
0.8.0
Version published
Maintainers
1
Created
Source

kit

El contrato común de las herramientas @kikedealba: den (el punto de entrada), bita (tiempo), nursery (subagentes y reglas), gaze (memoria por proyecto), atl (Atlassian), inkwell (documentación), recap (reuniones), tally (horas en Jira) y Den (la app de escritorio).

Ninguna herramienta depende de otra. Todas dependen solo de kit, y por medio de él se encuentran cuando están instaladas juntas: una herramienta que no está simplemente no aparece, y lo que dependía de ella falla con un error claro y una pista de cómo instalarla.

kit es una librería sin dependencias de ejecución. Trae además una CLI pequeña para ver qué hay instalado.

Requisitos

Node 24 o superior. Funciona en macOS, Windows y Linux.

Las piezas

1. Registro de herramientas instaladas

Cada herramienta, al correr su setup, deja un manifiesto en el registro:

SistemaRuta
macOS / Linux${XDG_CONFIG_HOME:-~/.config}/kikedealba/tools.d/<herramienta>.json
Windows%APPDATA%\kikedealba\tools.d\<herramienta>.json

KIT_REGISTRY_DIR cambia la ruta (útil en pruebas).

{
  "manifestVersion": 1,
  "name": "atl",
  "version": "1.0.0",
  "bin": ["/usr/local/bin/node", "/usr/local/lib/node_modules/@kikedealba/atl/dist/bin/atl.js"],
  "envelope": 1,
  "capabilities": ["jira.issue.write", "confluence.page.write"],
  "emits": [],
  "subscribes": [],
  "mcp": { "stdio": ["atl", "mcp"], "http": "http://127.0.0.1:7781/mcp" },
  "install": "npm install -g @kikedealba/atl"
}
  • bin es la línea de comando completa para invocarla, con rutas absolutas. Quien la llama no depende del PATH.
  • capabilities son cadenas dominio.objeto.acción. Se pregunta por capacidad, no por versión, siempre que se pueda.
import { findTool, invokeTool, requireTool } from '@kikedealba/kit'

const atl = await findTool('atl', { capability: 'confluence.page.write' })
if (atl) {
  const { envelope } = await invokeTool(atl, ['confluence', 'page', 'update', '123', '--md', 'page.md'])
}

const recap = await requireTool('recap', 'npm install -g @kikedealba/recap', { minVersion: '1.0.0' })

requireTool lanza ToolMissingError con code = <HERRAMIENTA>_MISSING y la pista de instalación.

2. Sobre JSON

Todo comando con --json imprime una sola línea con este sobre:

{ "schemaVersion": 3, "ok": true, "command": "ls", "generatedAt": "2026-10-10T12:00:00.000Z", "data": {} }
{ "schemaVersion": 3, "ok": false, "command": "ls", "generatedAt": "…", "error": { "code": "NOT_FOUND", "message": "…", "hint": "…" } }

schemaVersion es la versión de los datos de cada herramienta; la forma del sobre es la misma para todas.

Toda herramienta responde capabilities --json:

{ "schemaVersion": 1, "ok": true, "command": "capabilities", "generatedAt": "…",
  "data": { "name": "atl", "version": "1.0.0", "envelope": 1, "capabilities": ["…"], "emits": [] } }

3. Eventos entre herramientas

Una herramienta declara en emits los eventos que dispara. Otra se suscribe desde su propio manifiesto, sin tocar la configuración de la primera:

"subscribes": [
  {
    "tool": "bita",
    "events": ["start", "stop", "cancel", "amend"],
    "filter": { "kind": ["remote-meeting", "in-person-meeting"] },
    "command": ["/abs/node", "/abs/recap.js", "bita-hook"]
  }
]

El emisor llama a fireEvents, que lee los suscriptores del registro, aplica el filtro y lanza cada comando desacoplado, con el documento del evento por stdin y KIT_EVENT / KIT_EVENT_SOURCE en el entorno. KIT_NO_EVENTS=1 los apaga; así una herramienta que responde a un evento no dispara otro en cadena.

4. Integración con agentes de código

agents.installIntegration instala skills, comandos, subagentes, memoria global, hooks y servidores MCP en los agentes que encuentre: Claude Code, opencode, Codex, Gemini CLI y Antigravity CLI (agy), más los servidores MCP de Claude Desktop.

Claude CodeopencodeCodexGemini CLIAntigravity CLI
Skill~/.claude/skills/<n>~/.config/opencode/skills/<n>~/.agents/skills/<n>extensión skills/<n>plugin skills/<n>
Comandos~/.claude/commands/*.md~/.config/opencode/commands/*.md~/.codex/prompts/*.mdextensión commands/*.tomlno soportado (skipped)
Hookssettings.jsonplugin~/.codex/hooks.jsonextensión hooks/hooks.jsonno soportado
MCPclaude mcp add --scope useropencode.jsonconfig.tomlgemini-extension.json~/.gemini/config/mcp_config.json
Subagentes~/.claude/agents/<n>.md~/.config/opencode/agents/<n>.md~/.codex/agents/<n>.tomlextensión agents/<n>.mdplugin agents/<n>.md
Memoria~/.claude/CLAUDE.md~/.config/opencode/AGENTS.md~/.codex/AGENTS.md~/.gemini/GEMINI.md~/.gemini/GEMINI.md (mismo bloque que Gemini)

Para Gemini se escribe una extensión completa en ~/.gemini/extensions/<herramienta>/. Gemini se detecta por el binario gemini o por la carpeta ~/.gemini/extensions; ~/.gemini sola ya no basta porque Antigravity también la crea.

Para Antigravity kit genera un plugin en <XDG_DATA_HOME>/kikedealba/agents/antigravity/<herramienta>/ (se borra y se regenera en cada instalación) con plugin.json, agents/, skills/ (de skills y userSkills, con el frontmatter reducido a name y description) y rules/<herramienta>.md si la integración trae antigravity.rules (se escribe con trigger: always_on). Luego lo instala con agy:

  • agy plugin list --json; si ya está y su carpeta es idéntica, sale present.
  • agy plugin validate <raíz> --strict --json; si falla, sale failed y no se instala.
  • agy plugin install <raíz>.

Sin agy en el PATH el plugin se genera igual y el paso sale unavailable con el comando a correr. Antigravity se detecta por el binario agy o por ~/.gemini/config (se cambia con homes.antigravity o, desde 0.8, con la variable KIT_ANTIGRAVITY_HOME; homes.antigravity gana). Desinstalar corre agy plugin uninstall <herramienta>.

Los plugins de agy no admiten hooks ni servidores MCP, así que los servidores MCP se escriben directo en <homes.antigravity>/mcp_config.json, sin pasar por agy mcp. Cada servidor queda bajo mcpServers.<nombre> como { command, args, env?, disabled: false, _kit: "<herramienta>" } o, si es remoto, { serverUrl, headers?, disabled: false, _kit: "<herramienta>" }. _kit marca la entrada como propia: agy lo conserva al reescribir el archivo. Si ya hay una entrada con el mismo nombre sin esa marca, no se pisa y el paso sale skipped con el conflicto en detail. La excepción es una entrada sin marca idéntica a la que kit escribiría (de una instalación anterior): se adopta y se le pone la marca. Las demás claves del archivo se conservan, la escritura es atómica, conserva el modo del archivo (o 0600 si es nuevo) y deja mcp_config.json.backup. Un archivo vacío cuenta como {}; uno con JSON inválido no se toca y el paso sale failed. uninstallIntegration quita solo las entradas con la marca de la herramienta.

Binarios de los agentes

detectAgents y la instalación buscan cada binario con findAgentBinary (findAgentBinarySync en la versión síncrona), que no se queda en el PATH. Esto importa cuando la herramienta corre desde un hook o una app de escritorio y no hereda el PATH de la terminal. El orden de búsqueda es este:

  • override, si se pasa. Si no es ejecutable devuelve null y no sigue buscando.
  • El PATH y luego defaultSearchDirs.
  • Los gestores de Node, con toolchainSearchDirs: NVM_BIN, ~/.nvm/versions/node/*/bin de la versión más nueva a la más vieja, el alias default de fnm, Volta, NPM_CONFIG_PREFIX, ~/.npm-global/bin, pnpm, Bun y ~/.local/bin. En Windows también %APPDATA%\npm y %LOCALAPPDATA%\Volta\bin.
  • Las carpetas propias de cada agente, con agentSearchDirs: ~/.gemini/antigravity-cli/bin (agy), ~/.opencode/bin y ~/.claude/local.

Agentes sin interfaz (headless)

runHeadless corre un prompt en cualquiera de los agentes de HEADLESS_AGENTS y devuelve el texto de la respuesta, más el id de sesión si lo hay:

import { agents } from '@kikedealba/kit'

const picked = await agents.pickHeadlessAgent(config.agent ?? 'claude')
if (!picked) throw new Error('no hay ningún agente instalado')
if (picked.fallback) console.warn(`usando ${picked.agent}`)
const { text, sessionId } = await agents.runHeadless({ agent: picked.agent, bin: picked.bin, prompt, cwd, access: 'read' })

El prompt va por stdin en todos los agentes, así que no cuenta para el límite de la línea de comandos, un prompt que empieza con - no se toma como flag y no queda visible en la lista de procesos (ps). A agy le llega como una línea NDJSON {"event":"user","message":{"content":"<prompt>"}} con --input-format stream-json y -p= vacío; la respuesta sale del evento result. Ojo: agy recorta por su cuenta los mensajes de más de unos 190 KB (avisa con <truncated N bytes>), venga el prompt por donde venga.

AgenteInvocaciónaccess: 'read' (por omisión)access: 'write'Respuesta
claudeclaude -p --output-format json [--model m]--strict-mcp-config --tools Read,Grep,Glob --permission-mode plan --setting-sources user--permission-mode acceptEditsresult, session_id
codexcodex exec --json -C <cwd> --skip-git-repo-check [-m m] ---sandbox read-only--sandbox workspace-writeúltimo item.completed con agent_message; id en thread.started
geminigemini -o json [-m m]--approval-mode plan, sin GEMINI_CLI_TRUST_WORKSPACE--approval-mode auto_edit y GEMINI_CLI_TRUST_WORKSPACE=trueresponse, session_id
antigravityagy --input-format stream-json --output-format stream-json [--model m] … -p=--mode plan--dangerously-skip-permissionsresult.response, result.conversation_id del evento result
opencodeopencode run --format json [-m proveedor/modelo]--agent planagente por omisiónpart.text de los eventos text, unidos; sessionID

extraArgs va después de los flags de acceso (en agy, antes de -p=). En agy cuenta la respuesta: si response trae texto y status no es ERROR, la corrida es buena aunque traiga error o denied_actions. Sin texto útil, falla con FAILED y el mensaje sale de error, de status o de las acciones denegadas. Cuenta el último evento result; si su response viene vacío, se usan los text_delta de los pasos agent_response de ese turno. Si el stream acaba sin result pero con deltas y agy sale con 0, se devuelve ese texto parcial. Un evento error hace fallar con FAILED y su mensaje, salvo que el result traiga una respuesta útil.

tools fija la lista exacta de herramientas de claude y reemplaza el --tools Read,Grep,Glob de lectura (claude suma los --tools repetidos, así que kit no manda el suyo). Va como --strict-mcp-config --tools <a,b>, de modo que tampoco se cargan los servidores MCP del usuario. Con tools: [] no queda ninguna: kit manda --strict-mcp-config --tools Read,Grep,Glob --disallowedTools Read,Grep,Glob, que el evento init de claude reporta como tools: []. Los demás agentes ignoran tools. Sin tools, la lectura deja solo Read,Grep,Glob y nunca carga los servidores MCP del usuario (--strict-mcp-config), porque esos servidores pueden escribir. La escritura sin tools usa las herramientas y los MCP que tenga el usuario.

maxOutputBytes (8 MiB por omisión, HEADLESS_DEFAULT_MAX_OUTPUT_BYTES) limita el stdout del hijo: si lo pasa, kit mata el árbol de procesos y lanza FAILED. También acota stderr, pero ahí no falla: guarda solo los últimos maxOutputBytes bytes.

El hijo no hereda todo el entorno. headlessEnv(agent, env) filtra con una lista blanca común, y bita, recap e inkwell pueden usarla para lo mismo:

  • PATH, HOME, USER, LOGNAME, SHELL, TMPDIR/TMP/TEMP, LANG, LC_*, TERM, XDG_*, NVM_*, DEN_PROFILE (desde 0.8, para que el perfil llegue a las herramientas anidadas; el resto de DEN_* no pasa) y KIT_HEADLESS.
  • Proxy y certificados: HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY (también en minúsculas), NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR y REQUESTS_CA_BUNDLE.
  • Windows: USERPROFILE, USERNAME, HOMEDRIVE, HOMEPATH, APPDATA, LOCALAPPDATA, PROGRAMDATA, ALLUSERSPROFILE, ProgramFiles, ProgramFiles(x86), ProgramW6432, CommonProgramFiles, SystemRoot, SystemDrive, WINDIR, ComSpec, PATHEXT, PROCESSOR_ARCHITECTURE y NUMBER_OF_PROCESSORS. En Windows los nombres se comparan sin distinguir mayúsculas, también al quitar variables como GEMINI_CLI_TRUST_WORKSPACE.

A eso se suman solo los prefijos del agente que corre:

AgentePrefijosCondicionales
claudeANTHROPIC_, CLAUDE_AWS_* si CLAUDE_CODE_USE_BEDROCK; GOOGLE_* y CLOUD_ML_REGION si CLAUDE_CODE_USE_VERTEX
codexOPENAI_, CODEX_
geminiGEMINI_, GOOGLE_GOOGLE_APPLICATION_CREDENTIALS solo si GOOGLE_GENAI_USE_VERTEXAI
antigravityANTIGRAVITY_, GEMINI_
opencodeOPENCODE_

Así claude no ve OPENAI_API_KEY ni codex ANTHROPIC_API_KEY. opencode solo recibe OPENCODE_*: sus credenciales salen de opencode auth login o se pasan en env. Lo que el llamador pase en env se agrega encima del filtro y gana, así que puede reintroducir cualquier variable, incluso una que el filtro quitó; esa decisión es de quien llama. Al final se fija KIT_HEADLESS=1, que los hooks de bita y gaze respetan para no registrar esa sesión, y se pone la carpeta del binario al principio del PATH.

El tiempo límite por omisión es de 10 minutos. Al vencer, o al abortar con signal, se mata el árbol de procesos: en unix el hijo corre en su propio grupo y se manda SIGTERM y luego SIGKILL al grupo; en Windows se usa %SystemRoot%\System32\taskkill.exe /T /F. En unix, mientras haya hijos vivos (liveHeadlessGroups los lista), kit escucha exit, SIGINT, SIGTERM y SIGHUP del proceso padre y mata sus grupos. Si nadie más escucha esa señal, se quita su manejador y la vuelve a mandar al padre, que termina como lo haría sin kit. Al terminar el último hijo quita todos los manejadores. Un SIGKILL al padre no se puede atrapar.

Los fallos lanzan HeadlessError con un code, además de exitCode, stdout y stderr:

codeCuándo
UNAVAILABLENo se encontró el binario o no se pudo lanzar.
FAILEDSalió con código distinto de cero, el agente reportó un error en su JSON, se abortó, su stdout pasó de maxOutputBytes, cwd no existe o el prompt trae saltos de línea y debe ir como argumento a un .cmd.
TIMEOUTSe venció timeoutMs.
PARSESalió bien pero su salida no trae la respuesta.
PROMPT_TOO_LONGEl sistema rechazó la línea de comandos (E2BIG), o en Windows la línea ya escapada para cmd.exe pasa de 8191 caracteres.

parseHeadlessOutput(agent, stdout) y headlessInvocation(agent, opciones) están expuestos para probar sin lanzar procesos. isHeadlessAgent valida un id; uno desconocido hace fallar runHeadless con UNAVAILABLE. El mensaje de error toma la primera línea de stderr que contenga «Error», o la última si no hay. pickHeadlessAgent(preferido) devuelve el preferido si está instalado. Si no, devuelve el primero disponible de order (claude va primero) con fallback: true para que quien llama avise.

Subagentes

subagents recibe archivos en el formato de Claude Code: frontmatter con name, description, tools, model, effort y maxTurns, y el cuerpo es el prompt. En Claude se enlazan tal cual (o renderizados si tienen bloques ::: agent). En los demás agentes kit los convierte:

CampoopencodeCodexGemini CLIAntigravity CLI
descriptiondescription, con mode: subagentdescriptiondescription, con kind: localdescription, con model: inherit
toolspermission con deny para edit, bash, webfetch y websearch que no estén[features] shell_tool = false si no hay Bashtools con los nombres de Geminitools: [...] con los nombres de Antigravity
effort—model_reasoning_effort (max pasa a xhigh)——
maxTurnssteps—max_turns—
cuerpocuerpodeveloper_instructionscuerpocuerpo

En Antigravity Read pasa a view_file y list_dir, Grep a grep_search, Glob a find_by_name, LS a list_dir, Bash a run_command, Edit a replace_file_content, MultiEdit a multi_replace_file_content, Write a write_to_file, WebFetch a read_url_content y WebSearch a search_web. Lo demás (Skill, Agent, Task, TodoWrite...) se descarta. Si tools no viene, viene vacío o incluye *, se escribe la lista completa (view_file, list_dir, grep_search, find_by_name, run_command, replace_file_content, multi_replace_file_content, write_to_file, read_url_content, search_web), porque sin la clave agy deja al subagente sin escribir ni ejecutar. Si tools trae valores y ninguno existe en Antigravity, la conversión falla y el plugin no se instala.

model solo se respeta en Claude. En los demás se hereda el modelo de la sesión, porque los alias de Claude no existen ahí.

Memoria global

memory es un markdown que se escribe como bloque administrado en la memoria global de cada agente, entre <!-- kit:<herramienta>:begin --> y <!-- kit:<herramienta>:end -->. Lo demás del archivo no se toca: instalar de nuevo reemplaza solo el bloque, y desinstalar lo quita. Antes de cambiar un archivo existente se deja una copia en <archivo>.backup. Admite bloques ::: agent.

Gemini y Antigravity comparten ~/.gemini/GEMINI.md: los dos escriben el mismo bloque, renderizado como gemini (renderMemory), así que la segunda instalación sale present. Al desinstalar uno, el bloque se queda (skipped, "still used by") mientras el otro siga instalado; lo quita el último. memoryFile(agent, homes) da la ruta de la memoria de cada agente.

Hay una sola skill por herramienta. Lo que aplica solo a algunos agentes va en bloques:

::: agent codex opencode
Esto solo lo leen Codex y opencode.
:::

::: agent !gemini
Esto lo leen todos menos Gemini.
:::

Si la skill no tiene bloques se enlaza tal cual; si los tiene, se genera una copia por agente.

userSkills son skills que solo dispara el usuario (disable-model-invocation: true). En Claude Code viajan dentro del plugin; en opencode, Codex y Gemini kit las convierte en comando, igual que commands. Como el comando es una copia del SKILL.md, la skill no debe depender de otros archivos de su directorio.

En opencode, si hay un plugin que reejecuta los hooks y comandos de Claude Code (por ejemplo plugins/claude-compat), kit no instala lo que se duplicaría:

  • el plugin de opencode, si los hooks de la herramienta siguen en ~/.claude/settings.json;
  • cada comando que siga en ~/.claude/commands.

Lo marca como skipped y borra la copia que había dejado antes. Las skills se instalan siempre, y también los comandos que salen de userSkills, porque el relay no los ve. claudeCompat: false desactiva esta detección y true omite el plugin y los comandos sin revisar. El valor por defecto es 'auto'.

Claude Desktop

El agente claude-desktop solo recibe servidores MCP, y solo los de claudeDesktop.mcp. El mcp genérico nunca llega ahí, así que una herramienta puede tener MCP en Claude Desktop y quedarse en skill y CLI en los demás agentes. Si la herramienta no define claudeDesktop, el paso sale como skipped.

SistemaArchivo
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linuxno existe; el paso sale como unavailable

Se detecta si existe la carpeta Claude. kit fusiona mcpServers.<nombre> y respeta el resto del archivo (por ejemplo preferences); crea el archivo solo si la carpeta ya existe. Un servidor stdio queda como { command, args, env: { PATH } }, con el directorio del node que corre la instalación al frente del PATH, porque las apps gráficas no heredan el PATH de la terminal. Un servidor http queda como { type: 'http', url, headers }. Desinstalar quita solo las entradas de la herramienta. Hay que reiniciar Claude Desktop para que los cargue.

EDIT_TOOL_MATCHERS da el matcher de las herramientas de edición de cada agente. Sirve para que un hook PostToolUse no corra tras cada herramienta.

Plugins de Claude Code

En Claude Code cada herramienta se instala como plugin. El marketplace es su propio repo de GitHub, con .claude-plugin/plugin.json y marketplace.json.

import { agents } from '@kikedealba/kit'

const spec = { marketplace: 'KikeDeAlba/bita-cli', marketplaceName: 'bita', plugin: 'bita' }
await agents.installClaudePlugin(spec)
await agents.removeLegacyClaude(integration)
await agents.installIntegration(integration, { agents: ['opencode', 'codex', 'gemini'] })

installClaudePlugin corre lo siguiente:

  • claude plugin marketplace add <marketplace>, si el marketplace no estaba.
  • claude plugin marketplace update <marketplaceName>.
  • claude plugin install <plugin>@<marketplaceName> --scope user si el plugin es nuevo, o claude plugin update si ya estaba.

Para saber qué hay instalado usa claude plugin list --json. Si no puede leerlo, intenta instalar y después actualiza. Si claude no está en el PATH, devuelve un paso unavailable con los comandos para correrlos a mano. uninstallClaudePlugin quita el plugin y deja el marketplace, porque otro plugin puede compartirlo.

removeLegacyClaude quita lo que kit 0.2 dejaba suelto en ~/.claude:

  • los enlaces de skills y comandos, solo si apuntan al paquete de la herramienta o a la copia que kit generó;
  • los hooks y permisos de la integración en settings.json;
  • sus servidores MCP (claude mcp remove --scope user), salvo con skipMcp.

Hay que pasarle la integración tal como la instaló la versión anterior. Si los hooks cambiaron, los viejos no se reconocen.

Una herramienta que ya trae plugin debe llamar a installIntegration sin claude en agents. Si no, vuelve a escribir en ~/.claude lo que el plugin ya trae y Claude Code lo carga dos veces.

Entorno de los comandos y escrituras atómicas

Desde 0.8, los comandos que kit corre por su cuenta (claude plugin …, claude mcp …, agy plugin …) usan ctx.env de InstallOptions, no process.env. Así, installIntegration({ ctx: platformContext({ env: envFor(perfil, 'all', process.env) }) }) instala plugins y servidores MCP en el CLAUDE_CONFIG_DIR del perfil. Si se pasa exec, kit lo usa tal cual.

Toda escritura atómica de kit (writeJsonFile, registerTool, el almacén de credenciales en archivo, profiles.json y settings.json) resuelve antes el destino con realpath y crea el temporal (con nombre aleatorio) junto al archivo real. Un symlink (por ejemplo, CLAUDE.md de un perfil que apunta a ~/.claude/CLAUDE.md) sigue en pie y se actualiza el archivo al que apunta; un symlink colgante tampoco se reemplaza: se escribe en su destino. Sin mode explícito se conserva el modo del archivo que ya existía.

writeFileAtomic(archivo, contenido, { mode?, dirMode? }) y withFileLock(archivo, fn, { timeoutMs?, staleMs?, retryMs? }) se exportan desde la raíz y desde @kikedealba/kit/atomic. El candado es <archivo>.lock, creado de forma atómica con un token propio que solo su dueño borra. Mientras dura la sección crítica se refresca su fecha; uno de más de 30 s o de un proceso muerto se roba renombrándolo, y solo se borra si sigue siendo el mismo. profiles.json y settings.json se leen y escriben dentro de ese candado, así que cambios en paralelo no se pierden.

Procesos, notificaciones y Windows

spawnSpec(comando, args, ctx) devuelve { command, args, windowsVerbatimArguments } listo para spawn. En Windows un .cmd o .bat se envuelve en %SystemRoot%\System32\cmd.exe /d /s /c con cada argumento entre comillas y los metacaracteres escapados, así un argumento con &, | o comillas no se interpreta como comando. Un argumento con saltos de línea lanza un error, porque cmd.exe corta ahí la línea. En los demás casos devuelve el comando tal cual.

run usa spawnSpec. Esto cambia su comportamiento en Windows respecto a 0.6: antes corría los .cmd/.bat con shell: true y sin escapar, y ahora pasan por %SystemRoot%\System32\cmd.exe (no por ComSpec) con los argumentos escapados. Un argumento con saltos de línea ahora lanza un error en vez de cortarse. whichSync es la versión síncrona de which, y las dos aceptan un cuarto argumento con carpetas que se buscan después de defaultSearchDirs.

notify(título, cuerpo) (nuevo en 0.7) muestra una notificación del sistema y devuelve true o false, nunca lanza. El título se recorta a 256 caracteres y el cuerpo a 2000. En macOS usa osascript con el texto por argv después de --, en Linux notify-send con <, > y & escapados, y en Windows un toast de PowerShell con -EncodedCommand. El texto va en base64 dentro del script y no se salta la política de ejecución (-ExecutionPolicy no se toca). notifyCommand da el comando sin ejecutarlo.

5. Credenciales

credentialStore() elige el almacén del sistema:

SistemaAlmacén
macOSLlavero (/usr/bin/security)
WindowsAdministrador de credenciales (advapi32 vía PowerShell)
LinuxSecret Service (secret-tool); si no está, archivo 0600

KIT_CREDENTIALS=file fuerza el archivo. resolveSecret revisa primero una variable de entorno, para CI. El secreto nunca viaja como argumento de línea de comandos, salvo en macOS, donde security no admite otra vía.

6. Perfiles (@kikedealba/kit/profiles)

Un perfil es una cuenta. Separa el login de Claude Code y de Codex: cada perfil no builtin tiene su propio CLAUDE_CONFIG_DIR y CODEX_HOME. Gemini, Antigravity y opencode usan una sola cuenta en esta versión.

Todo vive bajo la raíz de configuración <configHome>/kikedealba (XDG_CONFIG_HOME o ~/.config; %APPDATA% en Windows). KIT_CONFIG_DIR reemplaza la raíz completa, pensado para pruebas. kitConfigRoot(ctx) la devuelve.

<raíz>/profiles.json (esquema en schemas/profiles.v1.json):

{
  "$schema": "https://raw.githubusercontent.com/KikeDeAlba/kit/main/schemas/profiles.v1.json",
  "version": 1,
  "default": "personal",
  "profiles": {
    "personal": { "builtin": true },
    "trabajo": {
      "claude": "~/.config/kikedealba/profiles/trabajo/claude",
      "codex": "~/.config/kikedealba/profiles/trabajo/codex",
      "createdAt": "2026-11-08T17:00:00.000Z"
    }
  },
  "dirs": [{ "path": "~/dev/empresa", "profile": "trabajo" }]
}
  • Nombres: ^[a-z][a-z0-9-]{0,31}$. Hay exactamente un perfil builtin, sin claude ni codex, y default apunta a un perfil que existe.
  • Las rutas bajo $HOME se guardan como ~/… y se expanden al leer. dirs guarda la ruta ya resuelta con realpath.
  • Si el archivo no existe, equivale a un único perfil builtin llamado default. No se crea hasta la primera escritura. Se escribe de forma atómica con modo 0600; la raíz de configuración (kikedealba/ o KIT_CONFIG_DIR), profiles/<n>/, claude/ y codex/ se crean con 0700.
  • Un archivo inválido lanza ProfileError con código PROFILES_INVALID.
  • Un home de perfil que sea ~/.claude o ~/.codex, o que repita la carpeta de otro perfil (o la suya entre claude y codex), no hace fallar la lectura: un profiles.json viejo se sigue leyendo, y el builtin y removeProfile siguen funcionando. addProfile lo rechaza con PROFILE_HOME_CONFLICT, profileHomeConflicts(n) lo lista ({ harness, home, conflictsWith }, comparando con realpath) e inspectSharedLinks marca esas entradas como home-conflict. En macOS y Windows las rutas se comparan sin distinguir mayúsculas.
import { profiles } from '@kikedealba/kit'

const { profile, source } = await profiles.resolveProfile({ cwd: process.cwd() })
const env = profiles.envFor(profile, 'claude', process.env)

resolveProfile({ cwd, explicit }) busca en este orden: explicit, DEN_PROFILE del entorno, el prefijo más largo de dirs (por segmentos) que contenga realpath(cwd) y, al final, default. Si explicit o DEN_PROFILE nombran un perfil que no existe, lanza PROFILE_NOT_FOUND; nunca cae en silencio al default.

envFor(perfil, harness | 'all', baseEnv) es pura y devuelve una copia:

  • Quita las variables de sesión de los agentes (withoutHarnessSession: CLAUDECODE, CLAUDE_CODE_SESSION_ID, CODEX_THREAD_ID, GEMINI_CLI, OPENCODE, ANTIGRAVITY_* de sesión, etc.).
  • Siempre quita CLAUDE_CONFIG_DIR y CODEX_HOME heredados.
  • En un perfil no builtin, con claude, codex o all, quita solo las credenciales de la cuenta del agente, sin importar mayúsculas: los prefijos de PROFILE_SECRET_ENV_PREFIXES (ANTHROPIC_*, que incluye ANTHROPIC_VERTEX_*, OPENAI_*, AZURE_OPENAI_*, CLAUDE_CODE_USE_*, CLAUDE_CODE_OAUTH_*, CLAUDE_CODE_API_KEY* y GOOGLE_GENAI_*) y los nombres de PROFILE_SECRET_ENV (AWS_BEARER_TOKEN_BEDROCK, CODEX_API_KEY, CODEX_ACCESS_TOKEN, GEMINI_API_KEY, GOOGLE_API_KEY y CLOUD_ML_REGION). isProfileSecretEnv(nombre) aplica la misma regla. No hay patrones por subcadena: GITHUB_TOKEN, NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN o AWS_PROFILE pasan, porque la sesión los necesita para trabajar. El builtin las conserva, y con gemini, opencode o antigravity también (esos agentes no cambian de cuenta con el perfil).
  • En un perfil no builtin define CLAUDE_CONFIG_DIR (con claude o all) y CODEX_HOME (con codex o all).
  • Siempre define un único DEN_PROFILE (quita antes cualquier variante con otras mayúsculas).

Con gemini, antigravity u opencode no define ningún home; profileSupports(harness) devuelve false para que quien llama avise de que esos agentes usan una sola cuenta. Las rutas de perfil y KIT_CONFIG_DIR deben ser absolutas: una ruta de perfil relativa es PROFILES_INVALID y un KIT_CONFIG_DIR relativo se ignora.

FunciónQué hace
listProfiles() / getProfile(n)Lee el archivo
addProfile(n)Registra el perfil y crea sus carpetas con 0700. Idempotente: created: false si ya existía
removeProfile(n, { purge })Falla con PROFILE_BUILTIN o PROFILE_IS_DEFAULT; quita sus dirs y solo borra la carpeta con purge. Devuelve { removed, purged, unmapped, logoutRequired }
setDefaultProfile(n)Cambia el default
renameBuiltinProfile(n)Da nombre al perfil builtin (el asistente de den usa personal)
mapDir(dir, n) / unmapDir(dir)Asocia o suelta un directorio
profileHomes(n){ root, claude, codex } bajo <raíz>/profiles/<n>
inspectSharedLinks(n) / repairSharedLinks(n)Revisa y repara los enlaces y copias compartidos
syncSharedCopies(n)Regenera las copias filtradas que faltan o quedaron viejas; no toca una editada
authStatus(perfil)Corre claude auth status --json con el entorno del perfil

Enlaces compartidos. Un perfil enlaza con ~/.claude y ~/.codex: CLAUDE.md, skills/, agents/, commands/ y AGENTS.md (SHARED_LINKS). Nunca se enlazan plugins/, projects/, .claude.json, .credentials.json, todos/, auth.json, sessions/ ni history.jsonl. config.toml de Codex ya no se comparte: cada perfil tiene el suyo, porque no hay un parser TOML fiable para filtrarlo.

settings.json de Claude no es un symlink sino una copia filtrada (SHARED_COPIES): kit escribe <perfil>/claude/settings.json desde ~/.claude/settings.json sin apiKeyHelper, awsCredentialExport, awsAuthRefresh, gcpAuthRefresh ni otelHeadersHelper (CLAUDE_SETTINGS_SECRET_KEYS, sin distinguir mayúsculas), y sin las claves de env que cumplen isProfileSecretEnv, las OTEL_*HEADERS* ni CLAUDE_CONFIG_DIR, CODEX_HOME o DEN_PROFILE (filterSharedClaudeSettings hace ese filtro). El hash de lo escrito queda en <perfil>/claude/.den-shared.json, fuera del settings. Si el compartido no existe, la copia es {}; si no es un objeto JSON válido, inspectSharedLinks da invalid-shared y syncSharedCopies y repairSharedLinks lanzan SHARED_COPY_INVALID antes de tocar nada.

Aviso: todo lo demás del settings.json compartido se copia tal cual, también hooks, statusLine y fileSuggestion. Son comandos tuyos y en el perfil corren con la cuenta del perfil.

inspectSharedLinks da el estado de cada entrada:

EstadoQué significaSano
linkedsymlink al compartidosí
copieden Windows, archivo copiado en vez de enlazadosí
generatedcopia filtrada al díasí
missingno existeno
foreignsymlink a otro lado (también un settings.json enlazado de versiones anteriores)no
replacedClaude Code o Codex lo cambió por algo propiono
staleel compartido cambió desde la última copia: hay que regenerarno
driftedel perfil editó su copiano
invalid-shared~/.claude/settings.json no es un objeto JSON válidono
home-conflictel home del perfil es el compartido o el de otro perfilno

HEALTHY_LINK_STATES lista los sanos. syncSharedCopies(n) regenera las copias missing, stale o foreign y deja una drifted como está; den y nursery la llaman tras cambiar el compartido. repairSharedLinks no fusiona nada:

  • Crea el destino compartido de un enlace si falta (vacío para archivos, carpeta para directorios). Nunca reescribe uno que ya existe.
  • Si un enlace fue reemplazado o una copia está drifted o replaced, mueve lo del perfil a <nombre>.backup-<timestamp> (copia y borra si está en otro volumen) y devuelve esa ruta en backup, para que den doctor la muestre.
  • Quita un enlace foreign, vuelve a enlazar y regenera las copias. Los estados sanos no se tocan.

Si el home claude o codex de un perfil resuelve (por ejemplo, con un symlink) al home compartido, syncSharedCopies y repairSharedLinks lanzan PROFILE_HOME_SHARED sin mover nada; si choca con el de otro perfil (o entre su claude y su codex), lanzan PROFILE_HOME_CONFLICT, también sin tocar nada.

removeProfile(n, { purge: true }) borra la carpeta del perfil pero no puede borrar la entrada del Llavero de macOS sin claude: en macOS devuelve logoutRequired: true cuando el perfil tenía home de Claude; en Linux y Windows, false. Den corre claude auth logout con envFor(perfil, 'claude', …) antes de llamar a removeProfile con purge.

authStatus devuelve { loggedIn, authMethod, email, orgName, subscriptionType, configDirectory }, o null si claude no está o no responde JSON. Lee la respuesta también cuando claude sale con código 1 (sin sesión).

7. Configuración (@kikedealba/kit/settings)

<raíz>/settings.json guarda solo los valores distintos del default, anidados por namespace:

{ "version": 1,
  "agents": { "maxParallel": 4, "developer": { "model": "sonnet" } },
  "rules": { "gitflow": false },
  "harness": { "claude": { "skipPermissions": false } } }
ClaveTipoDefaultDueñoValidación
agents.maxParallelinteger10nursery1–20
agents.enabledstring[]los 10 roles más los agentes de usuarionurseryroles conocidos, no vacío
agents.<role>.modelstringel del frontmatter de origennurseryMODEL_PATTERN: ^[a-z0-9][a-z0-9.\-\[\]/#:]*$
agents.<role>.maxTurnsintegerel del frontmatternursery1–500
agents.<role>.effortenumel del frontmatternurserylow, medium, high, xhigh, max
rules.noAiTrace, noComments, language, worktrees, gitflow, delegationbooleantruenursery—
rules.baseBranchstringdevelopnurserynombre de rama git válido, solo [A-Za-z0-9._/-]
harness.defaultenumclaudedenHARNESS_IDS
harness.<h>.skipPermissionsbooleantrueden—
harness.<h>.extraArgsstring[][]densin --, sin saltos de línea ni NUL; por lo demás libre (es config del propio usuario)
session.summary.<h>.modelstringdefaultSummaryModel del adaptadorbitaMODEL_PATTERN
session.minDurationSecinteger60bita0–3600
tools.<t>.enabledbooleantruedenkit no se puede apagar

<role> es uno de los 10 roles de nursery (NURSERY_ROLES) o un agente de usuario de <raíz>/agents/<n>.md. Para los roles de nursery, agents.<role>.* sin definir vale undefined: "lo que diga la fuente". Para un agente de usuario, el default sale de su frontmatter. Las claves de nursery tienen refresh: 'nursery'; el resto, 'none'.

import { settings } from '@kikedealba/kit'

await settings.setSetting('agents.maxParallel', '4')
await settings.getSetting<number>('agents.maxParallel')
  • parseSettingValue(clave, texto): boolean acepta true|false|1|0, integer en base 10, string[] como lista separada por comas o arreglo JSON.
  • setSetting acepta el valor ya tipado o como texto. Si queda igual al default, borra la clave del archivo. Devuelve { key, previous, value, refresh }.
  • unsetSetting vuelve al default. listSettings expande todos los patrones con source: 'user' | 'default' y su dueño. checkSettings devuelve { ok, errors } para un doctor.
  • Errores (SettingsError): UNKNOWN_KEY, INVALID_VALUE y SETTINGS_INVALID (archivo corrupto; setSetting se niega a sobrescribirlo).
  • onChange(prefijo, handler) avisa dentro del proceso cuando un valor cambia de verdad. Los manejadores corren en orden y aislados: si uno falla, los demás corren igual, el valor ya quedó guardado y el error vuelve en change.warnings; setSetting no se rechaza. Entre procesos, el refresco lo orquesta den.

8. Adaptadores de agentes (@kikedealba/kit/harness)

Los adaptadores de bita (claude, opencode, codex, gemini, antigravity) viven aquí desde 0.8: argumentos de lanzamiento, lectura de transcripciones, resumen de sesión y afterExit.

import { harness } from '@kikedealba/kit'

const adapter = harness.getAdapter('claude')
const plan = adapter.launchArgs(userArgs, await harness.launchOptionsFor('claude'))
const turns = await adapter.extractTranscript({ ...ref, profile: 'trabajo' }, baseEnv)
  • adapter.profiles dice si el agente admite perfiles (claude y codex).
  • launchArgs(args, { skipPermissions, extraArgs }): con skipPermissions: false no agrega el flag por defecto (--allow-dangerously-skip-permissions, --dangerously-bypass-approvals-and-sandbox, --approval-mode=yolo, --auto, --dangerously-skip-permissions). extraArgs va tal cual antes de los argumentos del usuario; si ya trae el flag por defecto, no se repite. Sin opciones equivale a { skipPermissions: true, extraArgs: [] }. launchOptionsFor(id) las lee de harness.<id>.*.
  • extractTranscript(ref, baseEnv?) y summarize({ …, profile, baseEnv? }) calculan su entorno con harnessEnv(id, profile, baseEnv): quitan las variables de sesión y, si hay perfil, aplican envFor. Sin perfil (null o undefined) se comportan como el perfil builtin: quitan CLAUDE_CONFIG_DIR, CODEX_HOME y DEN_PROFILE heredados y no definen ninguno. Para quien consume kit (bita, den, nursery), profile: null significa el perfil builtin, no "lo que haya en el entorno".
  • Las transcripciones se confinan al home: Claude a projects/, Codex a sessions/ y archived_sessions/, Gemini a tmp/ de ~/.gemini. El realpath del archivo (también el de un transcriptPath explícito) debe quedar ahí dentro y el sessionId debe cumplir SESSION_ID_PATTERN (^\w[\w-]*$, sin guion inicial); opencode lo valida antes de session export. Si no, devuelven [].
  • El resumen de opencode corre con --agent plan y un opencode.json temporal (OPENCODE_SUMMARY_CONFIG) que niega bash, edit, webfetch y write. Un perfil que ya no existe hace que summarize devuelva { ok: false }; un nombre vacío se rechaza. Cambio respecto a bita 1.9: sin baseEnv, los adaptadores parten de process.env sin las variables de sesión, pero ya no ponen BITA_CHILD ni quitan BITA_WRAPPER_ID, BITA_HARNESS o BITA_SESSION_PROJECT. Quien llama (bita) pasa su childEnv() como baseEnv.
  • El binario se busca primero en KIT_HARNESS_BIN_<ID>, luego en BITA_HARNESS_BIN_<ID> (las dos con ruta absoluta) y luego con findAgentBinarySync.
  • Los avisos de flags sin equivalente ya no llevan el prefijo bita:; quien los imprime pone el suyo.

9. Catálogo (@kikedealba/kit/catalog)

La lista canónica de herramientas, en orden de instalación: kit, nursery, bita, gaze, inkwell, recap, tally y atl.

import { catalog } from '@kikedealba/kit'

for (const tool of catalog.CATALOG) console.log(tool.name, tool.package, tool.setup, tool.agentsFlag)

Cada entrada trae name, package, bin, description, homepage, setup y uninstall (argumentos del binario), agentsFlag (si su setup acepta --agents) y optional. DEN_APP apunta a las releases de Den.app. El paquete incluye la misma lista en schemas/catalog.v1.json (@kikedealba/kit/schemas/catalog.v1.json), que Den.app lee al compilar.

CLI

kit ls            # herramientas registradas
kit doctor        # verifica que cada una responda capabilities --json
kit events bita   # quién escucha los eventos de bita
kit agents        # agentes de código encontrados

Pruebas de conformidad

Cada herramienta corre esto en su CI:

import { assertConformance } from '@kikedealba/kit/conformance'

await assertConformance([process.execPath, 'src/bin/atl.ts'], manifest)

Revisa que el manifiesto sea válido, que capabilities --json imprima un sobre correcto que coincida con el manifiesto, y que un comando desconocido falle con un sobre de error y código distinto de cero.

FAQs

Package last updated on 09 Nov 2026

Related posts