SDK導入状況トラッカー は何をしますか?
SDK名を入力します。GitHubでそれをインポートしている公開リポジトリを検索します。各リポジトリを、企業シグナル、アクティビティ、ノイズ指標に基づいてスコアリングします。高シグナルリポジトリには、所有者とコントリビューターデータを付加します。企業採用者向けのアウトリーチコンテキストを付けた、ランク付けされた導入レポートを出力します。
重要なルール: 出力内のすべてのリポジトリは、GitHubコード検索APIのレスポンスに存在している必要があります。すべての企業名は、GitHubユーザーまたは組織APIの company フィールドまたは name フィールドから取得する必要があります。すべてのコントリビューターハンドルは、GitHubコントリビューターAPIのレスポンスから取得する必要があります。APIでフィールドが空の場合は「not listed」と記述し、推測、類推、外挿は行わないでください。
よくある間違い
| Agentがやりたがること | なぜそれが間違っているか |
|---|---|
| GITHUB_TOKENなしでコード検索を実行する | 認証されていないコード検索は、3 req/minのセカンダリレート制限に達し、意味のあるスキャンはすべて失敗します。GITHUB_TOKENは必須です。不足している場合は、ステップ1で明確なエラーを表示して停止してください。 |
| SDK自体のフォークを含める | SDKをフォークしたリポジトリは、コントリビューターまたはミラーであり、採用者ではありません。fork == true かつリポジトリ名がSDK名と一致するリポジトリはフィルタリングしてください。 |
| 500件すべての生の検索結果をAIに送信する | コード検索では最大500件の結果が返されますが、そのほとんどはノイズです。まずローカルでフィルタリングとスコアリングを行ってください。AI分析ステップには、上位20件の高シグナルリポジトリのみを送信してください。 |
| チュートリアルやサンプルリポジトリを採用者として報告する | リポジトリ名や説明に「example」「tutorial」「demo」「learn」「sample」「playground」「starter」が含まれているものは、本番環境ユーザーではありません。「tutorial_noise」としてマークし、リード概要から除外してください。 |
| 企業名や連絡先ハンドルをでっち上げる | すべての企業名はGitHubの company フィールドまたは組織の name フィールドから取得する必要があります。すべてのコントリビューターハンドルは、コントリビューターAPIのレスポンスから取得する必要があります。フィールドが空の場合は「not listed」と記述してください。 |
| すべてのエコシステムに対して1つのインポートパターンを使用する | require('sdk') ではPythonユーザーは見つかりません。SDK名からエコシステムを自動検出し、エコシステム固有のパターンを構築してください。自動検出があいまいな場合はユーザーに確認してください。 |
ステップ1: セットアップチェック
if [ -z "$GITHUB_TOKEN" ]; then
echo "ERROR: GITHUB_TOKEN is required for code search."
echo "Add a token at github.com/settings/tokens (no scopes needed for public repos)."
echo "Without it, GitHub code search hits a 3 req/min secondary rate limit and fails."
exit 1
fi
echo "GITHUB_TOKEN: set"
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'Search rate: {search[\"remaining\"]}/{search[\"limit\"]} remaining')
print(f'Core rate: {core[\"remaining\"]}/{core[\"limit\"]} remaining')
"
search remainingが0の場合: 停止します。X-RateLimit-Reset からのリセット時間をユーザーに伝えてください。
ステップ2: 入力の収集
会話から収集します:
- SDK名 (例:
@company/my-sdk、requests、github.com/org/go-sdk) - オプション: エコシステムの上書き (
npm、python、go、gem) -- 提供されなければ自動検出 - オプション: 結果から除外する組織/ユーザー (SDK所有者自身のリポジトリ)
- オプション: プロダクトコンテキスト文字列 (アウトリーチメッセージのパーソナライズに使用)
エコシステムの自動検出:
@で始まる、または-を含む: npm- スネークケースで
/や-を含まない: python github.com/を含む: go- それ以外: ユーザーに確認
SDK名が提供されない場合: 「追跡したいSDKまたはライブラリはどれですか?import文に表示されるパッケージ名を入力してください (例: stripe, @clerk/nextjs, requests)。」と尋ねます。
python3 << 'PYEOF'
import json, sys, re
sdk_name = "SDK_NAME_HERE"
ecosystem_override = "" # leave empty for auto-detect
exclude_owner = "" # optional: owner name to exclude (usually the SDK publisher)
product_context = "" # optional: what your product does
# Auto-detect ecosystem
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"Ecosystem: {ecosystem}")
print(f"Exclude owner: {exclude_owner or '(none)'}")
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
ステップ3: GitHubコードの検索
まずスタンドアロンスクリプトを確認します -- これはステップ3〜5を1回の呼び出しで処理します。
ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "script not found"
スクリプトが利用可能な場合、それを実行してステップ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
次に、ステップ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"Loaded: {len(out['raw_results'])} raw | {len(out['scored'])} scored | {len(out['enriched'])} enriched")
PYEOF
スクリプトが利用できない場合、以下のインラインコードを実行します。
インポートパターンを構築し、各パターンでGitHubコードを検索します:
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"]
# Build query patterns per ecosystem
if ecosystem == "npm":
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"Building queries for {sdk_name} ({ecosystem}):")
for q in queries:
print(f" {q}")
seen_repos = {} # full_name -> first match
search_rate_remaining = 30
flags = []
for i, query in enumerate(queries):
if search_rate_remaining <= 2:
flags.append(f"Code search rate limit low ({search_rate_remaining}) -- skipped remaining patterns")
print(f" Rate limit low ({search_rate_remaining}), stopping early")
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" Pattern '{query}': {total} total, {len(items)} fetched | rate_remaining={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"Code search rate limit hit on pattern '{query}'")
print(f" Rate limit hit (403) on '{query}'")
break
else:
print(f" HTTP {e.code} on '{query}'")
except Exception as e:
print(f" Error on '{query}': {e}")
# Respect 10 req/min code search limit
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 unique repos found: {len(results)}")
if flags:
print("Flags:", flags)
import urllib.parse # ensure imported
PYEOF
結果が0件の場合: ユーザーに伝えます: 「{sdk_name} をインポートしているリポジトリは見つかりませんでした。新しいパッケージの場合、GitHubコード検索のインデックス作成には1〜4週間かかります。SDKが確立されている場合は、references/import-patterns.md のインポートパターンを確認してください。」
ステップ4: リポジトリのスコアリングと分類
APIコールはありません。純粋なPythonです。ノイズをフィルタリングし、採用スコアを計算し、各リポジトリを分類します。
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")
# Exclude the SDK owner's own repos
if exclude_owner and owner_login == exclude_owner.lower():
continue
# Detect tutorial noise
name_words = set(repo_name.replace("-", " ").replace("_", " ").split())
desc_words = set(description.split())
is_tutorial = bool((name_words | desc_words) & TUTORIAL_WORDS)
# Also exclude if repo name IS the SDK name (likely a fork)
if repo_name == sdk_name or repo_name.startswith(sdk_name + "-"):
is_tutorial = True # treat as noise
# Classification tier
if is_tutorial:
tier = "tutorial_noise"
elif owner_type == "Organization":
tier = "company_org"
else:
tier = "solo_dev" # will be upgraded to affiliated_dev in Step 5 if company field populated
# Adoption score (filled with partial data now, enriched in Step 5)
score = 0
if owner_type == "Organization": score += 50
if not is_tutorial: score += 20
# stars, days_since_push, is_fork, is_archived added in Step 5
scored.append({
**repo,
"tier": tier,
"is_tutorial": is_tutorial,
"adoption_score": score,
"enriched": False
})
# Sort: company_org first, then by tier
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"Classification:")
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)} repos")
non_noise = [r for r in scored if r["tier"] != "tutorial_noise"]
print(f"\nTop repos for enrichment (non-noise): {len(non_noise)}")
for r in non_noise[:5]:
print(f" {r['full_name']} ({r['tier']}) -- {r.get('description','')[:60]}")
PYEOF
すべてのリポジトリがtutorial_noiseの場合: 停止します。ユーザーに伝えます: 「見つかったすべてのリポジトリはチュートリアルまたはサンプルであると思われます。公開GitHubでは本番環境の採用者は検出されませんでした。SDKが新しすぎるか、パッケージ名が一般的すぎて、検索結果がサンプルに支配されている可能性があります。」
ステップ5: 高シグナルリポジトリのエンリッチメント
ノイズでないリポジトリについて、完全なリポジトリメタデータ、所有者プロファイル、トップコントリビューターを取得します。tutorial_noiseリポジトリは完全にスキップします。
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"Enriching {len(target)} repos (skipping tutorial_noise)...")
enriched = []
for item in target:
full_name = item["full_name"]
owner_login = item["owner_login"]
owner_type = item.get("owner_type", "User")
# Fetch repo metadata
repo_data = gh_get(f"/repos/{full_name}")
if repo_data is None:
print(f" Skipping {full_name} (repo not found)")
continue
# Fetch owner profile for company field
owner_profile = gh_get(f"/users/{owner_login}") if owner_type == "User" else gh_get(f"/orgs/{owner_login}")
if core_remaining < 50:
flags.append(f"Core API rate limited near end ({core_remaining} remaining)")
print(" Core API rate low, stopping early")
break
# Fetch top contributors (max 5)
contributors = gh_get(f"/repos/{full_name}/contributors?per_page=5")
if contributors and isinstance(contributors, list):
contributor_list = [{"login": c["login"], "contributions": c["contributions"]} for c in contributors if not c.get("login", "").startswith("dependabot")]
else:
contributor_list = []
# Build enriched output
company_name = "not listed"
if owner_profile:
company_name = owner_profile.get("company") or "not listed"
if company_name == "not listed" and owner_type == "Organization":
company_name = owner_profile.get("name") or "not listed"
repo_name = repo_data.get("name", "")
stars = repo_data.get("stargazers_count", 0)
forks = repo_data.get("forks_count", 0)
is_fork = repo_data.get("fork", False)
is_archived = repo_data.get("archived", False)
pushed_at_str = repo_data.get("pushed_at", "")
days_since_push = 9999
if pushed_at_str:
pushed_at = datetime.fromisoformat(pushed_at_str.replace("Z", "+00:00"))
days_since_push = (datetime.now(timezone.utc) - pushed_at).days
# Update classification tier
tier = item["tier"]
if tier == "solo_dev" and company_name not in ["not listed", None, ""]:
tier = "affiliated_dev"
# Recalculate adoption score
score = 0
if owner_type == "Organization": score += 50
if tier == "affiliated_dev": score += 30
if not item["is_tutorial"]: score += 20
score += min(stars, 200) // 10 # up to 20 points for stars
days_score = max(0, 30 - days_since_push)
score += days_score # up to 30 points for recency
if is_fork: score -= 40
if is_archived: score -= 60
enriched.append({
"full_name": full_name,
"repo_url": repo_data.get("html_url", ""),
"description": repo_data.get("description") or "",
"owner_login": owner_login,
"owner_type": owner_type,
"company_name": company_name,
"stars": stars,
"forks": forks,
"is_fork": is_fork,
"is_archived": is_archived,
"pushed_at": pushed_at_str,
"days_since_push": days_since_push,
"created_at": repo_data.get("created_at", ""),
"top_contributors": contributor_list,
"matched_pattern": item["matched_pattern"],
"tier": tier,
"adoption_score": max(0, score)
})
time.sleep(0.1) # be gentle
# Sort by score descending
enriched.sort(key=lambda x: -x["adoption_score"])
json.dump(enriched, open("/tmp/sat-enriched.json", "w"), indent=2)
print(f"\nEnriched: {len(enriched)} repos")
if flags:
print("Flags:", flags)
# Summary
for r in enriched[:5]:
print(f" {r['full_name']} [score={r['adoption_score']}, tier={r['tier']}, company={r['company_name']}]")
PYEOF
ステップ6: 採用者レポートの生成
エンリッチされたデータをランク付けされたMarkdownレポートにフォーマットします。
python3 << 'PYEOF'
import json
enriched = json.load(open("/tmp/sat-enriched.json"))
input_data = json.load(open("/tmp/sat-input.json"))
sdk_name = input_data["sdk_name"]
# Filter noise for report body
company_repos = [r for r in enriched if r["tier"] == "company_org"]
affiliated_repos = [r for r in enriched if r["tier"] == "affiliated_dev"]
solo_repos = [r for r in enriched if r["tier"] == "solo_dev"]
print(f"# SDK Adoption Report: {sdk_name}\n")
print(f"**Generated:** $(date -u +%Y-%m-%dT%H:%M:%SZ)\n")
print(f"**Total repositories found:** {len(enriched)} (excluding tutorial noise)\n")
print("## Executive Summary\n")
print(f"- **Company organizations:** {len(company_repos)}")
print(f"- **Affiliated developers:** {len(affiliated_repos)}")
print(f"- **Solo developers:** {len(solo_repos)}")
print(f"- **Tutorial noise excluded:** {len([r for r in json.load(open('/tmp/sat-scored.json')) if r['tier'] == 'tutorial_noise'])}")
print()
if company_repos:
print("## Company Adopters (Ranked)\n")
for i, repo in enumerate(company_repos, 1):
dept = repo.get("department", "not specified")
company = repo["company_name"]
print(f"### {i}. {repo['full_name']}")
print(f"- **Company:** {company}")
print(f"- **Score:** {repo['adoption_score']}")
print(f"- **Stars:** {repo['stars']} | **Forks:** {repo['forks']}")
print(f"- **Last push:** {repo['days_since_push']} days ago")
print(f"- **Description:** {repo['description'][:150] if repo['description'] else 'not listed'}")
if repo['top_contributors']:
print(f"- **Top contributors:** {', '.join([c['login'] for c in repo['top_contributors'][:3]])}")
print()
if affiliated_repos:
print("## Affiliated Developers\n")
for i, repo in enumerate(affiliated_repos, 1):
print(f"### {i}. {repo['full_name']}")
print(f"- **Company:** {repo['company_name']}")
print(f"- **Score:** {repo['adoption_score']}")
print(f"- **Stars:** {repo['stars']} | **Forks:** {repo['forks']}")
print()
if solo_repos:
print("## Other Individual Developers\n")
for i, repo in enumerate(solo_repos[:10], 1):
print(f"{i}. **{repo['full_name']}** — Score: {repo['adoption_score']}, Stars: {repo['stars']}")
if len(solo_repos) > 10:
print(f"\n... and {len(solo_repos) - 10} more solo developer repos\n")
# Notify about unflagged potential
raw_results = json.load(open("/tmp/sat-raw-results.json"))
if len(raw_results) > len(enriched):
print(f"\n**Note:** {len(raw_results)} repos were found, but {len(raw_results) - len(enriched)} could not be enriched (API error or rate limit).\n")
print("---\n")
print("## Next Steps\n")
print("### Suggested Outreach Targets\n")
for repo in company_repos[:5]:
company = repo["company_name"]
print(f"- **{company}** ({repo['full_name']})")
print(f" - Outreach angle: This organization uses {sdk_name}. Reach out to their team.")
print()
print("### Notes\n")
print("- All company names verified against GitHub API")
print("- Tutorial and example repos filtered out")
print("- Scores are relative indicators, not absolute quality metrics")
PYEOF
ステップ7: AI分析 (オプション)
採用データを要約し、パーソナライズされたアウトリーチメッセージ案を生成するためにAIを使用します。重要: 上位20件の高シグナルリポジトリのみを送信します。すべてを送信しないでください。
各企業のアウトリーチメッセージには以下を含めます:
- リポジトリ名とそれがSDKをどのように使用しているか
- 会社名 (GitHub APIから検証済み)
- SDKの使用を認めたトップコントリビューター (依存関係ボットは除外)
- 製品コンテキスト (提供されている場合)
- パーソナライズされたアウトリーチの草案
# Example structure for AI analysis input (top 20 high-signal repos)
python3 << 'PYEOF'
import json
enriched = json.load(open("/tmp/sat-enriched.json"))[:20]
input_data = json.load(open("/tmp/sat-input.json"))
summary = {
"sdk_name": input_data["sdk_name"],
"product_context": input_data.get("product_context", "not provided"),
"total_repos_found": len(enriched),
"tiers": {},
"top_repos": enriched
}
# Count tiers
for r in enriched:
t = r["tier"]
summary["tiers"][t] = summary["tiers"].get(t, 0) + 1
print(json.dumps(summary, indent=2))
PYEOF
ステップ8: 完了と共有
結果をユーザーに提示します:
- 採用レポートを表示するかどうかを尋ねます
- 提供する場合はレポートの全文を表示します
- 企業リポジトリごとにパーソナライズされたアウトリーチメッセージを提案します
- let the user know if any flags were raised (rate limits, skipped patterns, etc.)