競合PRファインダー
製品のURLを入力してください。競合を特定し、彼らが利用しているすべてのPRチャネル(ニュース、ポッドキャスト、コミュニティ)を調査。複数の競合にまたがって出現するチャネル(実績のあるターゲット)を抽出し、各チャネルのジャーナリストやホストを特定して、あなたの製品向けにパーソナライズされたコールドピッチ文面を作成します。
ゼロ幻覚ポリシー: 出力内のすべてのチャネル、ジャーナリスト名、ストーリーアングル、ピッチ詳細は、特定のTavily検索結果または取得した製品ページに遡れる必要があります。これは以下に適用されます:
- 競合名:Tavily検索結果に出現する必要があり、AIの学習知識からは使用不可
- チャネル名:検索結果にURLが存在する必要がある
- ジャーナリスト/ホスト名:Tavilyのスニペットにそのままの形で出現する必要がある
- ストーリーアングル:検索結果の記事/エピソードタイトルからのみ抽出される
- ピッチ文面:検索データと製品分析からの具体的な証拠を参照する必要がある
よくある間違い
| エージェントがやりがちなこと | なぜそれが間違いか |
|---|---|
| 学習知識からジャーナリストの名前を挙げる | すべてのジャーナリスト名は検索結果のスニペットに遡れる必要がある。「Sarah PerezはTechCrunchでスタートアップを担当」と記憶から書くのは幻覚である。 |
| 証拠となるURLなしでチャネルを列挙する | 出力内のすべてのチャネルは、競合がそこで取り上げられたことを証明するPR検索結果からのURLが少なくとも1つ必要である。 |
| 競合確認ステップをスキップする | 常に発見した競合を表示し、ユーザーの確認を待つこと。間違った競合では検索と出力が無駄になる。 |
| 一般的なピッチ(「ぜひ取り上げてください」)を生成する | すべてのピッチは、証拠からの特定のアングルと製品分析からの具体的な差別化ポイントの両方を参照しなければならない。 |
| 1社の競合しかいないチャネルをTier 1とマークする | Tier 1 = 3社以上の競合。Tier 2 = 正確に2社。Tier 3 = 1社。実績のないチャネルを昇格させてはならない。 |
| 出力にemダッシュを使用する | すべてのemダッシュ(—)をハイフン(-)に置き換えること。 |
実行前に参照ファイルを読む
cat references/pr-channel-types.md
cat references/pitch-guide.md
cat references/tier-scoring.md
ステップ 1: セットアップ確認
echo "TAVILY_API_KEY: ${TAVILY_API_KEY:+set}${TAVILY_API_KEY:-NOT SET -- required}"
echo "FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:+set}${FIRECRAWL_API_KEY:-not set, Tavily extract will be used as fallback}"
TAVILY_API_KEYがない場合: 直ちに停止し、ユーザーに「競合調査とPR掲載実績調査にはTAVILY_API_KEYが必要です。代替手段はありません。app.tavily.comで取得してください。無料枠:月1000クレジット(フル実行約43回分、1回あたり約23回の検索)です。.envファイルに追加してください。」と伝えてください。
FIRECRAWL_API_KEYのみがない場合: 続行します。URL取得にはTavily extractが使用されます。
ステップ 2: 入力の解析
会話から収集:
product_url:取得するURL(必須。ユーザーが説明文を直接貼り付けた場合を除く)product_name:任意。提供されない場合はページから導出geography:任意。米国/欧州/グローバル。デフォルト:米国
ユーザーが説明文のみを貼り付けた場合(URLなし): ステップ3と4をスキップします。貼り付けられたテキストをproduct_contentとして使用し、ステップ4(製品分析)に直接進みます。page_sourceをuser_descriptionに設定し、data_quality_flagsにメモします。
URLも説明文もない場合: 「あなたの製品またはスタートアップのURLは何ですか?または、短い説明文を貼り付けてください:何をするものか、誰のためのものか、競合と何が違うのか。」と尋ねます。
製品スラッグを導出:
PRODUCT_SLUG=$(python3 -c "
from urllib.parse import urlparse
import sys
url = 'URL_HERE'
if url.startswith('http'):
host = urlparse(url).netloc.replace('www.', '')
print(host.split('.')[0])
else:
import re
print(re.sub(r'[^a-z0-9]', '-', url[:30].lower()).strip('-'))
")
echo "Product slug: $PRODUCT_SLUG"
ステップ 3: 製品ページの取得
プライマリ:Firecrawl(FIRECRAWL_API_KEYが設定されている場合)
curl -s -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "URL_HERE", "formats": ["markdown"], "onlyMainContent": true}' \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('data', {}).get('markdown', '') or d.get('markdown', '')
print(f'Fetched via Firecrawl: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
フォールバック:Tavily extract(FIRECRAWL_API_KEYが設定されていない場合)
curl -s -X POST https://api.tavily.com/extract \
-H "Content-Type: application/json" \
-d "{\"api_key\": \"$TAVILY_API_KEY\", \"urls\": [\"URL_HERE\"]}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
content = d.get('results', [{}])[0].get('raw_content', '')
print(f'Fetched via Tavily extract: {len(content)} characters')
open('/tmp/cprf-product-raw.md', 'w').write(content)
"
チェックポイント:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()
if len(content) < 200:
print('ERROR: fewer than 200 characters fetched')
else:
print(f'Content OK: {len(content)} characters')
"
コンテンツが200文字未満の場合: 取得を停止します。ユーザーに「製品ページから読み取り可能なコンテンツが返されませんでした。サイトがJavaScriptでレンダリングされており、取得がブロックされた可能性があります。製品の説明文(何をするものか、誰のためのものか、競合と何が違うのか)を直接貼り付けてください。」と伝えます。
ステップ 4: 製品分析(AI)
ページ内容を表示:
python3 -c "
content = open('/tmp/cprf-product-raw.md').read()[:5000]
print('=== PRODUCT PAGE (first 5000 chars) ===')
print(content)
"
AIへの指示: 上記の製品ページを分析し、以下を抽出:
product_name:製品または会社名one_line_description:何をするものか、誰のためか、中核的価値提案。20語未満。マーケティング用語は使用禁止。例:「パイプラインをセルフホストする開発者チーム向けのCI/CD自動化」industry_taxonomy:l1(最上位例:developer tools / fintech / healthtech / consumer)、l2(セクター例:devops / payments / telemedicine)、l3(特定ニッチ例:CI/CD automation / embedded payments / async video consultation)。「technology」のような曖昧なラベルは認められない。differentiators:この製品を一般的な競合から区別する具体的な点を2~3つ。これらはピッチ文面に直接使用される。具体的に書くこと。例:["セルフホスト型パイプラインランナー - データがインフラから流出しない", "動的ステップ生成によるモノレポのネイティブサポート"]icp:buyer_persona(役職)、company_type、company_sizegeography_bias:US / Europe / global / unclearpage_source:"live_page" または "user_description"
/tmp/cprf-product-analysis.jsonに書き込み:
python3 << 'PYEOF'
import json
analysis = {
# 上記の分析結果を記入
"product_name": "",
"one_line_description": "",
"industry_taxonomy": {"l1": "", "l2": "", "l3": ""},
"differentiators": [],
"icp": {"buyer_persona": "", "company_type": "", "company_size": ""},
"geography_bias": "US",
"page_source": "live_page"
}
json.dump(analysis, open('/tmp/cprf-product-analysis.json', 'w'), indent=2)
print('Product analysis written.')
PYEOF
確認:
python3 -c "
import json
a = json.load(open('/tmp/cprf-product-analysis.json'))
print('Product:', a['product_name'])
print('Industry:', a['industry_taxonomy']['l1'], '>', a['industry_taxonomy']['l2'], '>', a['industry_taxonomy']['l3'])
print('Differentiators:')
for d in a['differentiators']:
print(f' - {d}')
"
ステップ 4b: フェーズ1 -- 競合発見
ls scripts/research.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/research.py not found -- cannot continue"
python3 scripts/research.py \
--phase discover \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-competitors-raw.json
AIレビューのために結果を表示:
python3 -c "
import json
data = json.load(open('/tmp/cprf-competitors-raw.json'))
print(f'Searches run: {len(data[\"competitor_searches\"])}')
for s in data['competitor_searches']:
print(f'\nQuery: {s[\"query\"]}')
print(f'Answer: {s.get(\"answer\",\"\")[:400]}')
for r in s.get('results', [])[:5]:
print(f' - {r[\"title\"]} | {r[\"url\"]}')
print(f' {r.get(\"content\",\"\")[:200]}')
"
AIへの指示: 上記の検索結果を読んでください。正確に5社の競合企業を選びます。条件:
- 検索結果のタイトル、回答、またはスニペットに名前が記載されている
- 分析対象製品と同じL3ニッチに属している
- 実際の競合製品であること(代理店、コンサルタンシー、リスト記事ではない)
- 互いに明確に区別されていること(同一企業が別名で登場しているものではない)
各競合について記入:name、url(競合が出現した検索結果URL)、description(スニペットからの一文)、source_url(見つかった検索結果のURL)
ステップ 5: 競合確認
発見した競合をユーザーに表示:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# 上記の検索結果から5社の競合を記入
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\n{analysis['product_name']}の{analysis['industry_taxonomy']['l3']}における5社の競合を発見しました:\n")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['name']} -- {c['description']}")
print(f" {c['url']}")
data = json.load(open('/tmp/cprf-competitors-raw.json'))
data['competitor_candidates'] = candidates
json.dump(data, open('/tmp/cprf-competitors-raw.json', 'w'), indent=2)
PYEOF
ユーザーに伝える:「これらがPR掲載実績を調査する5社の競合です。追加、削除、入れ替えが可能です。または「よさそう」と言って続行してください。」
確認を待つ。 ユーザーがリストを編集(追加/削除/入れ替え)した場合は、それに応じて候補を更新します。その後、確認済みリストを書き込みます:
python3 << 'PYEOF'
import json
# 確認済み競合リストを記入(ユーザーレビュー後)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"PR調査対象の競合{len(confirmed)}社を確認しました。")
for c in confirmed:
print(f" - {c['name']} ({c['url']})")
PYEOF
ステップ 6: 3トラックPR調査(フェーズ2)
python3 scripts/research.py \
--phase pr-research \
--competitors /tmp/cprf-competitors-confirmed.json \
--product-analysis /tmp/cprf-product-analysis.json \
--tavily-key "$TAVILY_API_KEY" \
--output /tmp/cprf-pr-raw.json
これにより、競合1社あたり3回の検索が実行されます(合計15回):
- トラックA(編集系):
"[competitor]" featured press coverage TechCrunch Forbes Wired article interview - トラックB(ポッドキャスト):
"[competitor]" founder CEO podcast interview appeared on episode - トラックC(コミュニティ):
"[competitor]" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com
カバレッジサマリーを表示:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
print(f'調査した競合数: {data[\"competitors_researched\"]}')
print()
for r in data['results']:
print(f'{r[\"competitor\"]}:')
for track, tdata in r['tracks'].items():
n = len(tdata.get('results', []))
print(f' {track:12}: {n} results')
"
ある競合の全3トラックが0件の結果を返した場合: この競合はプレスカバレッジが非常に少ないです。data_quality_flagsにメモし、処理を続行します。残りの4社でクロス競合パターンが機能します。
ステップ 7: パターン分析(AI)
すべての生のPR結果を表示:
python3 -c "
import json
data = json.load(open('/tmp/cprf-pr-raw.json'))
for r in data['results']:
print(f'\n=== {r[\"competitor\"]} ===')
for track, tdata in r['tracks'].items():
print(f'\n--- Track {track.upper()} ---')
print(f'Query: {tdata[\"query\"]}')
print(f'Answer: {tdata.get(\"answer\",\"\")[:400]}')
for item in tdata.get('results', [])[:5]:
print(f' Title: {item[\"title\"]}')
print(f' URL: {item[\"url\"]}')
print(f' Snippet: {item.get(\"content\",\"\")[:200]}')
"
AIへの指示: 上記のすべての検索結果を読んでください。チャネルの出現頻度マップを構築します。
ステップ1 -- URLをルートドメインに正規化: https://techcrunch.com/2023/06/article-title → techcrunch.com。https://open.spotify.com/episode/... → ポッドキャストとして識別(spotify episode)。https://www.reddit.com/r/devops/ → reddit.com/r/devops。
ステップ2 -- 出現回数をカウント: 各チャネルのルートからの結果に、何社の異なる競合が出現しましたか?競合AのトラックAと競合BのトラックAの両方に出現するチャネルは頻度2としてカウントされます。
ステップ3 -- チャネルを階層化(references/tier-scoring.mdに従う):
- Tier 1:3社以上の競合に出現
- Tier 2:正確に2社の競合に出現
- Tier 3:1社の競合に出現
ステップ4 -- ストーリーアングルを抽出:結果の記事/エピソードタイトルから抽出します。以下に分類:funding-announcement / product-launch / founder-story / trend-piece / category-creation / how-to / comparison / award。推測はせず、タイトルに見えるアングルのみを分類します。
ステップ5 -- チャネルタイプを分類:各チャネルについて:editorial / podcast / community / newsletter。
/tmp/cprf-pr-patterns.jsonに書き込み:
python3 << 'PYEOF'
import json
patterns = {
"tier_1_channels": [
# 記入 -- 3社以上の競合に出現するチャネル
# 各要素:{"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": int, "found_in_competitors": [str],
# "evidence_urls": [str], "story_angles_used": [str],
# "journalist_name": "", "journalist_beat": ""}
],
"tier_2_channels": [
# 記入 -- 正確に2社の競合に出現するチャネル
# 各要素:{"channel_name": str, "channel_url": str, "channel_type": str,
…
[truncated for translation]