Communitygithub.com

경쟁사 PR 발굴기

제품 URL이나 설명을 입력하면 상위 5개 경쟁사를 찾고, 모든 경쟁사에 대해 3개 트랙(미디어, 팟캐스트, 커뮤니티)의 PR 조사를 수행합니다. 가장 많이 등장하는 채널을 식별하고, 각 채널의 기자나 진행자를 찾아 제품에 맞춤화된 계층별 아웃리치 목록, 스토리 앵글, 콜드 피치 초안을 반환합니다. PR 기회를 찾거나, 경쟁사가 소개된 곳을 알아보거나, 미디어 아웃리치 목록을 작성하거나, 내 분야를 담당하는 기자를 찾거나, 언론 보도를 위한 피치 템플릿이 필요할 때 사용하세요.

경쟁사 PR 발굴기란 무엇인가요?

이 스킬은 AI 에이전트가 제품 URL이나 설명만으로 경쟁사 PR 조사 전 과정을 자동화할 수 있게 해줍니다. 제품 페이지를 분석한 후, Tavily 검색 엔진을 사용해 관련 상위 5개 경쟁사를 발견하고 사용자 확인을 거칩니다. 이후 각 경쟁사에 대해 미디어/팟캐스트/커뮤니티의 세 가지 트랙으로 PR 취재 정보를 수집하고, 여러 경쟁사에 공통으로 등장하는 검증된 채널을 Tier 1~3으로 분류합니다. 최종적으로 각 Tier 1 채널의 기자나 호스트를 찾아 제품의 차별점을 반영한 개인화된 콜드 피치 초안을 생성하며, 모든 결과는 검색 결과와 제품 페이지에서 추출된 증거에 기반해야 한다는 '환각 금지(zero-hallucination)' 정책을 엄격히 준수합니다. Claude Code, Cursor, Codex 등 Tavily API와 쉘 스크립트 실행이 가능한 모든 AI 에이전트 플랫폼에서 활용할 수 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/Varnan-Tech/opendirectory/tree/main/skills/competitor-pr-finder

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

경쟁사 PR 발굴기 (Competitor PR Finder)

제품 URL을 입력하세요. 경쟁사를 찾고, 모든 경쟁사가 사용한 PR 채널(뉴스, 팟캐스트, 커뮤니티)을 조사하여 여러 경쟁사에 걸쳐 나타나는 채널(검증된 타겟)을 발굴하고, 각 채널의 기자나 진행자를 찾아 제품에 맞춤화된 콜드 피치를 Tier 1 채널별로 작성합니다.


환각 금지 정책: 출력의 모든 채널, 기자 이름, 스토리 앵글, 피치 세부 정보는 특정 Tavily 검색 결과 또는 가져온 제품 페이지에서 추적 가능해야 합니다. 이는 다음에 적용됩니다:

  • 경쟁사 이름: AI 훈련 지식이 아닌 Tavily 검색 결과에 나타난 이름이어야 함
  • 채널 이름: 검색 결과에 URL이 있어야 함
  • 기자/진행자 이름: Tavily 스니펫에 그대로 나타나야 함
  • 스토리 앵글: 검색 결과의 기사/에피소드 제목에서만 추출
  • 피치 초안: 검색 데이터 + 제품 분석의 구체적인 증거를 참조해야 함

일반적인 실수

에이전트가 하려는 행동왜 틀렸는가
훈련 지식에서 기자 이름을 말하기모든 기자 이름은 검색 결과 스니펫에서 추적되어야 합니다. 기억에서 "Sarah Perez가 TechCrunch에서 스타트업을 담당합니다"라고 쓰는 것은 환각입니다.
증거 URL 없이 채널 나열하기출력의 모든 채널은 경쟁사가 해당 채널에 소개되었음을 증명하는 PR 검색 결과의 URL이 하나 이상 있어야 합니다.
경쟁사 확인 단계 건너뛰기발견된 경쟁사를 항상 표시하고 사용자 확인을 기다리세요. 잘못된 경쟁사는 검색 낭비와 쓸모없는 출력을 초래합니다.
일반적인 피치 생성("저희를 소개해 주세요")모든 피치는 증거의 특정 앵글과 제품 분석의 특정 차별점을 모두 참조해야 합니다.
경쟁사 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크레딧(약 23회 검색/실행 기준으로 약 43회 전체 실행 가능). .env 파일에 추가하세요."

