¿Qué hace Rastreador de Adopción de SDK?
Toma un nombre de SDK. Busca en GitHub repositorios públicos que lo importen. Puntúa cada repositorio por señal de empresa, actividad e indicadores de ruido. Enriquece los repositorios de alta señal con datos del propietario y contribuidores. Genera un informe de adopción clasificado con contexto de alcance para los adoptantes empresariales.
Regla crítica: Cada repositorio en la salida debe existir en la respuesta de la API de búsqueda de código de GitHub. Cada nombre de empresa debe provenir del campo company o name de la API de usuario u organización de GitHub. Cada nombre de contribuidor debe provenir de la respuesta de la API de contribuidores de GitHub. Si algún campo está vacío en la API, escribe "no listado" — no infieras, adivines ni extrapoles.
Errores Comunes
| El agente querrá... | Por qué está mal |
|---|---|
| Ejecutar la búsqueda de código sin GITHUB_TOKEN | La búsqueda de código no autenticada tiene un límite de velocidad secundario de 3 solicitudes/minuto y falla en cualquier análisis significativo. GITHUB_TOKEN es obligatorio. Detente en el Paso 1 con un error claro si falta. |
| Incluir forks del propio SDK | Los repositorios que bifurcan el SDK son contribuidores o espejos, no adoptantes. Filtra los repositorios donde fork == true Y el nombre del repositorio coincide con el nombre del SDK. |
| Enviar los 500 resultados de búsqueda sin procesar a la IA | La búsqueda de código puede devolver hasta 500 resultados, la mayoría de los cuales son ruido. Filtra y puntúa localmente primero. Envía solo los 20 mejores repositorios de alta señal al paso de análisis de la IA. |
| Reportar repositorios de tutoriales y ejemplos como adoptantes | Los repositorios con "example", "tutorial", "demo", "learn", "sample", "playground", "starter" en el nombre o descripción no son usuarios de producción. Márcalos como tutorial_noise y exclúyelos de los informes de leads. |
| Inventar nombres de empresas o contactos | Cada nombre de empresa debe provenir del campo company de GitHub o del campo name de la organización. Cada nombre de contribuidor debe provenir de la respuesta de la API de contribuidores. Si un campo está vacío, escribe "no listado". |
| Usar un único patrón de importación para todos los ecosistemas | require('sdk') no encontrará usuarios de Python. Detecta automáticamente el ecosistema a partir del nombre del SDK y construye patrones específicos para cada ecosistema. Pregunta al usuario si la detección automática es ambigua. |
Paso 1: Verificación de Configuración
if [ -z "$GITHUB_TOKEN" ]; then
echo "ERROR: GITHUB_TOKEN es obligatorio para la búsqueda de código."
echo "Agrega un token en github.com/settings/tokens (no se necesitan permisos para repositorios públicos)."
echo "Sin él, la búsqueda de código de GitHub tiene un límite de velocidad secundario de 3 solicitudes/minuto y falla."
exit 1
fi
echo "GITHUB_TOKEN: establecido"
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/rate_limit" | python3 -c "
import json, sys
d = json.load(sys.stdin)
search = d['resources']['search']
core = d['resources']['core']
print(f'Tasa de búsqueda: {search[\"remaining\"]}/{search[\"limit\"]} restantes')
print(f'Tasa principal: {core[\"remaining\"]}/{core[\"limit\"]} restantes')
"
Si las solicitudes de búsqueda restantes son 0: detente. Indica al usuario la hora de reinicio desde X-RateLimit-Reset.
Paso 2: Recopilar Información
Recopila de la conversación:
- Nombre del SDK (ej.
@company/my-sdk,requests,github.com/org/go-sdk) - Opcional: anulación del ecosistema (
npm,python,go,gem) — se detecta automáticamente si no se proporciona - Opcional: organización/usuario a excluir de los resultados (los repositorios propios del propietario del SDK)
- Opcional: cadena de contexto del producto (se usa para personalizar mensajes de alcance)
Detectar ecosistema automáticamente:
- Empieza con
@o contiene-: npm - snake_case sin
/ni-: python - Contiene
github.com/: go - De lo contrario: pregunta al usuario
Si no se proporciona un nombre de SDK: Pregunta: "¿Qué SDK o librería te gustaría rastrear? Proporciona el nombre del paquete tal como aparece en las declaraciones de importación (ej. stripe, @clerk/nextjs, requests)."
python3 << 'PYEOF'
import json, sys, re
sdk_name = "SDK_NAME_HERE"
ecosystem_override = "" # déjalo vacío para detección automática
exclude_owner = "" # opcional: nombre del propietario a excluir (normalmente el publicador del SDK)
product_context = "" # opcional: qué hace tu producto
# Detectar ecosistema automáticamente
if ecosystem_override:
ecosystem = ecosystem_override
elif sdk_name.startswith("@") or "-" in sdk_name:
ecosystem = "npm"
elif re.match(r'^[a-z][a-z0-9_]*$', sdk_name):
ecosystem = "python"
elif "github.com/" in sdk_name:
ecosystem = "go"
else:
ecosystem = "generic"
print(f"SDK: {sdk_name}")
print(f"Ecosistema: {ecosystem}")
print(f"Excluir propietario: {exclude_owner or '(ninguno)'}")
with open("/tmp/sat-input.json", "w") as f:
json.dump({
"sdk_name": sdk_name,
"ecosystem": ecosystem,
"exclude_owner": exclude_owner,
"product_context": product_context
}, f)
PYEOF
Paso 3: Buscar en el Código de GitHub
Primero verifica si existe un script independiente — que maneja los Pasos 3-5 en una sola llamada.
ls scripts/fetch.py 2>/dev/null && echo "script disponible" || echo "script no encontrado"
Si el script está disponible, ejecútalo y salta al Paso 6:
python3 scripts/fetch.py "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d['sdk_name'])")" \
--ecosystem "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d['ecosystem'])")" \
--exclude "$(python3 -c "import json; d=json.load(open('/tmp/sat-input.json')); print(d.get('exclude_owner',''))")" \
--output /tmp/sat-script-out.json
Luego carga los datos en el formato de archivo temporal que esperan los Pasos 6-8:
python3 << 'PYEOF'
import json
out = json.load(open("/tmp/sat-script-out.json"))
json.dump(out["raw_results"], open("/tmp/sat-raw-results.json", "w"), indent=2)
json.dump(out["scored"], open("/tmp/sat-scored.json", "w"), indent=2)
json.dump(out["enriched"], open("/tmp/sat-enriched.json", "w"), indent=2)
print(f"Cargados: {len(out['raw_results'])} sin procesar | {len(out['scored'])} puntuados | {len(out['enriched'])} enriquecidos")
PYEOF
Si el script no está disponible, ejecuta el código integrado a continuación.
Construye patrones de importación y busca en el código de GitHub cada uno:
python3 << 'PYEOF'
import json, urllib.request, ssl, time, os
from datetime import datetime, timezone
ctx = ssl._create_unverified_context()
token = os.environ["GITHUB_TOKEN"]
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"User-Agent": "sdk-adoption-tracker/1.0"
}
data = json.load(open("/tmp/sat-input.json"))
sdk_name = data["sdk_name"]
ecosystem = data["ecosystem"]
# Construir patrones de consulta por ecosistema
if ecosystem == "npm":
# Usar el nombre del SDK sin el prefijo @ para búsquedas simples
bare = sdk_name.lstrip("@").replace("/", "/")
queries = [
f'require("{sdk_name}")',
f"require('{sdk_name}')",
f'from "{sdk_name}"',
f"from '{sdk_name}'",
]
elif ecosystem == "python":
queries = [
f"import {sdk_name}",
f"from {sdk_name} import",
f"from {sdk_name}.",
]
elif ecosystem == "go":
queries = [f'"{sdk_name}"']
else:
queries = [sdk_name]
print(f"Construyendo consultas para {sdk_name} ({ecosystem}):")
for q in queries:
print(f" {q}")
seen_repos = {} # full_name -> primera coincidencia
search_rate_remaining = 30
flags = []
for i, query in enumerate(queries):
if search_rate_remaining <= 2:
flags.append(f"Límite de velocidad de búsqueda bajo ({search_rate_remaining}) -- se saltaron los patrones restantes")
print(f" Límite de velocidad bajo ({search_rate_remaining}), deteniendo temprano")
break
url = f"https://api.github.com/search/code?q={urllib.parse.quote(query)}&per_page=100"
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req, timeout=20, context=ctx) as resp:
search_rate_remaining = int(resp.headers.get("X-RateLimit-Remaining", 10))
raw = json.loads(resp.read())
items = raw.get("items", [])
total = raw.get("total_count", 0)
print(f" Patrón '{query}': {total} total, {len(items)} obtenidos | tasa_restante={search_rate_remaining}")
for item in items:
repo = item.get("repository", {})
full_name = repo.get("full_name", "")
if full_name and full_name not in seen_repos:
seen_repos[full_name] = {
"full_name": full_name,
"name": repo.get("name", ""),
"owner_login": repo.get("owner", {}).get("login", ""),
"owner_type": repo.get("owner", {}).get("type", ""),
"file_path": item.get("path", ""),
"matched_pattern": query,
"html_url": repo.get("html_url", ""),
"description": repo.get("description") or "",
}
except urllib.error.HTTPError as e:
if e.code == 403:
flags.append(f"Límite de velocidad de búsqueda alcanzado en el patrón '{query}'")
print(f" Límite de velocidad alcanzado (403) en '{query}'")
break
else:
print(f" HTTP {e.code} en '{query}'")
except Exception as e:
print(f" Error en '{query}': {e}")
# Respetar el límite de 10 solicitudes/minuto de búsqueda
if i < len(queries) - 1:
time.sleep(6)
results = list(seen_repos.values())
json.dump(results, open("/tmp/sat-raw-results.json", "w"), indent=2)
print(f"\nTotal de repositorios únicos encontrados: {len(results)}")
if flags:
print("Banderas:", flags)
import urllib.parse # asegurar importación
PYEOF
Si 0 resultados: Indica al usuario: "No se encontraron repositorios que importen {sdk_name}. La indexación de búsqueda de código de GitHub toma de 1 a 4 semanas para paquetes nuevos. Si el SDK está establecido, verifica los patrones de importación en references/import-patterns.md."
Paso 4: Puntuar y Clasificar Repositorios
Sin llamada a la API. Python puro. Filtra ruido, calcula puntuación de adopción, clasifica cada repositorio.
python3 << 'PYEOF'
import json
from datetime import datetime, timezone
data = json.load(open("/tmp/sat-input.json"))
exclude_owner = data.get("exclude_owner", "").lower()
sdk_name = data["sdk_name"].lower().split("/")[-1].replace("@", "")
results = json.load(open("/tmp/sat-raw-results.json"))
TUTORIAL_WORDS = {"example", "tutorial", "demo", "learn", "sample", "starter",
"boilerplate", "template", "playground", "test", "course", "workshop"}
scored = []
now = datetime.now(tz=timezone.utc)
for repo in results:
full_name = repo["full_name"]
owner_login = repo.get("owner_login", "").lower()
repo_name = repo.get("name", "").lower()
description = (repo.get("description") or "").lower()
owner_type = repo.get("owner_type", "User")
# Excluir los repositorios propios del propietario del SDK
if exclude_owner and owner_login == exclude_owner.lower():
continue
# Detectar ruido de tutorial
name_words = set(repo_name.replace("-", " ").replace("_", " ").split())
desc_words = set(description.split())
is_tutorial = bool((name_words | desc_words) & TUTORIAL_WORDS)
# También excluir si el nombre del repositorio ES el nombre del SDK (probablemente un fork)
if repo_name == sdk_name or repo_name.startswith(sdk_name + "-"):
is_tutorial = True # tratar como ruido
# Nivel de clasificación
if is_tutorial:
tier = "tutorial_noise"
elif owner_type == "Organization":
tier = "company_org"
else:
tier = "solo_dev" # se actualizará a affiliated_dev en el Paso 5 si el campo company está poblado
# Puntuación de adopción (rellenada con datos parciales ahora, enriquecida en el Paso 5)
score = 0
if owner_type == "Organization": score += 50
if not is_tutorial: score += 20
# estrellas, días_desde_push, es_fork, es_archivado añadidos en el Paso 5
scored.append({
**repo,
"tier": tier,
"is_tutorial": is_tutorial,
"adoption_score": score,
"enriched": False
})
# Ordenar: company_org primero, luego por nivel
tier_order = {"company_org": 0, "affiliated_dev": 1, "solo_dev": 2, "tutorial_noise": 3}
scored.sort(key=lambda x: (tier_order.get(x["tier"], 9), -x["adoption_score"]))
json.dump(scored, open("/tmp/sat-scored.json", "w"), indent=2)
tiers = {}
for r in scored:
tiers[r["tier"]] = tiers.get(r["tier"], 0) + 1
print(f"Clasificación:")
for tier, count in sorted(tiers.items(), key=lambda x: tier_order.get(x[0], 9)):
print(f" {tier}: {count}")
print(f"Total: {len(scored)} repositorios")
non_noise = [r for r in scored if r["tier"] != "tutorial_noise"]
print(f"\nPrincipales repositorios para enriquecer (sin ruido): {len(non_noise)}")
for r in non_noise[:5]:
print(f" {r['full_name']} ({r['tier']}) -- {r.get('description','')[:60]}")
PYEOF
Si todos los repositorios son tutorial_noise: Detente. Indica al usuario: "Todos los repositorios encontrados parecen ser tutoriales o ejemplos. No se detectaron adoptantes de producción en GitHub público. El SDK puede ser demasiado nuevo, o el nombre del paquete es lo suficientemente genérico como para que los resultados de búsqueda estén dominados por ejemplos."
Paso 5: Enriquecer Repositorios de Alta Señal
Obtiene metadatos completos del repositorio, perfil del propietario y principales contribuidores para repositorios sin ruido. Omite los repositorios tutorial_noise por completo.
python3 << 'PYEOF'
import json, urllib.request, ssl, os, time
from datetime import datetime, timezone
ctx = ssl._create_unverified_context()
token = os.environ["GITHUB_TOKEN"]
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"User-Agent": "sdk-adoption-tracker/1.0"
}
scored = json.load(open("/tmp/sat-scored.json"))
core_remaining = 5000
flags = []
def gh_get(path):
global core_remaining
req = urllib.request.Request(f"https://api.github.com{path}", headers=headers)
try:
with urllib.request.urlopen(req, timeout=15, context=ctx) as resp:
remaining = resp.headers.get("X-RateLimit-Remaining")
if remaining:
core_remaining = int(remaining)
return json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
except Exception:
return None
target = [r for r in scored if r["tier"] != "tutorial_noise"]
print(f"Enriqueciendo {len(target)} repositorios (saltando tutorial_noise)...")
enriched = []
for item in target:
full_name = item["full_name"]
owner_login = item["owner_login"]
# Obtener metadatos del repositorio
repo_data = gh_get(f"/repos/{full_name}")
if repo_data:
item["stars"] = repo_data.get("stargazers_count", 0)
item["forks"] = repo_data.get("forks_count", 0)
item["is_fork"] = repo_data.get("fork", False)
item["is_archived"] = repo_data.get("archived", False)
item["pushed_at"] = repo_data.get("pushed_at", "")
# Actualizar puntuación de adopción
score = item["adoption_score"]
if item["stars"] > 0:
score += min(30, item["stars"])
if item["is_fork"]:
score -= 50
if item["is_archived"]:
score -= 100
item["adoption_score"] = max(0, score)
else:
item["stars"] = 0
item["forks"] = 0
item["is_fork"] = False
item["is_archived"] = False
item["pushed_at"] = ""
# Obtener perfil del propietario
owner_type = item.get("owner_type", "User")
if owner_type == "Organization":
org_data = gh_get(f"/orgs/{owner_login}")
if org_data:
item["company_name"] = org_data.get("name") or org_data.get("company") or "no listado"
item["company_url"] = org_data.get("blog", "") or org_data.get("html_url", "")
else:
item["company_name"] = "no listado"
item["company_url"] = ""
else:
user_data = gh_get(f"/users/{owner_login}")
if user_data:
item["company_name"] = user_data.get("company") or user_data.get("name") or "no listado"
item["company_url"] = user_data.get("blog", "") or user_data.get("html_url", "")
if user_data.get("company"):
item["tier"] = "affiliated_dev"
else:
item["company_name"] = "no listado"
item["company_url"] = ""
# Obtener principales contribuidores
contribs = gh_get(f"/repos/{full_name}/contributors?per_page=5")
if contribs and isinstance(contribs, list):
item["top_contributors"] = [c["login"] for c in contribs if c.get("login")]
item["contributor_count"] = len(contribs)
else:
item["top_contributors"] = []
item["contributor_count"] = 0
item["enriched"] = True
enriched.append(item)
if core_remaining < 50:
flags.append(f"Límite de tasa principal bajo ({core_remaining}) -- enriquecimiento detenido temprano")
print(f"Límite de tasa principal bajo ({core_remaining}), deteniendo temprano")
break
time.sleep(0.5)
# Combinar repositorios enriquecidos y tutorial_noise
all_enriched = enriched + [r for r in scored if r["tier"] == "tutorial_noise"]
json.dump(all_enriched, open("/tmp/sat-enriched.json", "w"), indent=2)
print(f"\nRepositorios enriquecidos: {len(enriched)}")
print(f"Repositorios tutorial_noise sin cambios: {len([r for r in scored if r['tier'] == 'tutorial_noise'])}")
if flags:
print("Banderas:", flags)
# Mostrar muestra de alta señal
enriched.sort(key=lambda x: -x["adoption_score"])
print("\nPrincipales repositorios enriquecidos por puntuación:")
for r in enriched[:5]:
print(f" {r['full_name']} | Puntuación: {r['adoption_score']} | Empresa: {r.get('company_name','')}")
PYEOF
Paso 6: Análisis de la IA
Proporciona los repositorios enriquecidos a la IA para su análisis. El agente debe:
- Leer los repositorios enriquecidos
- Identificar los 20 principales repositorios de alta señal (por puntuación de adopción, excluyendo tutorial_noise)
- De esos 20, seleccionar los X más prometedores para leads de ventas
- Para cada uno, determinar:
- ¿Es un usuario de producción o un experimento?
- ¿Qué función del SDK parece estar usando?
- ¿Podría ser un cliente potencial? (si el producto llena un vacío o mejora su pila actual)
- Contexto de alcance: ¿cómo contactar o qué valor ofrecer?
- Detectar nuevos adoptantes: repositorios creados o con la primera importación en los últimos 30 días
Consejo: Usa el contexto del producto si se proporcionó para personalizar los mensajes de alcance.
Paso 7: Generar Informe
Genera el informe final de adopción del SDK. Incluye:
- Resumen ejecutivo (total de adoptantes, distribución por niveles, tendencias)
- Tabla de los 10 principales adoptantes empresariales (compañía, repositorio, puntuación, contribuidores clave)
- Nuevos adoptantes (últimos 30 días)
- Notas de contexto de alcance para cada empresa de alta señal
Paso 8: Salida Final
Presenta el informe al usuario en un formato limpio y legible. Incluye:
- Enlaces directos a cada repositorio
- La puntuación de adopción
- El nivel de clasificación
- El contexto de alcance para leads empresariales
- Bandera de nuevos adoptantes si corresponde
Referencia: Patrones de Importación por Ecosistema
| Ecosistema | Patrones |
|---|---|
| npm | require('pkg'), import { ... } from 'pkg' |
| Python | import pkg, from pkg import ... |
| Go | import "host.com/user/repo" |
| Ruby Gem | require 'gem_name' |
| Rust Crate | use crate_name; |
| PHP Composer | use Vendor\Package; |
| Java/Maven | import com.example.package; |
| .NET/NuGet | using PackageName; |