mcpField
M
map-your-market

map-your-market

github.com/varnan-tech营销与增长68746 次安装2 周前更新
代码仓库

关于 map-your-market

面向产品团队和营销人员的市场研究技能,从 Reddit、HN、GitHub Issues、G2 和 Google Trends 收集痛点信号。它对证据进行评分和聚类,构建 ICP 与定位框架,并要求引用和市场判断均依据实际采集数据。

主要功能

  1. 01从五个指定来源收集信号
  2. 02为痛点信号评分并聚类
  3. 03依据元数据构建 ICP 画像
  4. 04生成基于证据的定位角度
  5. 05报告方向性市场信号,不估算金额

适用场景

  1. 01研究某个产品类别的客户痛点
  2. 02比较竞争对手与市场信号
  3. 03依据客户用语制定产品定位

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/varnan-tech/opendirectory --skill map-your-market

安装后在 Claude Code 中输入/map-your-market确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.env.example
README.md
evals/
evals.json
references/
icp-signals.md
pain-scoring.md
subreddit-map.md
scripts/
fetch.py

详细介绍

绘制你的市场

输入产品描述、类别关键词或竞争对手名称。搜索 Reddit、HN、GitHub Issues、G2 和 Google Trends,寻找真实的痛点信号。为其评分并聚类。构建完整的定位框架:ICP 定义、按排名排列的痛点主题及原文引述、市场规模信号,以及根据人们实际使用的语言提炼的宣传角度。


**关键规则:**输出中的每条痛点引述都必须与脚本收集的原始数据完全一致。市场地图中的每个供应商名称都必须来自 G2 抓取结果或 GitHub 搜索结果。描述市场规模时必须说“信号表明”——绝不能根据薄弱的代理指标估算美元金额。如果某个来源返回 0 条结果,就报告 0——不要补充虚构的示例。


常见错误

Agent 会想要……为什么这是错的
编造痛点或市场规模数字每条痛点引述都必须与原始数据完全一致。市场规模必须引用发现的信号。绝不要估算“典型”市场规模。
按帖子数量而不是 pain_score 评分一条获得 2,000 个赞、讨论定价的帖子,比 50 条各获得 10 个赞的帖子更有说服力。使用 references/pain-scoring.md 中的 pain_score 公式。
对每个类别都使用相同的 subreddit在 devops 搜索中,r/politics 会带来噪声。搜索前,根据类别和竞争对手名称自动检测相关 subreddit。
不评分就把所有原始信号都发给 AI先在本地评分。只将 pain_score 最高的 60 条信号发送给 AI 聚类。这样可以节省令牌并提升聚类质量。
跳过从帖子元数据中提取 ICPSubreddit、flair、作者简介(HN)和 GitHub 组织类型,比帖子内容能提供更丰富的 ICP 信号。务必采集并报告这些信息。
混淆供应商数量与市场规模“G2 上有 47 家供应商”意味着竞争激烈,而不是市场规模大。所有信号都应作为方向性指标呈现,而非精确数字。

第 1 步:检查设置

echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- GitHub Issues search runs at 60 req/hr unauthenticated}"
echo "No other API keys required."
echo ""
echo "Data sources this run will use:"
echo "  Reddit public JSON  (no auth, 10 req/min)"
echo "  HN Algolia API      (no auth, free)"
echo "  GitHub Issues API   (${GITHUB_TOKEN:+authenticated, }60-5000 req/hr)"
echo "  G2 category scrape  (no auth, HTML parse)"
echo "  Google Trends       (no auth, unofficial endpoint)"

如果未设置 GITHUB_TOKEN:继续执行。未经身份验证的 GitHub 搜索每小时可请求 60 次,足以应付常规运行。若要重复使用,请在 github.com/settings/tokens 添加令牌(公共仓库无需任何权限范围)。


第 2 步:解析输入

从对话中收集:

  • category -- 描述市场领域的关键词(例如“开发者可观测性”“B2B 分析”“devops 工具”)
  • competitors -- 可选的竞争对手产品名称或域名列表(例如“Datadog、New Relic、Grafana”)
  • product_context -- 可选:用户的产品用途(有助于定制宣传角度)

如果用户只提供了产品描述,没有类别关键词:自行从描述中提取 2-3 个类别关键词。

如果用户只提供了竞争对手名称,没有类别:查找竞争对手并推断类别。

写入解析后的输入:

python3 << 'PYEOF'
import json, os

