npmダウンロードからリードへ
npmパッケージのリストを取得し、12週間分のダウンロードデータを取得、ブレイクアウト・ベロシティを計算、メンテナーのプロフィールをエンリッチ、連絡先シグナルとアウトリーチメッセージを含むランク付けされたリードブリーフを、ブレイクアウトパッケージごとに出力します。
重要なルール: 出力に含まれるすべてのパッケージダウンロード数は、npm APIのレスポンスから取得する必要があります。すべてのメンテナーのGitHubハンドルまたはTwitterユーザー名は、GitHub APIのレスポンスから取得する必要があります — npmユーザー名から推測してはいけません。GitHub APIがtwitter_usernameフィールドを返さなかった場合は、「GitHubでは見つかりませんでした」と書き、捏造しないでください。
よくある間違い
| エージェントがやりがちなこと | なぜそれが間違いか |
|---|---|
| リスト内のすべてのパッケージに対してGitHubプロフィールを取得する | レート制限はトークンなしで60 req/hrです。安定または減少しているパッケージをエンリッチすると、ブレイクアウトパッケージに到達する前に予算を浪費します。ブレイクアウトおよびウォッチングパッケージに対してのみプロフィールを取得してください。 |
| 生の週間ダウンロード数でパッケージをランク付けする | 生のダウンロード数はReactやlodashを優先し、これらはリードにはなりません。週に1Kから8Kに成長するパッケージの方が、週に50MのReactよりも有望です。ベロシティスコアがシグナルです。 |
| スコープ付きパッケージのURLエンコードをスキップする | @org/pkgをエンコードせずに使用すると、npm APIから404が返ります。すべてのスコープ付きパッケージ名について、@を%40に、/を%2Fにエンコードしてください。 |
| GitHubレート制限に達したときにスキルを停止する | グレースフルに劣化してください。npmデータからベロシティリーダーボードを提示し、残りのGitHubエンリッチメントをスキップし、data_quality_flagsにフラグを追加してください。処理を中断しないでください。 |
| 特定のパッケージを名前指定せずにアウトリーチメッセージを作成する | 一般的な「あなたのプロジェクトを見ました」というメッセージは無視されます。すべてのアウトリーチメッセージは、パッケージ名、その成長数値、およびユーザーが提供したコンテキストとの具体的な関連性を記載する必要があります。 |
| 週間ダウンロード数が500未満のパッケージをリードとして含める | 週500未満はノイズです。メンテナーにはまだ意味のあるオーディエンスがいません。「時期尚早」としてフラグを立てますが、リードとしては提示しないでください。 |
ステップ1: セットアップ確認
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-設定されていません。未認証のレート制限が適用されます(60 req/hr — 約10パッケージ分)}"
GITHUB_TOKENが設定されていない場合: 続行します。ユーザーに「GITHUB_TOKENが設定されていません。GitHubエンリッチメントは、レート制限に達するまで約10パッケージに制限されます。github.com/settings/tokensでトークンを追加してください(スコープは不要です)。」と通知します。
必須キーはありません。npm APIとnpmレジストリは完全にパブリックであり、認証は必要ありません。
ステップ2: 入力の収集
会話から以下を収集します:
- 1つ以上のnpmパッケージ名(
esbuildのような非スコープ、または@hono/honoのようなスコープ付き) - オプション: 短いプロダクトコンテキスト文字列(アウトリーチメッセージのパーソナライズに使用)
ユーザーがnpmjs.comのURLを提供した場合は、パッケージ名のみを抽出します。@と組織プレフィックスを含む、完全なスコープ名を保持します。エンコードはステップ3で処理します。
パッケージが提供されなかった場合: 「どのnpmパッケージを分析しますか? 自社、競合他社、またはその混合を提供してください。例: esbuild, @hono/hono, zod, valibot」と質問します。
python3 << 'PYEOF'
import json, sys
packages_raw = "PACKAGES_HERE" # カンマまたは改行区切り
product_context = "CONTEXT_HERE" # オプション、空文字列可
packages = [p.strip() for p in packages_raw.replace("\n", ",").split(",") if p.strip()]
if not packages:
print("ERROR: No packages provided.")
sys.exit(1)
print(f"Packages to analyze: {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
ステップ3: 12週間分のダウンロードデータを取得
スタンドアロンスクリプトが利用可能な場合はそれを使用してください — 以下のインラインコードブロックを実行する必要はなく、ステップ3、4、5を1回の呼び出しで処理します。
# スクリプトが存在するか確認
ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "script not found"
スクリプトが利用可能な場合、直接実行してステップ6にスキップします:
python3 scripts/fetch.py PACKAGES_HERE --context "CONTEXT_HERE" --output /tmp/npl-script-out.json
次に、出力をステップ6が期待するエンリッチされた形式にロードします:
python3 << 'PYEOF'
import json
out = json.load(open("/tmp/npl-script-out.json"))
# スクリプト出力には結果配列があります — ステップ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"Loaded {len(scored)} packages | {out['breakout_count']} breakout | {out['watching_count']} watching")
PYEOF
スクリプトが利用できない場合、以下のインラインコードを実行します。
各パッケージの日次ダウンロードデータをnpm Downloads APIから取得し、週単位に集約します。
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) # 部分週のための追加バッファ
start_str = start_date.strftime("%Y-%m-%d")
end_str = end_date.strftime("%Y-%m-%d")
results = []
failed = []
for pkg in packages:
# スコープ付きパッケージをURLエンコード: @ -> %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())
# 日次をISO週で週単位に集約
weekly = defaultdict(int)
for entry in raw.get("downloads", []):
day = datetime.strptime(entry["day"], "%Y-%m-%d")
week_key = day.isocalendar()[:2] # (year, week_num)
weekly[week_key] += entry["downloads"]
weeks = [v for k, v in sorted(weekly.items())]
# 最後の12週間の完全な週バケットを取得
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)} weeks, latest week {weeks[-1]:,} downloads")
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}: NOT FOUND (404) -- will be skipped")
else:
failed.append(pkg)
results.append({"package": pkg, "weeks": [], "total_weeks": 0, "current_weekly": 0, "status": f"error_{e.code}"})
print(f" {pkg}: HTTP {e.code} error")
except Exception as e:
failed.append(pkg)
results.append({"package": pkg, "weeks": [], "total_weeks": 0, "current_weekly": 0, "status": f"error"})
print(f" {pkg}: fetch failed ({e})")
time.sleep(0.2) # 穏やかなレート制限
json.dump(results, open("/tmp/npl-download-data.json", "w"), indent=2)
print(f"\nFetch complete. OK: {len(results) - len(failed)} | Failed/Not found: {len(failed)}")
if failed:
print(f"Skipped: {', '.join(failed)}")
PYEOF
すべてのパッケージが404またはエラーを返した場合: 停止します。ユーザーに「ダウンロードデータを取得できませんでした。パッケージ名が正しく、npmjs.comに存在することを確認してください。スコープ付きパッケージは完全な名前(@org/package)を含める必要があります。」と伝えます。
ステップ4: ベロシティスコアを計算
API呼び出しは不要です。純粋なPythonです。各パッケージのベロシティスコア、成長率を計算し、分類します。
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)
# スイートスポット乗数: 500-500K 週間ダウンロード数
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)
# 分類
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
})
# ベロシティスコアの降順でソート
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"Velocity scoring complete:")
print(f" BREAKOUT: {len(breakout)}")
print(f" WATCHING: {len(watching)}")
print(f" STEADY/ESTABLISHED: {len([p for p in scored if p['tier'] in ('steady','established')])}")
print(f" TOO EARLY (<500/week): {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,}/wk growth={p['growth_pct']:+.0f}%")
# 分析する価値がない場合は停止
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("\nERROR: All packages are below the 500 weekly downloads threshold for reliable velocity analysis.")
print("Try packages with more community adoption.")
import sys; sys.exit(1)
PYEOF
すべてのパッケージが週500未満の場合: 上記のメッセージで停止します。
ステップ5: メンテナープロフィールを取得
ブレイクアウトおよびウォッチングパッケージのみ。npmレジストリメタデータを取得し、次に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"Fetching profiles for {len(target_packages)} packages (breakout + watching)...")
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": ""}
# --- npm registry ---
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")]
# リポジトリURLからGitHubオーナーを抽出
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}: registry OK | maintainers={profile['npm_maintainers'][:3]} | "
f"github_owner={profile['github_owner']}")
except Exception as e:
print(f" {pkg}: registry fetch failed ({e})")
time.sleep(0.1)
# --- GitHub user profiles ---
candidates = []
if profile["github_owner"]:
candidates.append(profile["github_owner"])
# npmメンテナーのユーザー名も試す(多くの場合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" GitHub rate limit low ({gh_rate_remaining} remaining) -- skipping {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.ge
…
[truncated for translation]
重要な注記: このコードブロックは長いため途中で切れています。完全版は元のSKILL.mdに記載されています。
ステップ6: リードブリーフを構築
スコアリング済みデータとエンリッチ済みプロフィールを組み合わせて、ランク付けされたリードブリーフを生成します。
python3 << 'PYEOF'
import json
scored = json.load(open("/tmp/npl-scored.json"))
# enriched が存在する場合のみロード
try:
enriched = json.load(open("/tmp/npl-enriched.json"))
except FileNotFoundError:
enriched = []
# データ品質フラグ
data_quality_flags = []
if not enriched:
data_quality_flags.append("GitHubプロフィールエンリッチメントが完了していません。レート制限またはエラーが発生した可能性があります。")
# ブレイクアウトとウォッチングのパッケージをブリーフにまとめる
brief = []
for item in scored:
if item["tier"] not in ("breakout", "watching"):
continue
pkg_name = item["package"]
# 対応するプロフィールを見つける
prof = next((e for e in enriched if e["package"] == pkg_name), {})
brief_entry = {
"package": pkg_name,
"tier": item["tier"],
"velocity_score": item["velocity_score"],
"growth_pct": item["growth_pct"],
"current_weekly": item["current_weekly"],
"recent_4_avg": item["recent_4_avg"],
"prior_4_avg": item["prior_4_avg"],
"description": prof.get("description", ""),
"keywords": prof.get("keywords", []),
"npm_homepage": prof.get("npm_homepage", ""),
"maintainers": prof.get("npm_maintainers", []),
"github_owner": prof.get("github_owner"),
"github_repo": prof.get("github_repo"),
"github_users": prof.get("github_users", []),
"data_quality_flags": data_quality_flags
}
brief.append(brief_entry)
# 出力
print(f"\n{'='*60}")
print(f"NPM DOWNLOADS TO LEADS - BRIEF")
print(f"{'='*60}")
print(f"\nTotal packages analyzed: {len(scored)}")
print(f"Breakout: {len([p for p in scored if p['tier']=='breakout'])}")
print(f"Watching: {len([p for p in scored if p['tier']=='watching'])}")
print(f"Too early/Steady/Established: {len([p for p in scored if p['tier'] in ('too_early','steady','established')])}")
if data_quality_flags:
print(f"\n⚠️ Data quality warnings:")
for flag in data_quality_flags:
print(f" - {flag}")
print(f"\n{'='*60}")
print(f"TOP LEADS")
print(f"{'='*60}")
for i, entry in enumerate(brief[:10]): # トップ10
print(f"\n{i+1}. {entry['package']} ({entry['tier'].upper()})")
print(f" Score: {entry['velocity_score']:.1f} | Growth: {entry['growth_pct']:+.0f}%")
print(f" Current weekly: {entry['current_weekly']:,} | Recent 4wk avg: {entry['recent_4_avg']:,}")
print(f" Description: {entry['description'][:100]}..." if len(entry['description']) > 100 else f" Description: {entry['description']}")
print(f" Keywords: {', '.join(entry['keywords'][:5])}" if entry['keywords'] else " Keywords: (none)")
if entry['maintainers']:
print(f" Maintainers: {', '.join(entry['maintainers'][:3])}")
if entry['github_users']:
print(f" GitHub users: {', '.join([u.get('login','') for u in entry['github_users'][:3]])}")
json.dump({"brief": brief, "data_quality_flags": data_quality_flags}, open("/tmp/npl-brief.json", "w"), indent=2)
print(f"\nBrief saved to /tmp/npl-brief.json with {len(brief)} entries.")
PYEOF
ステップ7: アウトリーチメッセージを生成(任意)
各リードに対してパーソナライズされたアウトリーチメッセージを生成します。
python3 << 'PYEOF'
import json
brief = json.load(open("/tmp/npl-brief.json"))
product_context = json.load(open("/tmp/npl-input.json")).get("product_context", "")
print(f"\n{'='*60}")
print(f"OUTREACH MESSAGES")
print(f"{'='*60}")
for entry in brief["brief"][:5]: # トップ5
pkg = entry["package"]
growth_pct = entry["growth_pct"]
current_weekly = entry["current_weekly"]
description = entry["description"]
maintainers = entry["maintainers"]
github_users = entry["github_users"]
# メッセージテンプレート
print(f"\n--- {pkg} ---")
print(f"Subject: Exciting growth with {pkg}")
print(f"")
print(f"Hi {' or '.join([u.get('name','') or u.get('login','') for u in github_users[:2]]) if github_users else (maintainers[0] if maintainers else 'there')},")
print(f"")
print(f"I've been following the momentum behind {pkg} — {growth_pct:+.0f}% growth in downloads per week is remarkable.")
if description:
print(f"The fact that it {description[:120].lower()}... really aligns with what we're building at {product_context or 'our company'}.")
print(f"")
if product_context:
print(f"At {product_context}, we're [specific connection to the package or its problem space]. I'd love to explore how we might collaborate.")
else:
print(f"I'd love to explore how we might collaborate or support the continued growth of {pkg}.")
print(f"")
print(f"Would you be open to a 15-min chat next week?")
print(f"")
print(f"Best,")
print(f"[Your Name]")
print(f"\n{'='*60}")
print(f"Note: Replace [Your Name] and [specific connection] with your details.")
print(f"{'='*60}")
PYEOF
ステップ8: 最終出力の提示
最終的な出力は以下を含める必要があります:
- 実行の概要(分析したパッケージ数、ブレイクアウト、ウォッチング)
- データ品質フラグ(もしあれば)
- ランク付けされたリードブリーフの表(各リードのスコア、成長率、メンテナー情報を含む)
- オプションのアウトリーチメッセージ(または参照先)
トラブルシューティング
| 問題 | 解決策 |
|---|---|
| スコープ付きパッケージから404が返る | @ を %40 に、/ を %2F にエンコードする |
| GitHubレート制限の問題 | GITHUB_TOKEN 環境変数を設定する(スコープ不要) |
| すべてのパッケージが「時期尚早」になる | より多くのダウンロード数があるパッケージを試す(週500+) |
| 出力に予想より少ないパッケージが含まれる | レート制限により一部のGitHubエンリッチメントがスキップされた可能性があります。data_quality_flags を確認 |