FIRECRAWL_API_KEY만 없는 경우: 계속 진행합니다. URL 가져오기에는 Tavily 추출이 사용됩니다.


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 추출 (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). "기술"과 같은 모호한 라벨만으로는 허용되지 않습니다.
  • differentiators: 이 제품을 일반 경쟁사와 구별하는 정확히 2-3가지 구체적인 사항. 이는 피치 초안에 직접 반영됩니다. 구체적으로 작성하세요. 예시: ["자체 호스팅 파이프라인 러너 -- 데이터가 인프라를 떠나지 않음", "동적 단계 생성을 통한 모노레포 기본 지원"]
  • icp: buyer_persona (직책), company_type, company_size
  • geography_bias: US / Europe / global / unclear
  • page_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개의 경쟁사 회사를 선택하십시오:

  1. 검색 결과 제목, 답변 또는 스니펫에 이름이 언급되어야 함
  2. 분석 중인 제품과 동일한 L3 틈새에 있어야 함
  3. 실제 경쟁 제품(에이전시, 컨설팅 회사, 목록 기사가 아님)이어야 함
  4. 서로 구별되어야 함(다른 이름의 동일 회사가 아님)

각 경쟁사에 대해 작성: name, 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"\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

# 사용자 검토 후 확인된 경쟁사 목록 채우기
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 (미디어): "[competiser]" featured press coverage TechCrunch Forbes Wired article interview
  • 트랙 B (팟캐스트): "[competiser]" founder CEO podcast interview appeared on episode
  • 트랙 C (커뮤니티): "[competiser]" 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인 경우: 이 경쟁사는 PR 취재가 매우 적습니다. 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": [
        # 채우기 -- 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,
        #        "frequency": int, "found_in_competitors": [str],
        #        "evidence_urls": [str], "story_angles_used": [str]}
    ],
    "tier_3_channels": [
        # 채우기 -- 1개 경쟁사에 나타난 채널
        # 각 항목: {"channel_name": str, "channel_url": str, "channel_type": str,
        #        "frequency": int, "found_in_competitors": [str],
        #        "evidence_urls": [str]}
    ],
    "cross_pattern_insights": {
        "dominant_channel_type": "",
        "dominant_story_angle": "",
        "niche_uniqueness": ""
    },
    "data_quality_flags": []
}

json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('PR patterns written.')
PYEOF

8단계: 기자/진행자 찾기 (3단계)

Tier 1 채널의 경우 저널리스트/호스트 이름과 활동 분야를 찾으려면 추가 검색이 필요합니다.

# Tier 1 채널에 대해서만 실행
for channel_name in "TechCrunch" "The Verge"; do
  echo "--- Looking up: $channel_name ---"
  python3 scripts/research.py \
    --phase lookup-journalist \
    --channel "$channel_name" \
    --niche "CATEGORY_L3" \
    --tavily-key "$TAVILY_API_KEY" \
    --output "/tmp/cprf-lookup-$(echo $channel_name | tr '[:upper:]' '[:lower:]' | tr ' ' '-').json"
done

조회 결과 출력 및 검토:

ls /tmp/cprf-lookup-*.json
python3 -c "
import json, glob
for f in sorted(glob.glob('/tmp/cprf-lookup-*.json')):
    data = json.load(open(f))
    channel = f.split('-',2)[-1].replace('.json','')
    print(f'\n=== {channel} ===')
    for r in data.get('results', [])[:4]:
        print(f'  Title: {r[\"title\"]}')
        print(f'  Snippet: {r.get(\"content\",\"\")[:200]}')
        print(f'  URL: {r[\"url\"]}')
"

AI 지침: 위 조회 결과를 읽습니다. 각 Tier 1 채널에 대해 journalist_name과 journalist_beat를 추출합니다. 베스트는: 결과 중 하나의 제목이나 스니펫에 나타난 정확한 이름. 두 번째 베스트는: 스니펫의 "기고자:" 라인. 저널리스트가 특정 틈새(예: "DevOps at The New Stack")를 다룬다는 증거가 스니펫에 없으면 journalist_beat를 "general tech"로 설정합니다. 이름이 전혀 없는 경우 빈 문자열로 남겨둡니다.