data = {
    "category": "CATEGORY_HERE",
    "competitors": ["COMP_1", "COMP_2"],
    "product_context": "PRODUCT_CONTEXT_HERE"
}

with open("/tmp/mym-input.json", "w") as f:
    json.dump(data, f, indent=2)
print("Input written to /tmp/mym-input.json")
print(f"Category: {data['category']}")
print(f"Competitors: {', '.join(data['competitors']) if data['competitors'] else 'none provided'}")
PYEOF

第 3 步:运行独立数据采集脚本

该脚本负责采集所有数据。首先检查脚本是否存在:

ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "not found"

如果可用,则运行:

GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
    "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['category'])")" \
    --competitors "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(','.join(d['competitors']))")" \
    --context "$(python3 -c "import json; d=json.load(open('/tmp/mym-input.json')); print(d['product_context'])")" \
    --output /tmp/mym-raw.json

等待运行完成(最多等待 4 分钟——Reddit + HN 搜索总共需要约 90 秒)。

验证输出:

python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
    d = json.load(f)
print(f'Reddit signals: {d[\"market_signals\"][\"reddit_signals_found\"]}')
print(f'HN signals:     {d[\"market_signals\"][\"hn_signals_found\"]}')
print(f'GitHub signals: {d[\"market_signals\"][\"github_issue_signals\"]}')
print(f'G2 vendors:     {d[\"market_signals\"][\"vendor_count_g2\"]}')
print(f'Trends:         {d[\"market_signals\"][\"trends_direction\"]}')
print(f'Total signals:  {d[\"summary\"][\"total_pain_signals\"]}')
"

如果信号总数少于 10:停止。告知用户:“为此类别找到的痛点信号少于 10 条。该市场可能过于小众,Reddit/HN 上的相关内容不足,也可能需要调整类别关键词。请尝试更宽泛的关键词,或添加竞争对手名称。”


步骤 4:AI 痛点聚类

打印用于 AI 分析的前 60 条痛点信号:

python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
    d = json.load(f)
top60 = sorted(d['raw_pains'], key=lambda x: x['pain_score'], reverse=True)[:60]
print(json.dumps(top60, indent=2))
"

现在你已有前 60 条痛点信号。请分析这些信号并生成痛点聚类。

AI 分析说明:

  • 在所有来源中识别 5-7 个反复出现的痛点主题
  • 为每个主题选取一个符合市场自身用语的名称(而非你自己的措辞)
  • 汇总每个聚类中所有信号的 pain_score
  • 为每个主题选出 3-5 条最合适的原文引述(分数最高、措辞最具体)
  • 标明每个主题主要集中在哪些来源和 subreddit
  • 标记仅出现在单一来源中的主题(置信度较低)

将聚类写入 /tmp/mym-clusters.json:

{
  "clusters": [
    {
      "theme": "exact language from the data",
      "total_score": 847,
      "signal_count": 34,
      "sources": {"reddit": 18, "hn": 12, "github_issue": 4},
      "top_subreddits": ["devops", "sysadmin"],
      "verbatim_quotes": [
        {"text": "exact quote", "source": "reddit", "score": 234, "url": "..."},
        {"text": "exact quote", "source": "hn", "score": 87, "url": "..."}
      ],
      "who_has_this_pain": "description of who is posting about this"
    }
  ]
}
python3 -c "
import json, os
# Confirm clusters file was written
with open('/tmp/mym-clusters.json') as f:
    d = json.load(f)
print(f'Clusters written: {len(d[\"clusters\"])}')
for c in d['clusters']:
    print(f'  {c[\"theme\"]} -- score: {c[\"total_score\"]}, signals: {c[\"signal_count\"]}')
"

步骤 5:ICP 画像

打印原始数据中的 ICP 信号:

python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
    d = json.load(f)
print('ICP signals:')
print(json.dumps(d['icp_signals'], indent=2))
print()
print('Subreddit distribution:')
sub_counts = {}
for p in d['raw_pains']:
    s = p.get('subreddit', '')
    if s:
        sub_counts[s] = sub_counts.get(s, 0) + 1
for sub, count in sorted(sub_counts.items(), key=lambda x: -x[1])[:10]:
    print(f'  r/{sub}: {count} signals')
"

根据上述 ICP 信号和 subreddit 分布,综合生成 ICP 画像。通过添加 icp 键,将其写入 /tmp/mym-clusters.json:

