Communitygithub.com

Téléchargements npm vers Leads

Analyse les téléchargements npm, calcule la vélocité des packages et enrichit les profils des mainteneurs pour générer des leads commerciaux exploitables.

Qu'est-ce que Téléchargements npm vers Leads ?

Cette compétence transforme une liste de packages npm en un tableau de bord de leads priorisés. Elle récupère 12 semaines de données de téléchargement quotidiennes depuis l'API publique npm, calcule un score de vélocité pour chaque package et identifie ceux en phase de croissance exponentielle (hockey-stick). Pour chaque package en breakout, elle enrichit le profil des mainteneurs en interrogeant le registre npm et l'API GitHub (nom, description, repository, followers, twitter_username). Le résultat final est un brief classé par priorité, incluant le score de vélocité, le taux de croissance, les informations de contact du mainteneur et un message de proposition de valeur personnalisé. Elle est conçue pour les développeurs, les équipes DevTools ou les responsables de croissance qui souhaitent identifier les évangélistes avant qu'ils ne deviennent célèbres, suivre la dynamique des packages concurrents ou générer des leads B2B à partir de signaux de croissance de packages. La compétence gère gracieusement les limites de taux de l'API GitHub (avec ou sans jeton) et évite d'enrichir les packages stables ou trop précoces afin d'économiser le budget d'appels API.

Compatible avec✓Claude Code~Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/Varnan-Tech/opendirectory/tree/main/skills/npm-downloads-to-leads

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait Téléchargements npm vers Leads ?

Prenez une liste de packages npm. Récupérez 12 semaines de données de téléchargement. Calculez la vélocité de croissance. Enrichissez les profils des mainteneurs. Produisez un brief de lead classé par priorité pour chaque package en breakout avec des signaux de contact et un message de prospection.


Règle critique : Chaque chiffre de téléchargement de package dans la sortie doit provenir de la réponse de l'API npm. Chaque identifiant GitHub ou nom d'utilisateur Twitter d'un mainteneur doit provenir de la réponse de l'API GitHub — ne doit pas être deviné à partir du nom d'utilisateur npm. Si l'API GitHub n'a pas renvoyé de champ twitter_username, écrivez "non trouvé sur GitHub" — n'en inventez pas.


Erreurs Fréquentes

L'agent va vouloir...Pourquoi c'est faux
Récupérer les profils GitHub pour tous les packages de la listeLa limite de débit est de 60 requêtes/heure sans jeton. Enrichir les packages stables ou en déclin gaspille le budget avant d'atteindre ceux en breakout. Ne récupérez les profils que pour les packages en breakout et en surveillance.
Classer les packages par téléchargements hebdomadaires brutsLes téléchargements bruts favorisent React et lodash, qui ne sont pas des leads. Un package passant de 1 000 à 8 000 téléchargements/semaine est plus intéressant que React à 50 M/semaine. Le score de vélocité est le signal.
Ignorer l'encodage URL pour les packages scopés@org/pkg sans encodage provoque une erreur 404 de l'API npm. Encodez @ en %40 et / en %2F pour chaque nom de package scopé.
Arrêter la compétence lorsque la limite de débit GitHub est atteinteDégradation gracieuse. Présentez le classement de vélocité à partir des données npm, ignorez les enrichissements GitHub restants et ajoutez un indicateur à data_quality_flags. N'arrêtez pas.
Rédiger des messages de prospection sans nommer le package spécifiqueLes messages génériques "J'ai vu votre projet" restent sans réponse. Chaque message de prospection doit nommer le package, ses chiffres de croissance et un lien spécifique avec le contexte fourni par l'utilisateur.
Inclure les packages avec moins de 500 téléchargements hebdomadaires comme leadsMoins de 500/semaine, c'est du bruit. Le mainteneur n'a pas encore d'audience significative. Signalez-le comme "trop tôt" mais ne le présentez pas comme un lead.

Étape 1 : Vérification de Configuration

echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-non défini, limite de débit non authentifiée applicable (60 req/h — suffisant pour ~10 packages)}"

Si GITHUB_TOKEN n'est pas défini : Continuez. Informez l'utilisateur : "GITHUB_TOKEN n'est pas défini. L'enrichissement GitHub est limité à ~10 packages avant d'atteindre la limite de débit. Ajoutez un jeton sur github.com/settings/tokens (aucun scope nécessaire)."

Aucune clé requise. L'API npm et le registre npm sont entièrement publics, sans authentification.


Étape 2 : Collecte des Entrées

Recueillez à partir de la conversation :

  • Un ou plusieurs noms de packages npm (non scopés comme esbuild, ou scopés comme @hono/hono)
  • Optionnel : une courte chaîne de contexte produit (utilisée pour personnaliser les messages de prospection)