추출된 이름으로 /tmp/cprf-pr-patterns.json 업데이트:

python3 << 'PYEOF'
import json

patterns = json.load(open('/tmp/cprf-pr-patterns.json'))

for channel in patterns['tier_1_channels']:
    # 조회 결과에서 채운 journalist_name 및 journalist_beat
    channel['journalist_name'] = ""
    channel['journalist_beat'] = ""

json.dump(patterns, open('/tmp/cprf-pr-patterns.json', 'w'), indent=2)
print('Journalists added to patterns.')
PYEOF

9단계: 피치 작성 (AI)

references/pitch-guide.md의 지침에 따라 각 Tier 1 채널에 대한 개인화된 콜드 이메일을 작성합니다. 각 피치는 다음을 포함해야 합니다:

  • 스니펫에 언급된 해당 채널에 소개된 경쟁사의 구체적인 출처
  • 동일한 출처에서 추출한 스토리 앵글(예: "product launch for X")
  • /tmp/cprf-product-analysis.json의 differentiators 중 하나
  • 특정 워딩과 함께 제품을 해당 채널에 맞는 스토리로 포지셔닝하는 방법

출력을 /tmp/cprf-pitches.json에 저장:

python3 << 'PYEOF'
import json

patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))

pitches = []

for channel in patterns['tier_1_channels']:
    # 사용자 정의 피치 작성
    pitch = {
        "channel": channel['channel_name'],
        "channel_url": channel['channel_url'],
        "journalist_name": channel.get('journalist_name', 'Editor'),
        "journalist_beat": channel.get('journalist_beat', 'General tech'),
        "story_angle": channel['story_angles_used'][0] if channel.get('story_angles_used') else '',
        "source_competitor": channel['found_in_competitors'][0],
        "evidence_url": channel['evidence_urls'][0],
        "pitch_draft": f"""Hi {{{{Journalist Name}}}},

I saw you covered {{{{Source Competitor}}}}'s {{story_angle}} in {{{{Channel Name}}}}: {{{{evidence_url}}}}

{{product_name}} is doing something similar but with a key difference: {{{{differentiator}}}}.

Would you be interested in {{product_name}}? We believe it would make a great story for your readers.

Best,
{{Your Name}}"""
    }
    pitches.append(pitch)

json.dump({"pitches": pitches}, open('/tmp/cprf-pitches.json', 'w'), indent=2, ensure_ascii=False)
print(f'{len(pitches)} pitches written to /tmp/cprf-pitches.json')
PYEOF

10단계: 최종 출력 생성

모든 데이터를 사람이 읽을 수 있는 형식으로 조합합니다:

python3 << 'PYEOF'
import json

patterns = json.load(open('/tmp/cprf-pr-patterns.json'))
analysis = json.load(open('/tmp/cprf-product-analysis.json'))
pitches = json.load(open('/tmp/cprf-pitches.json'))

output = f"""# 경쟁사 PR 분석 보고서: {analysis['product_name']}

## 개요
- **제품:** {analysis['product_name']}
- **설명:** {analysis['one_line_description']}
- **산업:** {analysis['industry_taxonomy']['l1']} > {analysis['industry_taxonomy']['l2']} > {analysis['industry_taxonomy']['l3']}
- **차별점:**
"""
for d in analysis['differentiators']:
    output += f"  - {d}\n"

output += f"\n## Tier 1 채널 (우선 순위 타겟)\n\n"
for c in patterns['tier_1_channels']:
    output += f"### {c['channel_name']}\n"
    output += f"- **유형:** {c['channel_type']}\n"
    output += f"- **URL:** {c['channel_url']}\n"
    output += f"- **발견된 경쟁사:** {', '.join(c['found_in_competitors'])}\n"
    output += f"- **기자/호스트:** {c.get('journalist_name', '알 수 없음')} ({c.get('journalist_beat', '')})\n"
    output += f"- **사용된 스토리 앵글:** {', '.join(c.get('story_angles_used', []))}\n"
    output += f"- **증거 URL:**\n"
    for url in c['evidence_urls'][:3]:
        output += f"  - {url}\n"
    output += "\n"

