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 liste | La 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 bruts | Les 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 atteinte | Dé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écifique | Les 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 leads | Moins 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": []
}