Si l'utilisateur donne une URL npmjs.com, extrayez uniquement le nom du package. Conservez le nom scopé complet incluant @ et le préfixe de l'organisation — l'encodage est géré à l'Étape 3.

Si aucun package n'est fourni : Demandez : "Quels packages npm souhaitez-vous analyser ? Fournissez les vôtres, ceux des concurrents, ou un mélange. Exemple : esbuild, @hono/hono, zod, valibot"

python3 << 'PYEOF'
import json, sys

packages_raw = "PACKAGES_ICI"  # séparés par des virgules ou des sauts de ligne
product_context = "CONTEXTE_ICI"  # optionnel, peut être une chaîne vide

packages = [p.strip() for p in packages_raw.replace("\n", ",").split(",") if p.strip()]
if not packages:
    print("ERREUR : Aucun package fourni.")
    sys.exit(1)

print(f"Packages à analyser : {len(packages)}")
for p in packages:
    print(f"  {p}")

with open("/tmp/npl-input.json", "w") as f:
    json.dump({"packages": packages, "product_context": product_context}, f)
PYEOF

Étape 3 : Récupération des Données de Téléchargement sur 12 Semaines

Utilisez le script autonome s'il est disponible — il gère les Étapes 3, 4 et 5 en un seul appel, vous n'avez donc pas besoin d'exécuter les blocs de code en ligne ci-dessous.

# Vérifiez si le script existe
ls scripts/fetch.py 2>/dev/null && echo "script disponible" || echo "script non trouvé"

Si le script est disponible, exécutez-le directement et passez à l'Étape 6 :

python3 scripts/fetch.py PACKAGES_ICI --context "CONTEXTE_ICI" --output /tmp/npl-script-out.json

Chargez ensuite la sortie dans le format enrichi attendu à l'Étape 6 :

python3 << 'PYEOF'
import json

out = json.load(open("/tmp/npl-script-out.json"))
# La sortie du script a un tableau results — divisez-le en scored et enriched pour les Étapes 6-8
enriched = [r for r in out["results"] if "profile" in r]
scored = out["results"]
json.dump(scored, open("/tmp/npl-scored.json", "w"), indent=2)
json.dump(enriched, open("/tmp/npl-enriched.json", "w"), indent=2)
json.dump({"packages": [r["package"] for r in scored], "product_context": out.get("product_context", "")},
          open("/tmp/npl-input.json", "w"), indent=2)
print(f"Chargé : {len(scored)} packages | {out['breakout_count']} en breakout | {out['watching_count']} en surveillance")
PYEOF

Si le script n'est pas disponible, exécutez le code en ligne ci-dessous.

Récupérez les données de téléchargement quotidiennes pour chaque package depuis l'API npm Downloads. Agrégées en lots hebdomadaires.

python3 << 'PYEOF'
import json, urllib.request, sys, time
from datetime import datetime, timedelta, timezone
from collections import defaultdict
import urllib.parse

data = json.load(open("/tmp/npl-input.json"))
packages = data["packages"]

end_date = datetime.now(tz=timezone.utc)
start_date = end_date - timedelta(weeks=13)  # marge d'une semaine supplémentaire pour les semaines partielles
start_str = start_date.strftime("%Y-%m-%d")
end_str = end_date.strftime("%Y-%m-%d")

results = []
failed = []

for pkg in packages:
    # Encoder les packages scopés : @ -> %40, / -> %2F
    encoded = pkg.replace("@", "%40").replace("/", "%2F")
    url = f"https://api.npmjs.org/downloads/range/{start_str}:{end_str}/{encoded}"

    try:
        req = urllib.request.Request(url, headers={"User-Agent": "npm-downloads-to-leads/1.0"})
        with urllib.request.urlopen(req, timeout=20) as resp:
            raw = json.loads(resp.read())

        # Agrégation quotidienne en hebdomadaire par semaine ISO
        weekly = defaultdict(int)
        for entry in raw.get("downloads", []):
            day = datetime.strptime(entry["day"], "%Y-%m-%d")
            week_key = day.isocalendar()[:2]  # (année, numéro_semaine)
            weekly[week_key] += entry["downloads"]

        weeks = [v for k, v in sorted(weekly.items())]
        # Prenez les 12 derniers lots hebdomadaires complets
        weeks = weeks[-12:]

        results.append({
            "package": pkg,
            "weeks": weeks,
            "total_weeks": len(weeks),
            "current_weekly": weeks[-1] if weeks else 0,
            "status": "ok"
        })
        print(f"  {pkg} : {len(weeks)} semaines, dernière semaine : {weeks[-1]:,} téléchargements")

    except urllib.error.HTTPError as e:
        if e.code == 404:
            failed.append(pkg)
            results.append({"package": pkg, "weeks": [], "total_weeks": 0, "current_weekly": 0, "status": "not_found"})
            print(f"  {pkg} : NON TROUVÉ (404) — sera ignoré")
        else:
            failed.append(pkg)
            results.append({"package": pkg, "weeks": [], "total_weeks": 0, "current_weekly": 0, "status": f"error_{e.code}"})
            print(f"  {pkg} : Erreur HTTP {e.code}")
    except Exception as e:
        failed.append(pkg)
        results.append({"package": pkg, "weeks": [], "total_weeks": 0, "current_weekly": 0, "status": f"error"})
        print(f"  {pkg} : échec de la récupération ({e})")

    time.sleep(0.2)  # limitation de débit douce

