Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions .claude/skills/mkt-competitive-brief/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ argument-hint: "<competitor or market segment>"

> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md).

> **LIMITES DE FONTE — leia antes de coletar (spec MUST #3):**
> - **Meta Ad Library API** é restrita a político/social-issue + somente EU em 2026. Para concorrente **comercial**, criativos são UI-only (`facebook.com/ads/library`) — não existe API pública comercial. Não prometa criativos via API; declare este limite no brief.
> - **Google Ads Transparency Center** não tem API pública (até v23/jan-2026). Não expõe spend, keyword-trigger nem targeting. Somente UI (`adstransparency.google.com`). Declare o limite.
> - **Spend e keyword-trigger** não são observáveis em nenhuma fonte pública. Qualquer número é estimativa de terceiros (Semrush/DataForSEO) com faixa de erro de 15-30% — rotule sempre.
> - **DataForSEO** (SERP features, AI Overview, keyword gap) está disponível internamente via `scripts/competitor_intel.py`. Use quando `DATAFORSEO_LOGIN` + `DATAFORSEO_PASSWORD` estiverem no `.env`.

Research competitors and generate a structured competitive analysis comparing positioning, messaging, content strategy, and market presence.

## Trigger
Expand All @@ -33,15 +39,43 @@ Gather the following from the user:
- Pricing and packaging (if publicly available)
- Market presence and audience

## Proveniência obrigatória (gate anti-fabricação)

**Todo dado sobre um concorrente exige fonte + URL + data de coleta.** Sem isso, o dado não entra no brief — vai para a tabela de proveniência com status `não verificado / pesquisar`.

Inclua sempre uma tabela de proveniência no início do brief entregue:

| Dado | Fonte | URL | Data coleta | Status |
|------|-------|-----|-------------|--------|
| Tagline homepage | Site oficial | https://... | YYYY-MM-DD | verificado |
| Keyword gap | DataForSEO Labs | docs.dataforseo.com | YYYY-MM-DD | verificado (±20%) |
| Criativos Meta | Meta Ad Library UI | facebook.com/ads/library | — | não verificado / pesquisar |
| Spend estimado | — | — | — | não observável (fonte pública inexistente) |

Regras:
- **Estimativas de terceiros** (DataForSEO, Semrush, SpyFu): sempre rotule com faixa de erro (tipicamente 15-30% em volume).
- **"Não observável"**: spend, keyword-trigger, targeting Meta/Google — nunca estime, declare o limite.
- **Memória do modelo não é fonte**: qualquer afirmação sobre o concorrente que não tenha URL verificável nesta sessão vai para `não verificado`.

## Research Process

For each competitor, research using web search:
Para cada concorrente, colete nas seguintes prioridades:

1. **Company website** — homepage messaging, product pages, about page, pricing page
2. **Recent news** — press releases, funding announcements, product launches, partnerships (last 6 months)
3. **Content strategy** — blog topics, resource types, social media presence, webinars, podcasts
4. **Review sites and comparisons** — third-party comparisons, analyst mentions, customer review themes
5. **Job postings** — hiring signals that indicate strategic direction (optional)
### Fontes verificáveis com dado real (prioridade 1)

1. **Company website** — homepage messaging, product pages, about page, pricing page (WebFetch direto, não search genérico)
2. **SEO/SERP/AI-Overview** — via `scripts/competitor_intel.py` quando `DATAFORSEO_LOGIN`/`DATAFORSEO_PASSWORD` disponíveis:
- Keyword gap: `python3 scripts/competitor_intel.py --gap --target competitor.com.br --self cliente.com.br`
- SERP features + AI Overview: `python3 scripts/competitor_intel.py --keywords "keyword1,keyword2"`
- Se DataForSEO indisponível: marcar `N/A — DataForSEO não configurado` na tabela de proveniência.
3. **Recent news** — press releases, funding, product launches (últimos 6 meses)
4. **Review sites** — G2, Capterra, TrustRadius: WebFetch da página do concorrente, extrair temas de elogio/reclamação com citação
5. **Job postings** — sinais estratégicos (novo produto, expansão de mercado)

### Fontes sem API — coleta manual (prioridade 2, declarar limite)

6. **Meta Ad Library** — `facebook.com/ads/library` (UI pública; API comercial inexistente). Pesquisar manualmente e registrar URL + data na tabela de proveniência. Se não coletado: marcar `não verificado — UI-only, requer coleta manual`.
7. **Google Ads Transparency Center** — `adstransparency.google.com` (sem API; não mostra spend). Mesma lógica.

### Research Sources

Expand All @@ -61,7 +95,7 @@ Gather intelligence from these categories of sources:
- **Analyst reports**: Gartner, Forrester, IDC — market positioning and category placement
- **News coverage**: TechCrunch, industry publications — funding, partnerships, narrative
- **Social listening**: mentions, sentiment, share of voice across social platforms
- **SEO tools**: keyword rankings, organic traffic estimates, content gaps
- **SEO tools**: keyword rankings, organic traffic estimates, content gaps — **use `scripts/competitor_intel.py` para DataForSEO real (SERP features, AI Overview, keyword gap domain-vs-domain) quando disponível; estimativas de terceiros sempre com faixa de erro declarada**
- **Financial filings**: revenue, growth rate, investment areas (for public companies)
- **Community forums**: community forums (e.g. Reddit, Discourse), industry chat groups (e.g. Slack communities) — user sentiment

Expand Down Expand Up @@ -104,7 +138,7 @@ For each competitor:
- Content types produced (ebooks, webinars, case studies, tools)
- Social media presence and engagement approach
- Thought leadership themes
- SEO strategy observations (what terms they appear to target)
- SEO strategy observations — **não inferir, coletar**: use `scripts/competitor_intel.py --gap --target DOMINIO_CONCORRENTE --self DOMINIO_CLIENTE` para keywords reais com volume + rank. Se DataForSEO indisponível, declarar `N/A — requer DataForSEO` e não estimar.

#### Strengths
- What they do well
Expand Down Expand Up @@ -320,6 +354,10 @@ Questions competitors might encourage prospects to ask you, with prepared respon

## Output

O brief deve sempre começar com a **Tabela de Proveniência** (ver seção "Proveniência obrigatória") antes de qualquer análise. Dados sem fonte verificável nesta sessão ficam marcados como `não verificado` — nunca são apresentados como fatos.

Salvar em `workspace/marketing/{cliente}/[C]competitive-brief-{concorrente}-{YYYY-MM-DD}.md`.

Present the full competitive brief with clear formatting. Note the date of the research so the user knows the freshness of the data.

After the brief, ask:
Expand Down
286 changes: 286 additions & 0 deletions .claude/skills/mkt-competitive-brief/scripts/competitor_intel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""
competitor_intel.py — coletor de inteligência competitiva REAL (MUST #2 da spec).

Reutiliza o `dataforseo_client.py` que JÁ EXISTE em `mkt-seo-ops` (infra já paga e
autenticada) via importlib, sem duplicar credencial nem cliente HTTP. Cobre dois eixos:

1. SERP features + AI Overview presence + intent por keyword (DataForSEOClient.keyword_intel)
2. Keyword gap domínio-vs-domínio (DataForSEO Labs domain_intersection)

REGRA DURA — anti-fabricação (spec MUST #1):
Toda saída carrega `source`, `url` (quando aplicável) e `collected_at`.
Sem credencial → status "N/A" com instrução. Erro de rede/API → "N/A" com razão.
O LLM consumidor DEVE tratar N/A como "não verificado / pesquisar", nunca como zero.

LIMITES DE FONTE (spec MUST #3 — não observável por este script):
- Spend do concorrente, keyword que disparou o anúncio, targeting Meta/Google.
- Criativos Meta: Ad Library API restrita a político/social-issue + só EU em 2026;
uso comercial é UI-only (facebook.com/ads/library) — NÃO existe API comercial.
- Criativos Google: Ads Transparency Center sem API pública (até v23/jan-2026);
não mostra spend, keyword-trigger nem targeting.
Esses eixos exigem coletor de UI (SHOULD #4/#5 da spec) — fora do escopo aqui.

Uso:
python3 competitor_intel.py --check
python3 competitor_intel.py --keywords "usinagem cnc,retifica industrial" --location br
python3 competitor_intel.py --gap --target competitor.com.br --self mycliente.com.br
"""

import os
import sys
import json
import argparse
import importlib.util
from datetime import datetime, timezone
from pathlib import Path


# ─────────────────────────────────────────────
# Reuso do dataforseo_client.py de mkt-seo-ops
# ─────────────────────────────────────────────

def _candidate_paths():
"""
Candidatos ao dataforseo_client.py de mkt-seo-ops. Resolve o diretório real
do script (segue symlink) antes de subir a árvore procurando por
.claude/skills/mkt-seo-ops/dataforseo_client.py.
"""
here = Path(__file__).resolve().parent # .../mkt-competitive-brief/scripts
return [
base / ".claude" / "skills" / "mkt-seo-ops" / "dataforseo_client.py"
for base in [here, *here.parents]
]


def _load_client():
"""
Importa DataForSEOClient do mkt-seo-ops via importlib (padrão seo_ops.py).
Retorna (client, None) ou (None, motivo_str).
"""
for path in _candidate_paths():
if not path.exists():
continue
try:
spec = importlib.util.spec_from_file_location("dataforseo_client", str(path))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.DataForSEOClient(), None
except Exception as e: # noqa: BLE001
return None, f"falha ao carregar dataforseo_client.py em {path}: {e}"
return None, (
"dataforseo_client.py não encontrado — verifique se mkt-seo-ops está instalado "
"em .claude/skills/mkt-seo-ops/."
)


def _today():
return datetime.now(timezone.utc).strftime("%Y-%m-%d")


# ─────────────────────────────────────────────
# Eixo 1 — SERP features + AI Overview + intent
# ─────────────────────────────────────────────

def serp_intel(keywords: list, location="br", language="pt-BR") -> dict:
"""
SERP features / AI Overview presence / intent por keyword via DataForSEO SERP API.
Cada resultado carrega source + collected_at (proveniência obrigatória).
N/A graceful sem fabricar dado.
"""
client, reason = _load_client()
if client is None:
return {"status": "N/A", "reason": reason, "collected_at": _today()}
if not client.available():
return {
"status": "N/A",
"reason": (
"DATAFORSEO_LOGIN/DATAFORSEO_PASSWORD ausentes no .env — "
"SERP features, AI Overview presence e intent indisponíveis. "
"Configure para habilitar (Labs ~$0.025/1k kw)."
),
"collected_at": _today(),
}
# location não-mapeada → N/A explícito, nunca default BR silencioso (anti-fabricação).
if location.lower() not in _LOC:
return {
"status": "N/A",
"reason": (
f"location '{location}' não mapeada (suportadas: {', '.join(sorted(_LOC))}). "
"Default silencioso para BR fabricaria SERP de outro país."
),
"collected_at": _today(),
}
truncated = len(keywords) > 50
kw_slice = keywords[:50]
try:
raw = client.keyword_intel(kw_slice, location=location, language=language)
except Exception as e: # noqa: BLE001
return {
"status": "N/A",
"reason": f"erro DataForSEO (keyword_intel): {e}",
"collected_at": _today(),
}
if raw.get("status") != "OK":
raw.setdefault("collected_at", _today())
return raw
out = {
"status": "OK",
"source": "DataForSEO SERP API (serp/google/organic/live/advanced + Labs search_intent/live)",
"source_url": "https://docs.dataforseo.com/v3/serp/google/organic/live/advanced/",
"collected_at": _today(),
"location": location,
"language": language,
"keywords": raw.get("keywords", []),
}
if truncated:
out["truncated"] = True
out["truncated_note"] = (
f"{len(keywords)} keywords recebidas; processadas as primeiras 50. "
"As demais NÃO foram consultadas."
)
return out


# ─────────────────────────────────────────────
# Eixo 2 — keyword gap domínio-vs-domínio
# ─────────────────────────────────────────────

# Tabelas de location/language (replicadas aqui para não depender de acesso a
# atributos privados do módulo carregado via importlib).
_LOC = {"br": 2076, "us": 2840, "pt": 2620}
_LANG = {"pt-br": "pt", "pt": "pt", "en": "en", "en-us": "en"}


def keyword_gap(target_domain: str, self_domain: str,
location="br", language="pt-BR", limit=50) -> dict:
"""
Keywords que target_domain (concorrente) rankeia e self_domain (cliente) não.
Usa DataForSEO Labs domain_intersection com intersections=False.
Reusa _post/_auth_header do client existente — sem duplicar credencial.
Faixa de erro típica 15-30% (estimativa DataForSEO, não dado exato).
"""
client, reason = _load_client()
if client is None:
return {"status": "N/A", "reason": reason, "collected_at": _today()}
if not client.available():
return {
"status": "N/A",
"reason": "DATAFORSEO_LOGIN/DATAFORSEO_PASSWORD ausentes no .env.",
"collected_at": _today(),
}

# location não-mapeada → N/A explícito, nunca default BR silencioso (anti-fabricação).
if location.lower() not in _LOC:
return {
"status": "N/A",
"reason": (
f"location '{location}' não mapeada (suportadas: {', '.join(sorted(_LOC))}). "
"Default silencioso para BR fabricaria gap de outro país."
),
"collected_at": _today(),
}
loc_code = _LOC[location.lower()]
lang_code = _LANG.get(language.lower(), "pt")

payload = [{
"target1": target_domain,
"target2": self_domain,
"location_code": loc_code,
"language_code": lang_code,
"intersections": False, # False = target1 tem, target2 não tem
"limit": min(limit, 100),
"order_by": ["keyword_data.keyword_info.search_volume,desc"],
}]
try:
resp = client._post("dataforseo_labs/google/domain_intersection/live", payload)
except Exception as e: # noqa: BLE001
return {
"status": "N/A",
"reason": f"erro DataForSEO Labs (domain_intersection): {e}",
"collected_at": _today(),
}

gaps = []
for t in (resp.get("tasks") or []):
for res in (t.get("result") or []):
for item in (res.get("items") or []):
kd = item.get("keyword_data") or {}
ki = kd.get("keyword_info") or {}
first = item.get("first_domain_serp_element") or {}
gaps.append({
"keyword": kd.get("keyword", ""),
"search_volume": ki.get("search_volume"),
"competitor_rank": first.get("rank_absolute"),
"competitor_url": first.get("url"),
})

return {
"status": "OK",
"source": "DataForSEO Labs domain_intersection/live",
"source_url": "https://docs.dataforseo.com/v3/dataforseo_labs/google/domain_intersection/live/",
"collected_at": _today(),
"target_competitor": target_domain,
"self_domain": self_domain,
"location": location,
"note": (
"Estimativa 3rd-party (DataForSEO) — faixa de erro típica 15-30% em volume; "
"rank é snapshot da SERP na data de coleta, não histórico."
),
"gap_keywords": gaps,
}


# ─────────────────────────────────────────────
# CLI
# ─────────────────────────────────────────────

def main():
p = argparse.ArgumentParser(
description="Coletor de inteligência competitiva REAL via DataForSEO (reuso mkt-seo-ops)"
)
p.add_argument("--check", action="store_true",
help="Verifica disponibilidade do DataForSEO client")
p.add_argument("--keywords",
help="CSV de keywords para SERP features/AI-Overview/intent")
p.add_argument("--gap", action="store_true",
help="Keyword gap domínio-vs-domínio (exige --target e --self)")
p.add_argument("--target", help="Domínio do concorrente")
p.add_argument("--self", dest="self_domain", help="Domínio do cliente")
p.add_argument("--location", default="br")
p.add_argument("--language", default="pt-BR")
args = p.parse_args()

if args.check:
client, reason = _load_client()
print(json.dumps({
"client_loaded": client is not None,
"available": bool(client and client.available()),
"reason": reason,
}, indent=2, ensure_ascii=False))
return

if args.gap:
if not (args.target and args.self_domain):
print(json.dumps({"status": "ERROR",
"reason": "--gap exige --target e --self"}, ensure_ascii=False))
sys.exit(2)
print(json.dumps(
keyword_gap(args.target, args.self_domain, args.location, args.language),
indent=2, ensure_ascii=False
))
return

if args.keywords:
kws = [k.strip() for k in args.keywords.split(",") if k.strip()]
print(json.dumps(
serp_intel(kws, args.location, args.language),
indent=2, ensure_ascii=False
))
return

p.print_help()


if __name__ == "__main__":
main()
Loading