{
  "icp": {
    "who_they_are": "2-3 sentence profile using language from the data",
    "where_they_live": ["r/devops (89 posts)", "r/sysadmin (67 posts)", "HN ask-hn (34 threads)"],
    "what_they_say": ["verbatim quote 1", "verbatim quote 2", "verbatim quote 3"],
    "what_they_have_tried": ["alternative tools or approaches mentioned in the data"],
    "confidence": "high|medium|low -- based on signal volume and source diversity"
  }
}

步骤 6:市场规模综合评估

打印市场信号:

python3 -c "
import json
with open('/tmp/mym-raw.json') as f:
    d = json.load(f)
ms = d['market_signals']
print('Market signals:')
print(f'  G2 vendors:         {ms[\"vendor_count_g2\"]}')
print(f'  Trends direction:   {ms[\"trends_direction\"]}')
print(f'  HN signals (12mo):  {ms[\"hn_signals_found\"]}')
print(f'  Reddit signals:     {ms[\"reddit_signals_found\"]}')
print(f'  G2 top vendors:     {json.dumps(ms.get(\"top_vendors\", []), indent=4)}')
"

仅使用这些信号,对市场规模进行方向性评估。不要估算美元金额。使用类似以下的表述:

  • “信号显示市场竞争激烈且正在增长”(供应商众多 + 趋势向上)
  • “信号显示市场处于早期阶段”(供应商较少 + 信号量较低)
  • “信号显示市场已趋于饱和”(供应商众多 + 趋势持平或向下)

将评估结果作为 market_size 键添加到 /tmp/mym-clusters.json。


步骤 7:定位框架

根据聚类(步骤 4)、ICP(步骤 5)和市场规模(步骤 6),生成定位框架。

说明:

  • 选出排名前三的痛点聚类,作为主要定位角度
  • 为每个角度撰写一条定位陈述,使用数据中的原文措辞(而非改写)
  • 生成 3 条落地页标题,使用人们在痛点数据中使用的确切表述
  • 根据痛点措辞生成 3 条冷邮件主题行
  • 不要使用禁用词:powerful、robust、seamless、innovative、game-changing、streamline、leverage、transform、revolutionize

将完整的定位框架写入 /tmp/mym-output.json:

{
  "positioning_angles": [
    {
      "pain": "theme name",
      "statement": "one-line positioning using market language",
      "headline": "landing page headline using verbatim pain language",
      "cold_email_subject": "subject line"
    }
  ],
  "icp_card": {
    "one_liner": "one sentence: who they are + what they care about",
    "where_to_find_them": [...],
    "how_to_talk_to_them": "tone + vocabulary notes from the data"
  },
  "market_map": [...top vendors from G2 with positioning notes...]
}

步骤 8:自检并保存输出

运行自检:

python3 -c "
import json

# Load all outputs
with open('/tmp/mym-raw.json') as f:
    raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
    clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
    output = json.load(f)

raw_texts = set()
for p in raw['raw_pains']:
    raw_texts.add(p.get('title', ''))
    raw_texts.add(p.get('body_excerpt', ''))

# Check 1: No em dashes
import json as j
full_text = j.dumps(output)
if '—' in full_text:
    print('FAIL: em dash found in output')
else:
    print('PASS: no em dashes')

# Check 2: No banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
          'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
    print(f'FAIL: banned words found: {found}')
else:
    print('PASS: no banned words')

# Check 3: Market size language check
if 'billion' in full_text.lower() or 'trillion' in full_text.lower() or 'worth \$' in full_text.lower():
    print('FAIL: hard market size estimate found -- use directional language only')
else:
    print('PASS: no hard market size estimates')

# Check 4: Signal counts match
total = raw['summary']['total_pain_signals']
print(f'PASS: {total} total pain signals in raw data')

print('Self-QA complete.')
"

修复所有未通过的检查后再保存。

保存最终报告:

python3 << 'PYEOF'
import json, re
from datetime import datetime

with open('/tmp/mym-input.json') as f:
    inp = json.load(f)
with open('/tmp/mym-raw.json') as f:
    raw = json.load(f)
with open('/tmp/mym-clusters.json') as f:
    clusters = json.load(f)
with open('/tmp/mym-output.json') as f:
    output = json.load(f)

slug = re.sub(r'[^a-z0-9]+', '-', inp['category'].lower()).strip('-')
date = datetime.now().strftime('%Y-%m-%d')
outpath_md = f"docs/market-maps/{slug}-{date}.md"
outpath_json = f"docs/market-maps/{slug}-{date}.json"