json.dump(results, open("/tmp/npl-download-data.json", "w"), indent=2)
print(f"\nRécupération terminée. OK : {len(results) - len(failed)} | Échec/Non trouvé : {len(failed)}")
if failed:
    print(f"Ignorés : {', '.join(failed)}")
PYEOF

Si tous les packages renvoient une erreur 404 ou des erreurs : Arrêtez-vous. Dites à l'utilisateur : "Aucune donnée de téléchargement n'a pu être récupérée. Vérifiez que les noms des packages sont corrects et qu'ils existent sur npmjs.com. Les packages scopés doivent inclure le nom complet : @org/package."


Étape 4 : Calcul des Scores de Vélocité

Aucun appel API. Python pur. Calculez le score de vélocité, le taux de croissance et classez chaque package.

python3 << 'PYEOF'
import json

raw_results = json.load(open("/tmp/npl-download-data.json"))
scored = []

for item in raw_results:
    pkg = item["package"]
    weeks = item["weeks"]
    status = item["status"]

    if status != "ok" or len(weeks) < 4:
        scored.append({**item, "velocity_score": 0, "growth_pct": 0, "tier": "insufficient_data",
                       "recent_4_avg": 0, "prior_4_avg": 0})
        continue

    recent_4 = sum(weeks[-4:]) / 4
    prior_4 = sum(weeks[-8:-4]) / max(len(weeks) - 4, 1) if len(weeks) >= 8 else sum(weeks[:4]) / max(len(weeks[:4]), 1)
    recent_2 = sum(weeks[-2:]) / 2
    mid_2 = sum(weeks[-4:-2]) / 2 if len(weeks) >= 4 else recent_2

    growth_ratio = recent_4 / max(prior_4, 1)
    acceleration = recent_2 / max(mid_2, 1)
    growth_pct = round((growth_ratio - 1) * 100, 1)

    # Facteur de zone idéale : 500-500K téléchargements hebdomadaires
    if recent_4 < 500:
        noise_factor = max(recent_4 / 500, 0.1)
    elif recent_4 > 500_000:
        noise_factor = max(500_000 / recent_4, 0.1)
    else:
        noise_factor = 1.0

    velocity_score = round(growth_ratio * acceleration * noise_factor * 100, 1)

    # Classification
    if velocity_score > 80 and 500 < recent_4 < 500_000 and growth_ratio >= 1.5:
        tier = "breakout"
    elif velocity_score > 40 and recent_4 >= 500 and growth_ratio >= 1.2:
        tier = "watching"
    elif recent_4 < 500:
        tier = "too_early"
    elif recent_4 >= 500_000:
        tier = "established"
    else:
        tier = "steady"

    scored.append({
        **item,
        "velocity_score": velocity_score,
        "growth_pct": growth_pct,
        "recent_4_avg": round(recent_4),
        "prior_4_avg": round(prior_4),
        "tier": tier
    })

# Trier par velocity_score décroissant
scored.sort(key=lambda x: x["velocity_score"], reverse=True)

json.dump(scored, open("/tmp/npl-scored.json", "w"), indent=2)

breakout = [p for p in scored if p["tier"] == "breakout"]
watching = [p for p in scored if p["tier"] == "watching"]
too_early = [p for p in scored if p["tier"] == "too_early"]

print(f"Calcul des scores de vélocité terminé :")
print(f"  BREAKOUT : {len(breakout)}")
print(f"  SURVEILLANCE : {len(watching)}")
print(f"  STABLE/ÉTABLI : {len([p for p in scored if p['tier'] in ('steady','established')])}")
print(f"  TROP TÔT (<500/semaine) : {len(too_early)}")
print()
for p in scored[:10]:
    print(f"  {p['tier'].upper():12} {p['package']:30} score={p['velocity_score']:6.1f}  "
          f"{p['recent_4_avg']:>8,}/sem  croissance={p['growth_pct']:+.0f}%")

