competitor-pr-finder について
competitor-pr-finderは、製品URLまたは説明を基に競合のPR露出を調査するスキルです。Tavilyの検索結果と製品ページを根拠に掲載先を評価し、個別のピッチを作成します。
主な機能
- 01競合の発見と確認
- 02メディア、ポッドキャスト、コミュニティの調査
- 03競合数に基づく掲載先の分類
- 04検索根拠に基づく記者・司会者の特定
- 05製品の差別化要素を反映したピッチ作成
活用シーン
- 01競合が利用したPR掲載先の調査
- 02複数の競合に共通する掲載先の優先
- 03上位掲載先向けの根拠あるピッチ作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill competitor-pr-finderインストール後、Claude Code で/competitor-pr-finderを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.env.exampleREADME.mdevals/evals.jsonreferences/pitch-guide.mdpr-channel-types.mdtier-scoring.mdscripts/research.py詳細
競合他社のPR先を見つける
自社製品のURLを入力してください。競合他社を見つけ、競合各社が利用したすべてのPRチャネル(ニュース、ポッドキャスト、コミュニティ)を調査します。複数の競合に共通するチャネル(実績のあるターゲット)を提示し、それぞれのジャーナリストまたはホストを見つけ、Tier 1の各チャネル向けに自社製品を紹介するパーソナライズされたコールドピッチを作成します。
ハルシネーションゼロの方針: 出力に含まれるすべてのチャネル、ジャーナリスト名、ストーリーの切り口、ピッチの詳細は、特定のTavily検索結果または取得した製品ページに基づいている必要があります。これは次の項目に適用されます。
- 競合他社名:AIの学習知識ではなく、Tavily検索結果に記載されている必要があります
- チャネル名:検索結果にURLが含まれている必要があります
- ジャーナリスト/ホスト名:Tavilyのスニペットに一字一句そのまま記載されている必要があります
- ストーリーの切り口:検索結果にある記事/エピソードのタイトルからのみ抽出します
- ピッチの下書き:検索データと製品分析の具体的な根拠を参照します
よくある間違い
| エージェントがやりがちなこと | なぜ間違いなのか |
|---|---|
| 学習知識からジャーナリストの名前を挙げる | ジャーナリスト名はすべて検索結果のスニペットに基づいている必要があります。「Sarah PerezはTechCrunchでスタートアップを担当している」と記憶を頼りに書くのはハルシネーションです。 |
| 根拠となるURLなしでチャネルを列挙する | 出力に含める各チャネルには、競合他社が取り上げられたことを示すPR検索結果のURLが少なくとも1つ必要です。 |
| 競合他社の確認手順を省略する | 発見した競合他社を必ず提示し、ユーザーの確認を待ってください。競合他社を間違えると、検索が無駄になり、出力も役に立たなくなります。 |
| 一般的なピッチを作成する(「ぜひ取り上げていただきたいです」など) | 各ピッチでは、根拠にある具体的な切り口と、製品分析で明らかになった具体的な差別化要因の両方に言及する必要があります。 |
| 競合1社にしか登場していないチャネルをTier 1にする | Tier 1は競合3社以上、Tier 2はちょうど2社、Tier 3は1社です。実績のないチャネルを格上げしないでください。 |
| 出力でエムダッシュを使う | すべてのエムダッシュ(--)をハイフンに置き換えてください。 |
実行のたびに参照ファイルを読む
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で取得してください -- 無料枠は月1,000クレジットです(1回あたり約23回の検索で、月約43回実行できます)。.envファイルに追加してください。」
FIRECRAWL_API_KEYだけがない場合: 続行します。URLの取得にはTavily extractを使用します。
ステップ2:入力の解析
会話から次の情報を収集します。
product_url:取得するURL(必須。ただし、ユーザーが説明を直接貼り付けた場合を除く)product_name:任意。指定されていない場合はページから取得しますgeography:任意 -- US/Europe/global。デフォルト:US
ユーザーが説明だけを貼り付けた場合(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(最上位カテゴリ:例、開発者ツール/フィンテック/ヘルステック/コンシューマー)、l2(セクター:例、DevOps/決済/遠隔医療)、l3(具体的なニッチ:例、CI/CD 自動化/組み込み型決済/非同期ビデオ診療)。「テクノロジー」のような曖昧なラベルだけでは不十分です。differentiators:一般的な競合製品と異なる、具体的な特徴をちょうど 2~3 個挙げてください。これらはピッチの草案に直接使用されるため、具体的に記述してください。例:["セルフホスト型パイプラインランナー -- データが自社のインフラから外部に出ない", "モノレポにネイティブ対応し、ステップを動的に生成"]icp:buyer_persona(役職)、company_type、company_sizegeography_bias:米国/欧州/グローバル/不明page_source:「live_page」または「user_description」
/tmp/cprf-product-analysis.json に書き込みます:
python3 << 'PYEOF'
import json
analysis = {
# FILL from your analysis above
"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(スニペットからの 1 文)、source_url(企業が見つかった検索結果の URL)を記述してください。
ステップ 5:競合の確認
見つかった競合をユーザーに提示します:
python3 << 'PYEOF'
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
# FILL: 5 competitors from the search results above
candidates = [
# {"name": str, "url": str, "description": str, "source_url": str}
]
print(f"\nFound 5 competitors for {analysis['product_name']} in {analysis['industry_taxonomy']['l3']}:\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
# FILL: confirmed competitor list (after user review)
confirmed = [
# {"name": str, "url": str}
]
json.dump({"confirmed_competitors": confirmed}, open('/tmp/cprf-competitors-confirmed.json', 'w'), indent=2)
print(f"Confirmed {len(confirmed)} competitors for PR research.")
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
競合ごとに 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'Competitors researched: {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 のエピソード)として特定します。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": [
# FILL -- channels appearing in 3+ competitors
# Each: {"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": [
# FILL -- channels appearing in exactly 2 competitors
# Each: {"channel_name": str, "channel_url": str, "channel_type": str,
# "frequency": 2, "found_in_competitors": [str], "evidence_urls": [str],
# "story_angles_used": [str]}
],
"tier_3_channels": [
# FILL -- channels appearing in only 1 competitor (name + URL only)
# Each: {"channel_name": str, "channel_url": str, "found_in_competitor": str}
],
"data_quality_flags": []
}
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
PYEOF
検証します:
python3 -c "
import json
p = json.load(open('/tmp/cprf-pr-patterns.json'))
print(f'Tier 1 channels: {len(p[\"tier_1_channels\"])}')
for ch in p['tier_1_channels']:
print(f' {ch[\"frequency\"]}x {ch[\"channel_name\"]} ({ch[\"channel_type\"]}) -- {ch[\"found_in_competitors\"]}')
print(f'Tier 2 channels: {len(p[\"tier_2_channels\"])}')
print(f'Tier 3 channels: {len(p[\"tier_3_channels\"])}')
"
Tier 1 チャンネルが 3 未満の場合: ニッチ市場ではよくあることです。頻度が最も高い Tier 2 チャンネルを昇格させ、詳細分析の対象となるチャンネルが合計 3 以上になるようにします。昇格したことを data_quality_flags に記録してください。
ステップ 8:ジャーナリスト/ホストの検索
各 Tier 1 チャンネル(最大 7 件)について、Tavily で対象を絞った検索を 1 回実行します:
python3 << 'PYEOF'
import json, os, urllib.request
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
l2 = analysis['industry_taxonomy']['l2']
l3 = analysis['industry_taxonomy']['l3']
tavily_key = os.environ.get('TAVILY_API_KEY', '')
lookup_results = []
for channel in patterns.get('tier_1_channels', [])[:7]:
name = channel['channel_name']
ctype = channel['channel_type']
if ctype == 'editorial':
query = f'"{name}" journalist reporter writer covers {l2} {l3} startups technology'
elif ctype == 'podcast':
query = f'"{name}" podcast host interviewer {l2} {l3} founders'
else:
query = f'"{name}" moderator community manager {l2} {l3}'
payload = json.dumps({
"api_key": tavily_key,
"query": query,
"search_depth": "basic",
"max_results": 5
}).encode()
req = urllib.request.Request(
'https://api.tavily.com/search',
data=payload,
headers={'Content-Type': 'application/json'},
method='POST'
)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
data = json.loads(resp.read())
lookup_results.append({
'channel': name,
'channel_type': ctype,
'query': query,
'answer': data.get('answer', ''),
'results': [
{'title': r['title'], 'url': r['url'], 'content': r.get('content', '')[:400]}
for r in data.get('results', [])[:3]
]
})
print(f'Journalist lookup -- {name}: {len(data.get("results", []))} results')
except Exception as e:
lookup_results.append({
'channel': name, 'channel_type': ctype,
'query': query, 'answer': '', 'results': [], 'error': str(e)
})
print(f'Journalist lookup -- {name}: FAILED ({e})')
json.dump(lookup_results, open('/tmp/cprf-journalist-results.json', 'w'), indent=2)
print(f'Journalist lookups complete: {len(lookup_results)} channels')
PYEOF
AI による抽出用に結果を表示します:
python3 -c "
import json
results = json.load(open('/tmp/cprf-journalist-results.json'))
for r in results:
print(f'\n=== {r[\"channel\"]} ({r[\"channel_type\"]}) ===')
print(f'Answer: {r.get(\"answer\",\"\")[:400]}')
for item in r.get('results', []):
print(f' {item[\"title\"]}')
print(f' {item.get(\"content\",\"\")[:300]}')
"
AI への指示: 各 Tier 1 チャンネルについて、上記の検索結果から次の情報を抽出してください:
journalist_name:スニペットに記載された人物名をそのまま記入します。見つからない場合は「検索データに見つかりません」と記入してください。学習データから補完してはいけません。journalist_beat:スニペットのテキストから、その人物が扱うトピックを抽出します。見つからない場合は「検索データに見つかりません」と記入してください。
/tmp/cprf-pr-patterns.json を更新し、各 Tier 1 チャンネルの journalist_name と journalist_beat を入力します:
python3 << 'PYEOF'
import json
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
# FILL: update journalist_name and journalist_beat for each tier_1 channel
# journalist_name and journalist_beat come from search snippet text only
# Write "not found in search data" if the snippets don't name a person
# Example:
# patterns['tier_1_channels'][0]['journalist_name'] = 'Ingrid Lunden'
# patterns['tier_1_channels'][0]['journalist_beat'] = 'enterprise software and developer tools'
json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Journalist data updated.')
for ch in patterns['tier_1_channels']:
print(f" {ch['channel_name']}: {ch.get('journalist_name','--')} | {ch.get('journalist_beat','--')}")
PYEOF
ステップ 9:統合 -- アプローチ用パッケージを生成する(AI)
統合データを出力します:
python3 -c "
import json
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
print('=== PRODUCT ===')
print(f'Name: {analysis[\"product_name\"]}')
print(f'What it does: {analysis[\"one_line_description\"]}')
print(f'Differentiators:')
for d in analysis['differentiators']:
print(f' - {d}')
print(f'ICP: {analysis[\"icp\"]}')
print(f'Geography: {analysis[\"geography_bias\"]}')
print()
print('=== TIER 1 CHANNELS ===')
for ch in patterns['tier_1_channels']:
print(f'\n{ch[\"channel_name\"]} ({ch[\"channel_type\"]}, freq={ch[\"frequency\"]})')
print(f' Found in: {ch[\"found_in_competitors\"]}')
print(f' Evidence URLs: {ch[\"evidence_urls\"][:3]}')
print(f' Story angles: {ch[\"story_angles_used\"]}')
print(f' Journalist: {ch.get(\"journalist_name\",\"not found\")} | {ch.get(\"journalist_beat\",\"\")}')
print()
print('=== TIER 2 CHANNELS ===')
for ch in patterns['tier_2_channels']:
print(f' {ch[\"channel_name\"]} ({ch[\"channel_type\"]}) -- found in {ch[\"found_in_competitors\"]}')
"
AI の指示 -- ハルシネーションをゼロにするルール:
- チャネル名:
/tmp/cprf-pr-patterns.jsonにあるチャネルのみを含めます。チャネルをでっち上げてはいけません。 - ジャーナリスト/ホスト名: ステップ 8 で入力された情報のみを使用します。空欄の場合は「検索データ内に見つかりません」と記載します。学習データの知識で補ってはいけません。
- ストーリーの切り口: 検索結果に含まれる記事/エピソードのタイトルから抽出した切り口のみを使用します。学習データの知識から推測してはいけません。
- コールドピッチの草案: (a)根拠にある具体的なストーリーの切り口、(b)製品分析から得た具体的な差別化要素を少なくとも 1 つ、(c)見つかった場合はジャーナリストの担当分野に言及する必要があります。「ぜひ取り上げていただきたいです」や「当社の製品は革新的です」といった一般的な表現は使ってはいけません。
- チャネルの概要: 検索スニペットのみを使い、1~2 文で記述します。スニペットにチャネルの取材対象が記載されていない場合は、「検索データ内に見つかりません」と記載します。
- 追加の切り口: 競合他社が取り上げていない切り口を 3 つ挙げます。一般的なアドバイスではなく、製品の実際の差別化要素に基づいている必要があります。
- em ダッシュは使用しません。禁止語(powerful、seamless、game-changing、revolutionary、cutting-edge、leverage、transform)は使用しません。
Tier 1 の各チャネルについて生成する項目:
channel_overview:取材対象に関する概要を 1~2 文で記載します(スニペットに基づく)why_they_covered_competitors:根拠となるタイトルから抽出した具体的な切り口journalist_name+journalist_beatapproach_method:コールドメール/ポッドキャストのピッチフォーム/コミュニティ投稿/LinkedIn DM(チャネルの種類に基づく)cold_pitch_draft:subject:「[ジャーナリスト名]:[担当分野]+[具体的な切り口]」body:3~4 文。構成:冒頭(競合他社に関する過去の報道に言及)+提供内容(1 文)+担当分野に合う理由(具体的な差別化要素に結び付ける)+依頼(明確で負担の少ない CTA)
bonus_hooks も生成します:検索結果にある競合他社が使用していないピッチの切り口を 3 つ挙げます。それぞれ、具体的な製品の差別化要素に基づいてください。
/tmp/cprf-final.json に書き込みます:
python3 << 'PYEOF'
import json
result = {
"product_summary": {
# FILL from analysis
},
"competitors_researched": [], # FILL: names of confirmed competitors
"tier_1_deep_dives": [
# FILL per tier 1 channel:
# {
# "channel_name": str,
# "channel_type": str, # editorial / podcast / community
# "frequency": int,
# "found_in_competitors": [str],
# "evidence_urls": [str],
# "channel_overview": str,
# "why_they_covered_competitors": str,
# "story_angles_used": [str],
# "journalist_name": str,
# "journalist_beat": str,
# "approach_method": str,
# "cold_pitch_draft": {"subject": str, "body": str}
# }
],
"tier_2_channels": [
# FILL: {channel_name, channel_type, frequency, found_in_competitors, evidence_urls}
],
"tier_3_channels": [
# FILL: {channel_name, found_in_competitor}
],
"bonus_hooks": [
# FILL: 3 strings -- pitch angles not used by competitors
],
"data_quality_flags": []
}
json.dump(result, open('/tmp/cprf-final.json', 'w'), indent=2)
print(f'Synthesis written.')
print(f'Tier 1 deep dives: {len(result.get("tier_1_deep_dives", []))}')
print(f'Bonus hooks: {len(result.get("bonus_hooks", []))}')
PYEOF
ステップ 10:セルフ QA、提示、保存
セルフ QA:
python3 << 'PYEOF'
import json
result = json.load(open('/tmp/cprf-final.json'))
failures = []
# Check 1: em dashes
full_text = json.dumps(result)
if '—' in full_text:
result = json.loads(full_text.replace('—', '-'))
failures.append('Fixed: em dashes replaced with hyphens')
# Check 2: banned words
banned = ['powerful', 'seamless', 'innovative', 'game-changing', 'revolutionize',
'excited to announce', 'cutting-edge', 'best-in-class', 'world-class',
'leverage', 'transform', 'disrupt']
for word in banned:
if word.lower() in json.dumps(result).lower():
failures.append(f'Warning: banned word "{word}" found in output -- review before presenting')
# Check 3: cold pitch subjects exist
for dd in result.get('tier_1_deep_dives', []):
pitch = dd.get('cold_pitch_draft', {})
if not pitch.get('subject') or len(pitch.get('subject', '')) < 10:
dd['cold_pitch_draft']['subject'] = 'not generated'
failures.append(f'Fixed: missing subject line for {dd.get("channel_name")}')
if not pitch.get('body') or len(pitch.get('body', '')) < 50:
failures.append(f'Warning: very short pitch body for {dd.get("channel_name")}')
# Check 4: bonus hooks count
if len(result.get('bonus_hooks', [])) != 3:
failures.append(f'Expected 3 bonus hooks, got {len(result.get("bonus_hooks", []))}')
# Check 5: "not found in search data" count
nf_count = json.dumps(result).count('not found in search data')
if nf_count > 0:
failures.append(f'INFO: {nf_count} field(s) marked "not found in search data" -- verify before outreach')
# Check 6: tier 1 channels have evidence URLs
for ch in result.get('tier_1_deep_dives', []):
if not ch.get('evidence_urls'):
failures.append(f'Warning: {ch["channel_name"]} has no evidence_urls')
if 'data_quality_flags' not in result:
result['data_quality_flags'] = []
result['data_quality_flags'].extend(failures)
json.dump(result, open('/tmp/cprf-final.json', 'w'), indent=2)
print(f'QA complete. {len(failures)} issues addressed.')
for f in failures:
print(f' - {f}')
if not failures:
print('All QA checks passed.')
PYEOF
出力を提示します:
## PR Intel: [product_name]
Date: [today] | Competitors researched: [N] | Tier 1 channels: [N] | Tier 2 channels: [N]
---
### Your Product
[one_line_description]
Differentiators: [list]
Competitors researched: [names]
---
### Tier 1 Channels (Proven Beats -- Found in 3+ Competitors)
*These channels have already covered multiple companies in your space.*
| Channel | Type | Found in | Journalist/Host | Approach |
|---|---|---|---|---|
[one row per tier 1 channel]
---
### Deep Dives + Cold Pitches
#### 1. [Channel Name] (Tier 1 -- [Type], found in [N] competitors)
Covers: [channel_overview]
Covered competitors: [found_in_competitors with evidence URLs]
Story angle they used: [why_they_covered_competitors]
Journalist/Host: [journalist_name] | Beat: [journalist_beat]
How to reach: [approach_method]
**Cold pitch:**
Subject: [subject]
[body -- 3-4 sentences]
---
[repeat for each tier 1 channel]
---
### Tier 2 Channels (Warm -- Found in 2 Competitors)
| Channel | Type | Found in | URL |
|---|---|---|---|
[one row per tier 2 channel]
---
### Tier 3 Channels (Discovery -- Found in 1 Competitor)
[comma-separated list of channel names with URLs]
---
### 3 Bonus Hooks (Angles Your Competitors Didn't Use)
1. [hook_text]
2. [hook_text]
3. [hook_text]
---
Data notes: [data_quality_flags, or "None"]
Saved to: docs/pr-intel/[PRODUCT_SLUG]-[DATE].md
ファイルに保存して後片付けをします:
DATE=$(date +%Y-%m-%d)
OUTPUT_FILE="docs/pr-intel/${PRODUCT_SLUG}-${DATE}.md"
mkdir -p docs/pr-intel
echo "Saved to: $OUTPUT_FILE"
rm -f /tmp/cprf-product-raw.md /tmp/cprf-product-analysis.json \
/tmp/cprf-competitors-raw.json /tmp/cprf-competitors-confirmed.json \
/tmp/cprf-pr-raw.json /tmp/cprf-pr-patterns.json \
/tmp/cprf-journalist-results.json /tmp/cprf-final.json
echo "Temp files cleaned up."