output += f"## Tier 2 채널 (2차 타겟)\n\n"
for c in patterns['tier_2_channels']:
    output += f"- {c['channel_name']} ({', '.join(c['found_in_competitors'])})\n"

output += f"\n## Tier 3 채널 (1회성 타겟)\n\n"
for c in patterns['tier_3_channels']:
    output += f"- {c['channel_name']}\n"

output += f"\n## 교차 패턴 인사이트\n"
output += f"- **지배적인 채널 유형:** {patterns['cross_pattern_insights']['dominant_channel_type']}\n"
output += f"- **지배적인 스토리 앵글:** {patterns['cross_pattern_insights']['dominant_story_angle']}\n"
output += f"- **틈새 고유성:** {patterns['cross_pattern_insights']['niche_uniqueness']}\n"

if patterns.get('data_quality_flags'):
    output += f"\n## 데이터 품질 플래그\n"
    for flag in patterns['data_quality_flags']:
        output += f"- ⚠️ {flag}\n"

output += "\n" + "="*50 + "\n"

print(output)
PYEOF

스크립트: research.py

/tmp/cprf-script.py에서 scripts/research.py를 찾거나 재생성합니다:

#!/usr/bin/env python3
"""경쟁사 PR 발굴기 (CPRF) - research.py

용법:
  python3 research.py --phase discover --product-analysis <json> --tavily-key <key> --output <json>
  python3 research.py --phase pr-research --competitors <json> --product-analysis <json> --tavily-key <key> --output <json>
  python3 research.py --phase lookup-journalist --channel <str> --niche <str> --tavily-key <key> --output <json>
"""
import argparse
import json
import os
import sys
from urllib.parse import urlparse, quote

import requests

TAVILY_API_URL = "https://api.tavily.com/search"


def search_tavily(api_key: str, query: str, max_results: int = 10) -> dict:
    """Tavily 검색 API 호출"""
    payload = {
        "api_key": api_key,
        "query": query,
        "search_depth": "advanced",
        "include_answer": True,
        "max_results": max_results
    }
    resp = requests.post(TAVILY_API_URL, json=payload)
    resp.raise_for_status()
    return resp.json()


def phase_discover(product_analysis_path: str, tavily_key: str, output_path: str):
    """경쟁사 발견 단계: 제품 분석을 기반으로 검색을 실행하여 잠재적 경쟁사를 식별합니다."""
    with open(product_analysis_path) as f:
        analysis = json.load(f)
    
    l3 = analysis['industry_taxonomy']['l3']
    description = analysis['one_line_description']
    
    queries = [
        f"top competitors in {l3} {description}",
        f"alternatives to {analysis['product_name']} {l3}",
        f"best {l3} solutions {description}",
        f"{l3} market leaders competitors",
        f"who competes with {analysis['product_name']} in {l3}"
    ]
    
    results = []
    for q in queries:
        try:
            data = search_tavily(tavily_key, q, max_results=10)
            results.append({
                "query": q,
                "answer": data.get("answer", ""),
                "results": [
                    {"title": r["title"], "url": r["url"], "content": r["content"]}
                    for r in data.get("results", [])
                ]
            })
        except Exception as e:
            results.append({"query": q, "error": str(e)})
    
    output = {
        "product_name": analysis['product_name'],
        "competitor_searches": results,
        "competitor_candidates": []
    }
    with open(output_path, 'w') as f:
        json.dump(output, f, indent=2)
    print(json.dumps({"message": f"Discovered {len(results)} searches written."}), file=sys.stderr)