# Arrêtez-vous si rien d'intéressant à analyser
if not breakout and not watching:
    all_too_early = all(p["tier"] in ("too_early", "insufficient_data") for p in scored)
    if all_too_early:
        print("\nERREUR : Tous les packages sont en dessous du seuil de 500 téléchargements hebdomadaires pour une analyse de vélocité fiable.")
        print("Essayez des packages avec une adoption communautaire plus large.")
        import sys; sys.exit(1)
PYEOF

Si tous les packages sont en dessous de 500/semaine : Arrêtez-vous avec le message ci-dessus.


Étape 5 : Récupération des Profils des Mainteneurs

Uniquement pour les packages en breakout et en surveillance. Récupérez les métadonnées du registre npm, puis les profils utilisateur GitHub.

python3 << 'PYEOF'
import json, urllib.request, re, os, time

scored = json.load(open("/tmp/npl-scored.json"))
token = os.environ.get("GITHUB_TOKEN", "")

gh_headers = {"Accept": "application/vnd.github+json", "User-Agent": "npm-downloads-to-leads/1.0"}
if token:
    gh_headers["Authorization"] = f"Bearer {token}"

target_packages = [p for p in scored if p["tier"] in ("breakout", "watching")]
print(f"Récupération des profils pour {len(target_packages)} packages (breakout + surveillance)...")

gh_rate_remaining = 999
enriched = []

for item in target_packages:
    pkg = item["package"]
    profile = {"package": pkg, "npm_maintainers": [], "description": "", "keywords": [],
               "github_owner": None, "github_repo": None, "github_users": [], "npm_homepage": ""}

    # --- registre npm ---
    encoded = pkg.replace("@", "%40").replace("/", "%2F")
    reg_url = f"https://registry.npmjs.org/{encoded}"
    try:
        req = urllib.request.Request(reg_url, headers={"User-Agent": "npm-downloads-to-leads/1.0"})
        with urllib.request.urlopen(req, timeout=20) as resp:
            reg = json.loads(resp.read())

        profile["description"] = reg.get("description", "")
        profile["keywords"] = (reg.get("keywords") or [])[:6]
        profile["npm_homepage"] = reg.get("homepage", "")
        profile["npm_maintainers"] = [m.get("name", "") for m in reg.get("maintainers", []) if m.get("name")]

        # Extraire le propriétaire GitHub de l'URL du dépôt
        repo_field = reg.get("repository") or {}
        if isinstance(repo_field, dict):
            repo_url = repo_field.get("url", "")
        else:
            repo_url = str(repo_field)
        gh_match = re.search(r"github\.com[/:]([^/]+)/([^/.]+)", repo_url)
        if gh_match:
            profile["github_owner"] = gh_match.group(1)
            profile["github_repo"] = gh_match.group(2).rstrip(".git")

        print(f"  {pkg} : registre OK | mainteneurs={profile['npm_maintainers'][:3]} | "
              f"github_owner={profile['github_owner']}")
    except Exception as e:
        print(f"  {pkg} : échec de la récupération du registre ({e})")

    time.sleep(0.1)

    # --- Profils utilisateur GitHub ---
    candidates = []
    if profile["github_owner"]:
        candidates.append(profile["github_owner"])
    # Essayez également les noms d'utilisateur des mainteneurs npm (correspondent souvent à GitHub)
    for m in profile["npm_maintainers"][:2]:
        if m and m not in candidates:
            candidates.append(m)

    for username in candidates[:3]:
        if gh_rate_remaining <= 5:
            print(f"  Limite de débit GitHub basse ({gh_rate_remaining} restantes) — ignorance de {username}")
            break

        gh_url = f"https://api.github.com/users/{username}"
        req = urllib.request.Request(gh_url, headers=gh_headers)
        try:
            with urllib.request.urlopen(req, timeout=15) as resp:
                gh_rate_remaining = int(resp.headers.get("X-RateLimit-Remaining", 0))
                user_data = json.loads(resp.read())
                profile["github_users"].append({
                    "username": user_data.get("login", ""),
                    "name": user_data.get("name") or "",
                    "twitter": user_data.get("twitter_username") or "non trouvé sur GitHub",
                    "bio": (user_data.get("bio") or "")[:200],
                    "followers": user_data.get("followers", 0),
                    "public_repos": user_data.get("public_repos", 0),
                    "location": user_data.get("location") or ""
                })
                print(f"    -> GitHub @{username} : {user_data.get('name','')} | followers={user_data.get('followers',0)}")
                time.sleep(0.3)
        except urllib.error.HTTPError as e:
            if e.code == 403:
                print(f"    Limite de débit GitHub atteinte lors de la récupération de {username}")
                break
            elif e.code == 404:
                print(f"    Utilisateur GitHub @{username} non trouvé")
            else:
                print(f"    Erreur GitHub pour {username} : HTTP {e.code}")
            time.sleep(0.2)
        except Exception as e:
            print(f"    Échec GitHub pour {username} : {e}")
            time.sleep(0.2)

    enriched.append({**item, "profile": profile})