# Build markdown report
ms = raw['market_signals']
icp = clusters.get('icp', {})
market_assessment = clusters.get('market_size', {})
angles = output.get('positioning_angles', [])
icp_card = output.get('icp_card', {})
market_map = output.get('market_map', [])

lines = [
    f"# Market Map: {inp['category'].title()}",
    f"Date: {date} | Signals analyzed: {raw['summary']['total_pain_signals']} | Sources: Reddit ({ms['reddit_signals_found']}) + HN ({ms['hn_signals_found']}) + GitHub Issues ({ms['github_issue_signals']})",
    "",
    "---",
    "",
    "## Market Size Signals",
    f"Vendors on G2: {ms['vendor_count_g2']} | Google Trends: {ms['trends_direction'].upper()} | Market stage: {market_assessment.get('stage', 'see signals below')}",
    "",
    market_assessment.get('summary', ''),
    "",
    "---",
    "",
    "## Your ICP",
    "",
    f"**Who they are:** {icp.get('who_they_are', '')}",
    "",
    f"**Where they live:** {', '.join(icp.get('where_they_live', []))}",
    "",
    "**What they say:**",
]
for q in icp.get('what_they_say', []):
    lines.append(f'> "{q}"')
lines += ["", "---", "", "## Top Pains (ranked by signal strength)", ""]

for i, c in enumerate(clusters.get('clusters', []), 1):
    lines.append(f"### Pain {i}: {c['theme']} [score: {c['total_score']}]")
    sources = c.get('sources', {})
    source_str = " + ".join(f"{src} ({cnt})" for src, cnt in sources.items())
    lines.append(f"{c['signal_count']} signals | Sources: {source_str}")
    lines.append(f"Who has this pain: {c.get('who_has_this_pain', '')}")
    lines.append("")
    lines.append("Verbatim:")
    for q in c.get('verbatim_quotes', [])[:4]:
        lines.append(f'> "{q[\"text\"]}" ({q["source"]}, score: {q["score"]})')
    lines.append("")

lines += ["---", "", "## Market Map (Key Players)", ""]
if market_map:
    lines.append("| Vendor | Positioning |")
    lines.append("|---|---|")
    for v in market_map:
        lines.append(f"| {v.get('name','')} | {v.get('positioning','')} |")
else:
    top = ms.get('top_vendors', [])
    if top:
        lines.append("| Vendor | G2 Reviews | Rating |")
        lines.append("|---|---|---|")
        for v in top:
            lines.append(f"| {v.get('name','')} | {v.get('review_count','')} | {v.get('rating','')} |")

lines += ["", "---", "", "## Messaging Framework", ""]
for a in angles:
    lines.append(f"**{a['pain']}:** {a['statement']}")
    lines.append(f"Headline: \"{a['headline']}\"")
    lines.append(f"Cold email subject: \"{a['cold_email_subject']}\"")
    lines.append("")

lines += ["---", "", "## ICP Card", "",
    f"**One liner:** {icp_card.get('one_liner', '')}",
    "",
    f"**Find them at:** {', '.join(icp_card.get('where_to_find_them', []))}",
    "",
    f"**How to talk to them:** {icp_card.get('how_to_talk_to_them', '')}",
    "",
    "---",
    "",
    "## Data Quality Notes",
    f"- All pain quotes are verbatim from raw signals",
    f"- All vendor names from G2 scrape",
    f"- Market size is directional only (no dollar estimates)",
    f"- Sources: Reddit ({ms['reddit_signals_found']}), HN ({ms['hn_signals_found']}), GitHub Issues ({ms['github_issue_signals']}), G2 ({ms['vendor_count_g2']} vendors)",
    "",
    f"Saved to: {outpath_md}",
    f"JSON snapshot: {outpath_json}",
]

with open(outpath_md, 'w') as f:
    f.write('\n'.join(lines))

# Save JSON snapshot
snapshot = {"input": inp, "market_signals": ms, "clusters": clusters.get('clusters', []),
            "icp": icp, "market_size": market_assessment, "positioning": output, "date": date}
with open(outpath_json, 'w') as f:
    json.dump(snapshot, f, indent=2)

print(f"Report saved: {outpath_md}")
print(f"JSON snapshot: {outpath_json}")
PYEOF

清理临时文件:

rm -f /tmp/mym-input.json /tmp/mym-raw.json /tmp/mym-clusters.json /tmp/mym-output.json
echo "Done. Market map saved to docs/market-maps/"

向用户展示已保存的 .md 文件的完整内容。

评论

支持 Markdown