def phase_pr_research(competitors_path: str, product_analysis_path: str, tavily_key: str, output_path: str):
    """PR 조사 단계: 각 경쟁사에 대해 3-트랙 검색을 실행합니다."""
    with open(competitors_path) as f:
        comp_data = json.load(f)
    with open(product_analysis_path) as f:
        analysis = json.load(f)
    
    competitors = comp_data["confirmed_competitors"]
    l3 = analysis['industry_taxonomy']['l3']
    results = []
    
    for comp in competitors:
        name = comp["name"]
        tracks = {}
        
        # 트랙 A: 미디어
        query_a = f'"{name}" featured press coverage TechCrunch Forbes Wired article interview'
        try:
            data_a = search_tavily(tavily_key, query_a, max_results=10)
            tracks["editorial"] = {
                "query": query_a,
                "answer": data_a.get("answer", ""),
                "results": [
                    {"title": r["title"], "url": r["url"], "content": r["content"]}
                    for r in data_a.get("results", [])
                ]
            }
        except Exception as e:
            tracks["editorial"] = {"query": query_a, "error": str(e), "results": []}
        
        # 트랙 B: 팟캐스트
        query_b = f'"{name}" founder CEO podcast interview appeared on episode'
        try:
            data_b = search_tavily(tavily_key, query_b, max_results=10)
            tracks["podcast"] = {
                "query": query_b,
                "answer": data_b.get("answer", ""),
                "results": [
                    {"title": r["title"], "url": r["url"], "content": r["content"]}
                    for r in data_b.get("results", [])
                ]
            }
        except Exception as e:
            tracks["podcast"] = {"query": query_b, "error": str(e), "results": []}
        
        # 트랙 C: 커뮤니티
        query_c = f'"{name}" site:reddit.com OR site:news.ycombinator.com OR site:producthunt.com'
        try:
            data_c = search_tavily(tavily_key, query_c, max_results=10)
            tracks["community"] = {
                "query": query_c,
                "answer": data_c.get("answer", ""),
                "results": [
                    {"title": r["title"], "url": r["url"], "content": r["content"]}
                    for r in data_c.get("results", [])
                ]
            }
        except Exception as e:
            tracks["community"] = {"query": query_c, "error": str(e), "results": []}
        
        results.append({"competitor": name, "tracks": tracks})
    
    output = {
        "competitors_researched": len(competitors),
        "results": results
    }
    with open(output_path, 'w') as f:
        json.dump(output, f, indent=2)
    print(json.dumps({"message": f"PR research completed for {len(competitors)} competitors."}), file=sys.stderr)