json.dump(enriched, open("/tmp/npl-enriched.json", "w"), indent=2)
print(f"\nEnrichissement terminé. Profils récupérés pour {len(enriched)} packages.")
if gh_rate_remaining < 10:
    print(f"\n⚠ Limite de débit GitHub basse ({gh_rate_remaining} restantes).")
    print("Les packages non enrichis resteront sur le classement de vélocité sans données de contact.")
PYEOF

Étape 6 : Classement des Leads de Package en Breakout

Chargez les données notées et enrichies. Produisez un tableau de bord textuel des packages en breakout.

python3 << 'PYEOF'
import json

scored = json.load(open("/tmp/npl-scored.json"))
enriched = json.load(open("/tmp/npl-enriched.json"))

breakout = [p for p in scored if p["tier"] == "breakout"]
watching = [p for p in scored if p["tier"] == "watching"]

print("═" * 68)
print("             CLASSEMENT DES LEADS — PACKAGES EN BREAKOUT")
print("═" * 68)

if not breakout:
    print("Aucun package en breakout détecté. Consultez la section 'Surveillance' pour les packages à surveiller.")
else:
    print(f"\n🔥 {len(breakout)} packages en croissance exponentielle détectés :\n")
    for i, p in enumerate(breakout, 1):
        prof = next((e["profile"] for e in enriched if e["package"] == p["package"]), None)
        print(f"  {i}. {p['package']}")
        print(f"     Score de vélocité : {p['velocity_score']:.1f}")
        print(f"     Téléchargements actuels : {p['recent_4_avg']:,}/semaine (croissance de {p['growth_pct']:.0f}%)")
        if prof:
            if prof["description"]:
                desc = prof["description"][:120]
                print(f"     Description : {desc}")
            owners = prof.get("github_owner", "") or "non déterminé"
            repo = prof.get("github_repo", "") or ""
            if repo:
                print(f"     Dépôt : https://github.com/{owners}/{repo}")
            if prof["github_users"]:
                for u in prof["github_users"][:2]:
                    tw = u["twitter"] if u["twitter"] and u["twitter"] != "non trouvé sur GitHub" else "-"
                    print(f"     Mainteneur : @{u['username']} | {u['name']} | Twitter : @{tw} | {u['followers']} followers")
        print()

print("─" * 68)
print(f"SURVEILLANCE : {len(watching)} packages à suivre\n")
for p in watching[:8]:  # Top 8 en surveillance
    print(f"  {p['package']:35} score={p['velocity_score']:6.1f}  {p['recent_4_avg']:>8,}/sem  croissance={p['growth_pct']:+.0f}%")

if len(watching) > 8:
    print(f"  ... et {len(watching)-8} autres")

# Sauvegarder le classement
leaderboard = {
    "breakout": [{"package": p["package"], "velocity_score": p["velocity_score"],
                   "recent_avg": p["recent_4_avg"], "growth_pct": p["growth_pct"]} for p in breakout],
    "watching": [{"package": p["package"], "velocity_score": p["velocity_score"],
                   "recent_avg": p["recent_4_avg"], "growth_pct": p["growth_pct"]} for p in watching[:15]]
}
json.dump(leaderboard, open("/tmp/npl-leaderboard.json", "w"), indent=2)
PYEOF

Étape 7 : Génération des Briefs de Leads

Pour chaque package en breakout, créez un brief structuré incluant les informations de contact et un message de prospection personnalisé.

python3 << 'PYEOF'
import json

data = json.load(open("/tmp/npl-input.json"))
enriched = json.load(open("/tmp/npl-enriched.json"))
product_context = data.get("product_context", "")

breakout_packages = [e for e in enriched if e["tier"] == "breakout"]

if not breakout_packages:
    print("Aucun package en breakout à traiter.")
    import sys; sys.exit(0)

leads = []

