La semana pasada hablé del primer CLAUDE.md de agentdeck. Esta semana ya hay producto.

Lo cuento con números reales: hace dos días le pegué la URL de un repo público con 184 agentes Claude Code (msitarzewski/agency-agents) y los parseó todos en 8 segundos. Sin clonar, sin filesystem, sin mirar a GitHub. Solo URL → tarball → parser → DB tipada.

No es magia. Es ingeniería boba bien hecha. Y esta semana he aprendido dos cosas concretas montando este pipeline.

Cómo escanear un repo sin clonar nada

Mi reflejo inicial al diseñar el scanner fue el obvio: instalar simple-git, hacer git clone a /tmp, leer del filesystem, borrar al acabar. Lo descarté en cuanto me pregunté tres cosas.

Una. Vercel no te deja escribir filesystem persistente. Tienes /tmp durante la invocación (~512 MB), pero cualquier cosa que pase de unos segundos o invoques varias veces es frágil.

Dos. git clone es un binario. En Vercel no está. En contenedores tampoco salvo que lo metas tú.

Tres. Solo necesito unos KB del repo (.claude/ y los *.md raíz). Bajarme el repo entero a disco para usar el 1% es pereza arquitectónica.

La solución es bonita en su simpleza:

const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/refs/heads/${branch}`;
const response = await fetch(url);
const nodeStream = Readable.fromWeb(response.body);
nodeStream.pipe(createGunzip()).pipe(tarExtract);

codeload.github.com es el subdominio que GitHub usa para servir archivos. No requiere auth para repos públicos, no tiene rate limits agresivos, y devuelve un .tar.gz. Lo combino con tar-stream (Node, no usa filesystem) y zlib.createGunzip() (built-in) para descomprimir y parsear entry por entry mientras el stream llega. En el 'entry' del tar decido si el archivo me interesa o lo descarto sin acumularlo en memoria.

Resultado: un repo público se escanea en segundos, sin filesystem, sin git binary, sin cuotas. Funciona igual en local que en Vercel. Y como bonus, el mismo pipeline acepta también un .zip que sube el usuario — defines un tipo común VirtualFile = { path, bytes } y todos los métodos de ingesta producen VirtualFile[]. Cambias el origen, no cambias el resto.

La regla que me llevo: antes de instalar una librería que opera sobre filesystem (fs-extra, simple-git, etc.), preguntar si realmente necesito el repo en disco o solo unos archivos. Cuando un servicio (GitHub, GitLab, Bitbucket) ya te da el archivo empaquetado, úsalo. No reproduzcas su trabajo con un wrapper de git.

El bug del INSERT que reventó en producción

Este lo cuento porque me encantan las lecciones que solo aprendes cuando algo te explota delante.

El scanner funcionaba contra mi propio repo agentdeck (1 skill detectada, todo bien). Funcionaba contra repos sin .claude/ (0 archivos, mensaje honesto). Pero al apuntarlo a agency-agents —que tiene 184 agentes en carpetas como engineering/, design/, marketing/— el INSERT bulk en Postgres petó.

El error que me devolvía la app era ruido de Drizzle de medio kilómetro. La causa real, en cuanto miré:

duplicate key value violates unique constraint "scan_agents_scan_name_unique"

Mi schema decía: dentro de un mismo scan, dos agentes no pueden tener el mismo name. Yo había codificado esa unicidad sin pensar, asumiendo que el name del frontmatter era la identidad del agente.

Y resulta que en agency-agents hay agentes distintos con nombres parecidos en el frontmatter (name: "Frontend Developer", name: "Frontend Architect", etc.) que en algún caso colisionan. La identidad técnica del agente dentro de un scan no es el nombre, es el archivo: dos .md distintos pueden compartir nombre en frontmatter, pero el path (engineering/frontend-developer.md) siempre es único.

Migración chiquita y limpia:

DROP INDEX scan_agents_scan_name_unique;
CREATE UNIQUE INDEX scan_agents_scan_file_unique
  ON scan_agents (scan_id, file_id);
CREATE INDEX scan_agents_scan_name_idx
  ON scan_agents (scan_id, name);

Cambio del unique a (scan_id, file_id), dejo el name como índice no único para búsquedas futuras. Y refactorizo persist.ts para que el mapping interno entre agentes y sus tools use fileId en vez de name.

La regla que me llevo: al diseñar identidades en una tabla, distinguir identidad técnica (la que la base de datos necesita para resolver duplicados sin perder integridad) de identidad lógica (la que el usuario lee). El name de un agente es identidad lógica — útil para mostrarlo. El archivo donde vive es identidad técnica — útil para no perder filas. Confundirlas es lo que casi nunca está mal mientras tu propio repo sea el caso de prueba, y te explota la primera vez que escaneas un repo ajeno.

Lo que llevo construido a final de semana 2

  • Scanner end-to-end funcionando: URL pública de GitHub o subida de zip

  • Auth completa con Supabase (signup / login / dashboard protegido)

  • Detalle del scan que enseña cada agente, skill y hook con su frontmatter y contenido inspeccionable

  • Detección de agentes en layouts alternativos (no solo .claude/agents/)

  • 184 agentes parseados de un repo ajeno como prueba pública

Sigo sin tener un solo agente Claude Code activo en mi propio setup. Esto es deliberado: la regla de mi proyecto es no creo un agente hasta que tengo un trabajo repetitivo concreto para él. El primero llega esta semana.

Próxima semana

Semana 3 — primer agente real del equipo: View Builder. Construye pantallas a partir de referencias visuales y del schema de Drizzle. Si funciona, la semana 4 voy a por Backend & Integrations.

Si te interesa cómo se diseña un agente Claude Code que de verdad ahorra trabajo (en lugar de uno bonito en un README), nos leemos el martes que viene.

— Sergio

Sigue leyendo