def phase_lookup_journalist(channel: str, niche: str, tavily_key: str, output_path: str):
    """기자 조회 단계: 특정 채널의 기자/호스트를 찾습니다."""
    queries = [
        f"{channel} journalist who covers {niche}",
        f"{channel} reporter {niche}",
        f"{channel} editor {niche}",
        f"{channel} author {niche}"
    ]
    results = []
    for q in queries:
        try:
            data = search_tavily(tavily_key, q, max_results=5)
            results.append({
                "query": q,
                "results": [
                    {"title": r["title"], "url": r["url"], "content": r["content"]}
                    for r in data.get("results", [])
                ]
            })
        except Exception as e:
            results.append({"query": q, "error": str(e)})
    
    with open(output_path, 'w') as f:
        json.dump({"channel": channel, "results": results}, f, indent=2)
    print(json.dumps({"message": f"Journalist lookup completed for {channel}."}), file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(description="CPRF Research Script")
    parser.add_argument("--phase", required=True, choices=["discover", "pr-research", "lookup-journalist"])
    parser.add_argument("--product-analysis")
    parser.add_argument("--competitors")
    parser.add_argument("--channel")
    parser.add_argument("--niche")
    parser.add_argument("--tavily-key", required=True)
    parser.add_argument("--output", required=True)
    
    args = parser.parse_args()
    
    if args.phase == "discover":
        if not args.product_analysis:
            print(json.dumps({"error": "--product-analysis required for discover phase"}), file=sys.stderr)
            sys.exit(1)
        phase_discover(args.product_analysis, args.tavily_key, args.output)
    elif args.phase == "pr-research":
        if not args.competitors or not args.product_analysis:
            print(json.dumps({"error": "--competitors and --product-analysis required for pr-research phase"}), file=sys.stderr)
            sys.exit(1)
        phase_pr_research(args.competitors, args.product_analysis, args.tavily_key, args.output)
    elif args.phase == "lookup-journalist":
        if not args.channel or not args.niche:
            print(json.dumps({"error": "--channel and --niche required for lookup-journalist phase"}), file=sys.stderr)
            sys.exit(1)
        phase_lookup_journalist(args.channel, args.niche, args.tavily_key, args.output)


if __name__ == "__main__":
    main()

참조 파일

pr-channel-types.md

# PR 채널 유형

PR 채널은 일반적으로 세 가지 주요 유형으로 분류할 수 있습니다:
1. **미디어 (Editorial):** TechCrunch, Forbes, Wired, VentureBeat, The Verge 등 기술/비즈니스 뉴스 매체. 기사 형태의 보도.
2. **팟캐스트 (Podcast):** The Joe Rogan Experience, Lex Fridman Podcast, Software Engineering Daily 등. 인터뷰 또는 패널 토론 형식.
3. **커뮤니티 (Community):** Reddit (r/programming, r/devops), Hacker News, Product Hunt, Dev.to 등. 커뮤니티 중심의 발견 및 논의.

pitch-guide.md

# 피치 가이드

좋은 콜드 피치는 다음을 포함해야 합니다:
1. **맞춤형 인사말:** 기자/호스트의 이름을 사용합니다.
2. **관련성 증명:** 경쟁사가 그들의 채널에 소개된 기사/에피소드를 언급합니다.
3. **스토리 앵글:** 경쟁사가 어떤 각도로 소개되었는지 설명합니다.
4. **차별점:** 경쟁사와 달리 당신의 제품이 어떻게 다른지 명확히 합니다.
5. **명확한 CTA:** 기사, 인터뷰, 또는 데모를 요청합니다.

금지 사항:
- "우리는 당신의 채널을 좋아합니다"와 같은 일반적인 칭찬
- 요청 없이 긴 제품 기능 목록
- 증거 없이 "획기적인" 또는 "게임 체인저"와 같은 과장된 언어
- 모든 채널에 동일한 피치를 복사하여 붙여넣기

tier-scoring.md

# 계층 점수

채널은 여러 경쟁사에 걸친 출현 빈도에 따라 계층화됩니다:
- **Tier 1 (최우선):** 3개 이상의 경쟁사에 나타난 채널. 입증된 타겟.
- **Tier 2 (중간):** 정확히 2개의 경쟁사에 나타난 채널. 잠재적 타겟.
- **Tier 3 (낮음):** 1개의 경쟁사에만 나타난 채널. 탐색적 타겟.

계층이 높을수록 해당 채널이 이 틈새의 보도를 수용할 가능성이 높습니다.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

Varnan-Tech/opendirectory

Use when the user asks to generate a blog cover image, thumbnail, or article header. Automatically uses modern typography, brand logos, and Google Search grounding to create beautiful 16:9 images with Gemini 3.1 Flash Image Preview.

Varnan-Tech/opendirectory

World-class brand strategist and naming expert. Uses an interrogation-led discovery phase to extract your brand's DNA, then applies scientific naming frameworks (Phonosemantics) and automated multi-TLD domain checking.

Varnan-Tech/opendirectory

Use when the user asks to generate or update a project's CLAUDE or AGENTS context file from a codebase scan. Writes a focused file under 100 lines containing only the non-obvious build commands, conventions, and gotchas Claude Code needs.

Varnan-Tech/opendirectory

Use when the user wants to verify cold emails, enrich a lead list, or autonomously guess email addresses from a CSV using ValidEmail.co or the open-source Reacher engine.

Varnan-Tech/opendirectory

Competitive intelligence orchestrator tracking companies across 8+ platforms (GitHub, Twitter, Reddit, HN, PH, YC Jobs) with heat scores and AI briefings.

Varnan-Tech/opendirectory

Generate high-converting, deep-dive growth case studies in MDX format. Use this skill when asked to write a case study or blog post about a company's growth, tech stack, or product-led strategy. It handles the full pipeline (researching the company via Tavily, generating a 16:9 cover image, quality checking the draft, uploading assets to cloud storage, and pushing directly to the target repository).

Varnan-Tech/opendirectory

Scans your project for outdated npm, pip, Cargo, Go, or Ruby packages. Runs a CVE security audit. Fetches changelogs, summarizes breaking changes with Gemini, and opens one PR per risk group (patch, minor, major). Includes Diagnosis Mode for install conflicts. Use when asked to update dependencies, check for outdated packages, open dependency PRs, scan for package updates, audit for CVEs, or flag breaking changes in upgrades. Trigger when a user says "check for outdated packages", "update my dependencies", "open PRs for dependency updates", "scan for CVEs", or "which packages need upgrading".

Varnan-Tech/opendirectory

Generates and updates README.md and API reference docs by reading your codebase's functions, routes, types, schemas, and architecture. Uses graphify to build a knowledge graph first, then writes accurate docs from it. Use when asked to write docs, generate a README, document an API, update stale docs, create an API reference from code, add an architecture section, or document a project in any language. Trigger when a user says their docs are missing, outdated, or wants to document their codebase without writing it manually.

Varnan-Tech/opendirectory

Evaluates expired domain candidates against a target niche, scores them by topical relevance, historical activity level, and history cleanliness, then outputs a ranked shortlist with explainable reasoning and risk flags.

Varnan-Tech/opendirectory

Brutally honest developer-experience audit for a GitHub repo. Scores 10 DX dimensions (time-to-first-success, README clarity, visual proof, install, quick-start, docs, examples, community, trust, marketing), writes a shareable roast in the requested tone (brutal/honest/kind), produces a prioritized action plan ranked by impact × effort, and sketches an ideal README. Trigger when user says "roast my repo", "audit my README", "dx audit", "developer experience review", "score my GitHub project", "before launch checklist", or "make my repo shareable".

Varnan-Tech/opendirectory

Drafts and designs a complete HTML email newsletter from a topic or content brief. Outputs paste-ready HTML for Loops, Mailchimp, Beehiiv, Resend, or any standard email platform. Includes subject line options and plain-text fallback. Trigger when a user says "write a newsletter", "draft an email newsletter", "create an HTML email", "design an email for my subscribers", or "write a newsletter about [topic]".

Varnan-Tech/opendirectory

Takes a GitHub PR URL or the current branch and writes a plain-English explanation of what it does and why, then posts it as a PR comment. Use when asked to explain a PR, summarize a pull request, write a plain-English description of a PR, add a summary comment to a PR, or understand what a PR changes. Trigger when a user says "explain this PR", "summarize this pull request", "what does this PR do", "add a comment explaining the PR", or shares a GitHub PR URL and asks what it does.

Varnan-Tech/opendirectory

Audit how often LLMs recommend your brand vs competitors and generate a GEO action plan.

Varnan-Tech/opendirectory

Takes a competitor's public GitHub repo URL, fetches their open issues via the GitHub REST API, filters noise locally, clusters issues into 6 demand categories, computes a demand score per issue and per cluster, and outputs a ranked demand gap report with a GTM messaging brief. Use when asked to scan a competitor's GitHub issues, find what their users are begging for, turn GitHub complaints into product positioning, identify competitor feature gaps, or generate messaging from real user demand. Trigger when a user says "scan competitor issues", "what are users asking for on X repo", "find demand gaps in Y", "turn GitHub issues into messaging", or "what should I build based on competitor complaints".

Varnan-Tech/opendirectory

Find recurring confusion in your repo's GitHub Discussions, rank it by urgency, and draft the actual docs fixes and content angles — with verbatim community quotes and source links as evidence.

Varnan-Tech/opendirectory

SEO keyword research workflow for blog generation using Google Trends data. Use when writing blog posts, planning content calendars, or optimizing articles for search engines. Finds breakout keywords, builds content structure, and generates SEO-optimized blog outlines targeting tech and developer audiences.

Varnan-Tech/opendirectory

Generates a professionally designed case study PDF for B2B SaaS sales and marketing. Supports 7 page layouts, 9 style presets, 1-4 page output. Trigger when user says "create a case study", "write a customer story", "make a case study PDF", "design a success story", "turn this customer win into a PDF".

Varnan-Tech/opendirectory

Generates data visualization charts (bar, line, area, pie, doughnut, scatter, radar, treemap) as PNG using Apache ECharts v6. 1080×1080px default, 5 style presets, highlight annotations. Trigger when user says "create a chart", "visualize data", "make a bar chart", "line graph", "pie chart", "data visualization", "chart this data", "plot", "graph", or "visualize these numbers".

Varnan-Tech/opendirectory

Creates professionally designed B2B SaaS e-books in HTML + CSS, exported as print-ready PDF. 3–10 pages, 9 style presets, 11 page layout types. Trigger when user says "create an ebook", "design a lead magnet", "make a PDF guide", "build a gated content piece", "write a B2B ebook", "design a white paper", "create a nurture asset", or "make a PDF report".

Varnan-Tech/opendirectory

Fetches low-star App Store and Google Play reviews, clusters them into broken-promise patterns, and generates a ranked copy brief with positioning opportunities.

관련 스킬