for item in breakout_packages:
    pkg = item["package"]
    prof = item["profile"]
    score = item["velocity_score"]
    weekly = item["recent_4_avg"]
    growth = item["growth_pct"]

    lead = {
        "package": pkg,
        "score": score,
        "weekly_downloads": weekly,
        "growth_percent": growth,
        "description": prof.get("description", ""),
        "keywords": prof.get("keywords", []),
        "maintainers": [],
        "repository": f"https://github.com/{prof['github_owner']}/{prof['github_repo']}" if prof.get("github_owner") and prof.get("github_repo") else "",
        "homepage": prof.get("npm_homepage", ""),
        "outreach_message": "",
        "data_quality_flags": []
    }

    # Préparer les informations sur les mainteneurs
    for u in prof.get("github_users", []):
        m = {
            "github_handle": u["username"],
            "name": u["name"],
            "twitter_handle": u["twitter"],
            "bio": u["bio"],
            "followers": u["followers"]
        }
        lead["maintainers"].append(m)

    # Drapeaux de qualité des données
    if not prof.get("github_users"):
        lead["data_quality_flags"].append("Aucun profil GitHub trouvé pour les mainteneurs — les messages de prospection ne peuvent pas être envoyés sans contact.")
    elif not any(u["twitter_handle"] and u["twitter_handle"] != "non trouvé sur GitHub" for u in prof.get("github_users", [])):
        lead["data_quality_flags"].append("Twitter non trouvé sur GitHub — l'utilisateur a peut-être un compte Twitter mais il n'est pas lié.")

    # --- Message de prospection personnalisé ---
    if lead["maintainers"]:
        target = lead["maintainers"][0]
        name_to_use = target["name"] or target["github_handle"]
        growth_statement = f"{weekly:,} téléchargements/semaine (croissance de {growth:+.0f}%)"
        pkg_desc = f" ({lead['description'][:80]})" if lead['description'] else ""
        context_hook = f" en lien avec {product_context}" if product_context else ""

        message = (
            f"Bonjour {name_to_use},\n\n"
            f"Je suis tombé sur votre package **{pkg}**{pkg_desc} et j'ai remarqué une croissance impressionnante : "
            f"{growth_statement}. Félicitations pour cette dynamique !\n\n"
            f"Je travaille sur {product_context if product_context else 'une initiative similaire'}{context_hook} "
            f"et je serais ravi d'échanger sur votre expérience avec {pkg}. "
            f"Voulez-vous que l'on en discute rapidement ?\n\n"
            f"Merci, et continuez votre beau travail."
        )
        lead["outreach_message"] = message

    leads.append(lead)

# Afficher les briefs
for i, l in enumerate(leads, 1):
    print("\n" + "█" * 70)
    print(f"  BRIEF #{i} : {l['package']}")
    print("█" * 70)
    print(f"  Score de vélocité  : {l['score']}")
    print(f"  Téléchargements    : {l['weekly_downloads']:,}/semaine ({l['growth_percent']:+.0f}% de croissance)")
    if l['description']:
        print(f"  Description        : {l['description'][:100]}")
    if l['keywords']:
        print(f"  Mots-clés          : {', '.join(l['keywords'][:5])}")
    if l['repository']:
        print(f"  Dépôt              : {l['repository']}")
    if l['maintainers']:
        m = l['maintainers'][0]
        tw = f"@{m['twitter_handle']}" if m['twitter_handle'] and m['twitter_handle'] != "non trouvé sur GitHub" else "non trouvé"
        print(f"  Contact            : @{m['github_handle']} ({m['name']}) | Twitter : {tw} | {m['followers']} followers")
    if l['data_quality_flags']:
        print(f"  ⚠ Drapeau qualité  : {'; '.join(l['data_quality_flags'])}")
    print(f"\n  Message de prospection :\n{l['outreach_message']}")

# Sauvegarder tous les briefs
json.dump(leads, open("/tmp/npl-leads.json", "w"), indent=2, ensure_ascii=False)
print(f"\n{'─' * 70}")
print(f"{len(leads)} briefs de leads générés et sauvegardés dans /tmp/npl-leads.json")
PYEOF

Étape 8 : Résumé et Prochaines Étapes

python3 << 'PYEOF'
import json

scored = json.load(open("/tmp/npl-scored.json"))
tenriched = json.load(open("/tmp/npl-enriched.json"))
tleads = json.load(open("/tmp/npl-leads.json"))

input_data = json.load(open("/tmp/npl-input.json"))

breakout = [p for p in scored if p["tier"] == "breakout"]
watching = [p for p in scored if p["tier"] == "watching"]
too_early = [p for p in scored if p["tier"] == "too_early"]
other = [p for p in scored if p["tier"] not in ("breakout", "watching", "too_early")]

print("═" * 70)
print("  RÉSUMÉ — Téléchargements npm vers Leads")
print("═" * 70)
print()
print(f"  Packages analysés         : {len([p for p in scored if p['status'] == 'ok'])}")
print(f"  Non trouvés / en échec    : {len([p for p in scored if p['status'] != 'ok'])}")
print(f"  En BREAKOUT (leads prêts) : {len(breakout)}")
print(f"  En SURVEILLANCE           : {len(watching)}")
print(f"  Trop tôt (< 500/semaine)  : {len(too_early)}")
print(f"  Stables / Établis         : {len(other)}")
print()

if leads:
    print("  Leads générés :")
    for l in leads:
        tw = ""
        if l["maintainers"]:
            m = l["maintainers"][0]
            tw = f" | Twitter : @{m['twitter_handle']}" if m['twitter_handle'] and m['twitter_handle'] != "non trouvé sur GitHub" else ""
        print(f"    • {l['package']} — score={l['score']} — contact : @{m['github_handle']}{tw}" if l["maintainers"] else f"    • {l['package']} — score={l['score']} — aucun contact trouvé")
    print()
    print("  Le fichier /tmp/npl-leads.json contient les briefs complets avec messages de prospection.")
    print("  Passez en revue chaque lead, personnalisez si nécessaire, puis envoyez la prospection.")
else:
    print("  Aucun lead généré. Aucun package n'a atteint le seuil de breakout.")
    print()
    print("  Conseils :")
    if watching:
        print("    • Surveillez les packages en 'surveillance' — certains pourraient devenir des breakouts la semaine prochaine.")
    if too_early:
        print("    • Les packages 'trop tôt' n'ont pas encore assez d'adoption. Revenez dans 4 à 8 semaines.")
    print("    • Essayez d'ajouter davantage de packages ou des packages avec une traction existante.")

print()
print("─" * 70)
print("  PRÊT POUR L'EXPORT ?")
print("  Les leads sont dans /tmp/npl-leads.json (format JSON structuré).")
print("═" * 70)
PYEOF

Format de Sortie Final

Le fichier /tmp/npl-leads.json contient un tableau d'objets lead, chacun avec cette structure :

{
  "package": "@hono/hono",
  "score": 245.3,
  "weekly_downloads": 12500,
  "growth_percent": 78.5,
  "description": "Ultrafast web framework for Cloudflare Workers and Deno",
  "keywords": ["web", "framework", "cloudflare", "workers", "deno"],
  "maintainers": [
    {
      "github_handle": "yusukebe",
      "name": "Yusuke Wada",
      "twitter_handle": "yusukebe",
      "bio": "Full-stack developer",
      "followers": 456
    }
  ],
  "repository": "https://github.com/honojs/hono",
  "homepage": "https://hono.dev",
  "outreach_message": "Bonjour Yusuke,\n\nJe suis tombé sur votre package @hono/hono et j'ai remarqué une croissance impressionnante : 12 500 téléchargements/semaine (croissance de +78%). Félicitations pour cette dynamique !...",
  "data_quality_flags": []
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

Varnan-Tech/opendirectory

Use when the user asks to generate a blog cover image, thumbnail, or article header. Automatically uses modern typography, brand logos, and Google Search grounding to create beautiful 16:9 images with Gemini 3.1 Flash Image Preview.

Varnan-Tech/opendirectory

World-class brand strategist and naming expert. Uses an interrogation-led discovery phase to extract your brand's DNA, then applies scientific naming frameworks (Phonosemantics) and automated multi-TLD domain checking.

Varnan-Tech/opendirectory

Use when the user asks to generate or update a project's CLAUDE or AGENTS context file from a codebase scan. Writes a focused file under 100 lines containing only the non-obvious build commands, conventions, and gotchas Claude Code needs.

Varnan-Tech/opendirectory

Use when the user wants to verify cold emails, enrich a lead list, or autonomously guess email addresses from a CSV using ValidEmail.co or the open-source Reacher engine.

Varnan-Tech/opendirectory

Competitive intelligence orchestrator tracking companies across 8+ platforms (GitHub, Twitter, Reddit, HN, PH, YC Jobs) with heat scores and AI briefings.

Varnan-Tech/opendirectory

Give it your product URL or description. It finds your top 5 competitors, runs three-track PR research across all of them (editorial, podcasts, communities), identifies which channels appear most frequently, looks up the journalist or host for each, and returns a tiered outreach list with story angles and ready-to-send cold pitch drafts tailored to your product. Use when asked to find PR opportunities, discover where competitors got featured, build a media outreach list, find which journalists cover my space, or get pitch templates for press coverage.

Varnan-Tech/opendirectory

Generate high-converting, deep-dive growth case studies in MDX format. Use this skill when asked to write a case study or blog post about a company's growth, tech stack, or product-led strategy. It handles the full pipeline (researching the company via Tavily, generating a 16:9 cover image, quality checking the draft, uploading assets to cloud storage, and pushing directly to the target repository).

Varnan-Tech/opendirectory

Scans your project for outdated npm, pip, Cargo, Go, or Ruby packages. Runs a CVE security audit. Fetches changelogs, summarizes breaking changes with Gemini, and opens one PR per risk group (patch, minor, major). Includes Diagnosis Mode for install conflicts. Use when asked to update dependencies, check for outdated packages, open dependency PRs, scan for package updates, audit for CVEs, or flag breaking changes in upgrades. Trigger when a user says "check for outdated packages", "update my dependencies", "open PRs for dependency updates", "scan for CVEs", or "which packages need upgrading".

Varnan-Tech/opendirectory

Generates and updates README.md and API reference docs by reading your codebase's functions, routes, types, schemas, and architecture. Uses graphify to build a knowledge graph first, then writes accurate docs from it. Use when asked to write docs, generate a README, document an API, update stale docs, create an API reference from code, add an architecture section, or document a project in any language. Trigger when a user says their docs are missing, outdated, or wants to document their codebase without writing it manually.

Varnan-Tech/opendirectory

Evaluates expired domain candidates against a target niche, scores them by topical relevance, historical activity level, and history cleanliness, then outputs a ranked shortlist with explainable reasoning and risk flags.

Varnan-Tech/opendirectory

Brutally honest developer-experience audit for a GitHub repo. Scores 10 DX dimensions (time-to-first-success, README clarity, visual proof, install, quick-start, docs, examples, community, trust, marketing), writes a shareable roast in the requested tone (brutal/honest/kind), produces a prioritized action plan ranked by impact × effort, and sketches an ideal README. Trigger when user says "roast my repo", "audit my README", "dx audit", "developer experience review", "score my GitHub project", "before launch checklist", or "make my repo shareable".

Varnan-Tech/opendirectory

Drafts and designs a complete HTML email newsletter from a topic or content brief. Outputs paste-ready HTML for Loops, Mailchimp, Beehiiv, Resend, or any standard email platform. Includes subject line options and plain-text fallback. Trigger when a user says "write a newsletter", "draft an email newsletter", "create an HTML email", "design an email for my subscribers", or "write a newsletter about [topic]".

Varnan-Tech/opendirectory

Takes a GitHub PR URL or the current branch and writes a plain-English explanation of what it does and why, then posts it as a PR comment. Use when asked to explain a PR, summarize a pull request, write a plain-English description of a PR, add a summary comment to a PR, or understand what a PR changes. Trigger when a user says "explain this PR", "summarize this pull request", "what does this PR do", "add a comment explaining the PR", or shares a GitHub PR URL and asks what it does.

Varnan-Tech/opendirectory

Audit how often LLMs recommend your brand vs competitors and generate a GEO action plan.

Varnan-Tech/opendirectory

Takes a competitor's public GitHub repo URL, fetches their open issues via the GitHub REST API, filters noise locally, clusters issues into 6 demand categories, computes a demand score per issue and per cluster, and outputs a ranked demand gap report with a GTM messaging brief. Use when asked to scan a competitor's GitHub issues, find what their users are begging for, turn GitHub complaints into product positioning, identify competitor feature gaps, or generate messaging from real user demand. Trigger when a user says "scan competitor issues", "what are users asking for on X repo", "find demand gaps in Y", "turn GitHub issues into messaging", or "what should I build based on competitor complaints".

Varnan-Tech/opendirectory

Find recurring confusion in your repo's GitHub Discussions, rank it by urgency, and draft the actual docs fixes and content angles — with verbatim community quotes and source links as evidence.

Varnan-Tech/opendirectory

SEO keyword research workflow for blog generation using Google Trends data. Use when writing blog posts, planning content calendars, or optimizing articles for search engines. Finds breakout keywords, builds content structure, and generates SEO-optimized blog outlines targeting tech and developer audiences.

Varnan-Tech/opendirectory

Generates a professionally designed case study PDF for B2B SaaS sales and marketing. Supports 7 page layouts, 9 style presets, 1-4 page output. Trigger when user says "create a case study", "write a customer story", "make a case study PDF", "design a success story", "turn this customer win into a PDF".

Varnan-Tech/opendirectory

Generates data visualization charts (bar, line, area, pie, doughnut, scatter, radar, treemap) as PNG using Apache ECharts v6. 1080×1080px default, 5 style presets, highlight annotations. Trigger when user says "create a chart", "visualize data", "make a bar chart", "line graph", "pie chart", "data visualization", "chart this data", "plot", "graph", or "visualize these numbers".

Varnan-Tech/opendirectory

Fetches low-star App Store and Google Play reviews, clusters them into broken-promise patterns, and generates a ranked copy brief with positioning opportunities.

Skills associés