elo-skills-mcp 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # ELO Skills MCP
2
+
3
+ **O Método ELO Criativo dentro do seu Claude.** 18 skills de estratégia de conteúdo — post, reels, carrossel, stories, legenda, hooks, brief, ideias, análise, essência, branding, posicionamento, anúncio, público e mais — todas no seu tom de marca.
4
+
5
+ > Produto de acesso pago. Requer uma **chave de licença** (`ELO_LICENSE_KEY`). Adquira em **[ramonniellymorais.com.br/elo-skills](https://ramonniellymorais.com.br/elo-skills)**.
6
+
7
+ ## Como usar (após a compra)
8
+
9
+ Você recebe sua chave por e-mail com o passo a passo completo. Em resumo:
10
+
11
+ ```bash
12
+ npm install -g elo-skills-mcp
13
+ claude mcp add elo-skills --env ELO_LICENSE_KEY=SUA-CHAVE -- elo-skills-mcp
14
+ ```
15
+
16
+ Reinicie o Claude Code e rode `elo_setup` para configurar sua marca. Depois é só usar as skills (`elo_post`, `elo_carrossel`, etc.).
17
+
18
+ ## Requisitos
19
+
20
+ - [Claude Code](https://claude.ai/download)
21
+ - [Node.js](https://nodejs.org) 18+
22
+ - Conexão com a internet (as skills validam a licença a cada uso)
23
+
24
+ ## Suporte
25
+
26
+ Dúvida ou erro? **contato@ramonniellymorais.com.br**
27
+
28
+ ---
29
+
30
+ © Ramonnielly Silva Morais — Método ELO Criativo. Uso pessoal mediante licença. Redistribuição não autorizada.
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "elo-skills-mcp",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Método ELO Criativo — Skills de Estratégia de Conteúdo para Claude",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "elo-skills-mcp": "index.js"
9
9
  },
10
+ "files": [
11
+ "index.js",
12
+ "src/",
13
+ "README.md"
14
+ ],
10
15
  "scripts": {
11
16
  "start": "node index.js",
12
17
  "dev": "node --watch index.js"
@@ -0,0 +1,90 @@
1
+ // Cliente da API de prompts do Método ELO.
2
+ // O método NÃO viaja no pacote npm: cada tool busca seu template aqui,
3
+ // numa API autenticada que valida a licença server-side.
4
+ //
5
+ // Cache só em memória (some ao fechar o Claude) — nunca grava o método em disco.
6
+ // Fallback offline limitado: usa cache em RAM por até HARD_OFFLINE_LIMIT desde a
7
+ // última validação online bem-sucedida; passado esse limite, bloqueia mesmo offline.
8
+
9
+ import { getDeviceHash, getDeviceLabel } from './device.js'
10
+
11
+ const API_URL = process.env.ELO_API_URL || 'https://elo-skills-mcp.vercel.app'
12
+ const TEMPLATE_TTL = 1000 * 60 * 20 // 20 min de cache em RAM por tool
13
+ const HARD_OFFLINE_LIMIT = 1000 * 60 * 60 * 72 // 72h sem validação online → bloqueia
14
+
15
+ const PURCHASE_URL = 'ramonniellymorais.com.br/elo-skills'
16
+ const SUPPORT_EMAIL = 'contato@ramonniellymorais.com.br'
17
+
18
+ const templateCache = new Map() // toolName -> { template, ts }
19
+ let lastOnlineOk = 0
20
+
21
+ export async function fetchTemplate(toolName) {
22
+ const key = process.env.ELO_LICENSE_KEY
23
+ if (!key) throw new Error(missingKeyMessage())
24
+
25
+ const now = Date.now()
26
+ const cached = templateCache.get(toolName)
27
+ if (cached && now - cached.ts < TEMPLATE_TTL) return cached.template
28
+
29
+ try {
30
+ const res = await fetch(`${API_URL}/api/tool`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${key}`
35
+ },
36
+ body: JSON.stringify({
37
+ tool: toolName,
38
+ device_hash: getDeviceHash(key),
39
+ device_label: getDeviceLabel()
40
+ })
41
+ })
42
+
43
+ if (res.status === 401 || res.status === 403) {
44
+ const { reason } = await safeJson(res)
45
+ throw new Error(licenseError(reason))
46
+ }
47
+ if (res.status === 429) {
48
+ throw new Error('❌ Muitas requisições em pouco tempo. Aguarde alguns minutos e tente de novo.')
49
+ }
50
+ if (!res.ok) throw new Error(`Erro ${res.status} ao buscar a skill.`)
51
+
52
+ const data = await safeJson(res)
53
+ if (!data.template) throw new Error('Resposta inválida do servidor.')
54
+
55
+ templateCache.set(toolName, { template: data.template, ts: now })
56
+ lastOnlineOk = now
57
+ return data.template
58
+
59
+ } catch (err) {
60
+ if (err.message?.startsWith('❌')) throw err
61
+ // Falha de rede: usa cache em RAM se ainda dentro da janela offline
62
+ if (cached && now - lastOnlineOk < HARD_OFFLINE_LIMIT) return cached.template
63
+ throw new Error('❌ Sem conexão com o servidor das skills ELO. Verifique sua internet e tente de novo.')
64
+ }
65
+ }
66
+
67
+ function missingKeyMessage() {
68
+ return (
69
+ '❌ Licença não encontrada.\n\n' +
70
+ 'Adicione sua chave de licença no arquivo de configuração do Claude Code:\n\n' +
71
+ '"ELO_LICENSE_KEY": "SUA-CHAVE-AQUI"\n\n' +
72
+ `Adquira sua licença em: ${PURCHASE_URL}`
73
+ )
74
+ }
75
+
76
+ function licenseError(reason) {
77
+ switch (reason) {
78
+ case 'not_found': return `❌ Chave de licença inválida. Verifique a chave ou adquira em ${PURCHASE_URL}`
79
+ case 'revoked': return `❌ Sua licença foi revogada. Entre em contato: ${SUPPORT_EMAIL}`
80
+ case 'refunded': return '❌ Esta licença foi reembolsada e está inativa.'
81
+ case 'expired': return `❌ Sua licença expirou. Renove em ${PURCHASE_URL}`
82
+ case 'suspended': return `❌ Sua licença está suspensa. Entre em contato: ${SUPPORT_EMAIL}`
83
+ case 'too_many_devices': return `❌ Limite de dispositivos atingido para esta licença. Entre em contato: ${SUPPORT_EMAIL}`
84
+ default: return `❌ Não foi possível validar sua licença. Adquira ou renove em ${PURCHASE_URL}`
85
+ }
86
+ }
87
+
88
+ async function safeJson(res) {
89
+ try { return await res.json() } catch { return {} }
90
+ }
package/src/device.js ADDED
@@ -0,0 +1,35 @@
1
+ // Identidade de dispositivo para binding de licença.
2
+ // Gera um id estável por máquina (guardado em ~/.elo/device.json) e
3
+ // só envia ao servidor o HASH combinado com a chave — nunca o id cru.
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
6
+ import { homedir, hostname } from 'os'
7
+ import { join } from 'path'
8
+ import { createHash, randomUUID } from 'crypto'
9
+
10
+ const ELO_DIR = join(homedir(), '.elo')
11
+ const DEVICE_PATH = join(ELO_DIR, 'device.json')
12
+
13
+ function getDeviceId() {
14
+ if (!existsSync(ELO_DIR)) mkdirSync(ELO_DIR, { recursive: true })
15
+ if (existsSync(DEVICE_PATH)) {
16
+ try {
17
+ const saved = JSON.parse(readFileSync(DEVICE_PATH, 'utf-8'))
18
+ if (saved.id) return saved.id
19
+ } catch { /* recria abaixo */ }
20
+ }
21
+ const id = randomUUID()
22
+ writeFileSync(DEVICE_PATH, JSON.stringify({ id, created_at: new Date().toISOString() }, null, 2))
23
+ return id
24
+ }
25
+
26
+ // Hash estável do dispositivo combinado com a chave. O servidor limita
27
+ // quantos hashes distintos uma licença pode ter (max_activations).
28
+ export function getDeviceHash(licenseKey) {
29
+ return createHash('sha256').update(`${getDeviceId()}:${licenseKey}`).digest('hex')
30
+ }
31
+
32
+ // Rótulo amigável só para suporte ("MacBook da Ramon"). Não identifica de forma única.
33
+ export function getDeviceLabel() {
34
+ try { return hostname() } catch { return 'unknown' }
35
+ }
package/src/license.js CHANGED
@@ -1,66 +1,18 @@
1
- // Validação de licença via Supabase
2
- // Tabela: licenses (key TEXT, active BOOLEAN, expires_at TIMESTAMPTZ, owner_name TEXT)
1
+ // Checagem local mínima: só confirma que a chave está presente.
2
+ // A validação REAL (ativa / expirada / revogada / dispositivos) acontece
3
+ // server-side, na API de prompts, a cada chamada de tool. Nenhuma URL ou
4
+ // chave de Supabase vive mais no pacote.
3
5
 
4
- const SUPABASE_URL = 'https://dzicopswqijljhrimtee.supabase.co'
5
- const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImR6aWNvcHN3cWlqbGpocmltdGVlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU2NzExNjUsImV4cCI6MjA5MTI0NzE2NX0.JgL9jhJIxqQLm8aeKNLKh8HOS2idGqUOslwAqJUMoaY'
6
+ const PURCHASE_URL = 'ramonniellymorais.com.br/elo-skills'
6
7
 
7
- let cache = null
8
- let cacheTime = 0
9
- const CACHE_TTL = 1000 * 60 * 60 // 1 hora
10
-
11
- export async function validateLicense() {
8
+ export function assertLicensePresent() {
12
9
  const key = process.env.ELO_LICENSE_KEY
13
-
14
10
  if (!key) {
15
11
  throw new Error(
16
12
  '❌ Licença não encontrada.\n\n' +
17
13
  'Adicione sua chave de licença no arquivo de configuração do Claude Code:\n\n' +
18
14
  '"ELO_LICENSE_KEY": "SUA-CHAVE-AQUI"\n\n' +
19
- 'Adquira sua licença em: ramonniellymorais.com.br/elo-skills'
15
+ `Adquira sua licença em: ${PURCHASE_URL}`
20
16
  )
21
17
  }
22
-
23
- // Cache válido — não bate no Supabase a cada chamada
24
- const now = Date.now()
25
- if (cache && now - cacheTime < CACHE_TTL) return cache
26
-
27
- try {
28
- const res = await fetch(
29
- `${SUPABASE_URL}/rest/v1/licenses?key=eq.${encodeURIComponent(key)}&select=active,expires_at,owner_name`,
30
- {
31
- headers: {
32
- 'apikey': SUPABASE_ANON_KEY,
33
- 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
34
- }
35
- }
36
- )
37
-
38
- if (!res.ok) throw new Error('Erro ao verificar licença.')
39
-
40
- const data = await res.json()
41
-
42
- if (!data.length) {
43
- throw new Error('❌ Chave de licença inválida. Verifique a chave ou adquira em ramonniellymorais.com.br/elo-skills')
44
- }
45
-
46
- const license = data[0]
47
-
48
- if (!license.active) {
49
- throw new Error('❌ Sua licença está desativada. Entre em contato: contato@ramonniellymorais.com.br')
50
- }
51
-
52
- if (license.expires_at && new Date(license.expires_at) < new Date()) {
53
- throw new Error('❌ Sua licença expirou. Renove em ramonniellymorais.com.br/elo-skills')
54
- }
55
-
56
- cache = license
57
- cacheTime = now
58
- return license
59
-
60
- } catch (err) {
61
- if (err.message.startsWith('❌')) throw err
62
- // Fallback offline: se Supabase estiver fora, usa cache anterior
63
- if (cache) return cache
64
- throw new Error('❌ Não foi possível verificar a licença. Verifique sua conexão.')
65
- }
66
18
  }
@@ -0,0 +1,32 @@
1
+ // Fábrica de tools "finas". Os metadados (name/description/inputSchema) ficam
2
+ // locais e públicos — o MCP precisa deles para listar. O TEMPLATE do método é
3
+ // buscado na API autenticada em runtime e renderizado em memória com o contexto
4
+ // de marca local (que nunca vai ao servidor).
5
+
6
+ import { getBrandConfig, formatBrandContext } from './brand.js'
7
+ import { fetchTemplate } from './api-client.js'
8
+ import { renderTemplate } from './render.js'
9
+
10
+ function resolveContext(marca) {
11
+ const config = getBrandConfig(marca || 'minha-marca')
12
+ if (!config) {
13
+ return `⚠️ Marca "${marca || 'minha-marca'}" não configurada. Use elo_setup primeiro.`
14
+ }
15
+ return formatBrandContext(config)
16
+ }
17
+
18
+ export function makeTool({ name, description, inputSchema, prepare }) {
19
+ return {
20
+ name,
21
+ description,
22
+ inputSchema,
23
+ handler: async (args = {}) => {
24
+ const { marca, ...params } = args
25
+ const ctx = resolveContext(marca)
26
+ const data = prepare ? prepare(params) : params
27
+ const template = await fetchTemplate(name) // valida licença server-side; lança erro amigável
28
+ const body = renderTemplate(template, data)
29
+ return { content: [{ type: 'text', text: `${ctx}\n\n---\n\n${body}` }] }
30
+ }
31
+ }
32
+ }
package/src/render.js ADDED
@@ -0,0 +1,32 @@
1
+ // Renderizador leve estilo mustache para os templates do método.
2
+ // Suporta:
3
+ // {{var}} → substitui pelo valor (vazio se ausente)
4
+ // {{#var}}...{{/var}} → inclui o bloco só se var for "verdadeiro"
5
+ // {{^var}}...{{/var}} → inclui o bloco só se var for "falso" (else)
6
+ //
7
+ // Os templates vêm da API autenticada; o cliente injeta os params aqui,
8
+ // em memória, e nunca grava o método em disco.
9
+
10
+ function isTruthy(v) {
11
+ if (Array.isArray(v)) return v.length > 0
12
+ return v !== undefined && v !== null && v !== '' && v !== false && v !== 0
13
+ }
14
+
15
+ export function renderTemplate(template, data = {}) {
16
+ // Seções (normais e invertidas) — resolvidas primeiro
17
+ let out = template.replace(
18
+ /\{\{([#^])(\w+)\}\}([\s\S]*?)\{\{\/\2\}\}/g,
19
+ (_, kind, key, inner) => {
20
+ const show = kind === '#' ? isTruthy(data[key]) : !isTruthy(data[key])
21
+ return show ? inner : ''
22
+ }
23
+ )
24
+
25
+ // Variáveis simples
26
+ out = out.replace(/\{\{(\w+)\}\}/g, (_, key) => {
27
+ const v = data[key]
28
+ return v === undefined || v === null ? '' : String(v)
29
+ })
30
+
31
+ return out
32
+ }
package/src/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
4
- import { validateLicense } from './license.js'
4
+ import { assertLicensePresent } from './license.js'
5
5
  import * as tools from './tools/index.js'
6
6
 
7
7
  // Lista de todas as ferramentas registradas
@@ -9,7 +9,7 @@ const ALL_TOOLS = Object.values(tools)
9
9
 
10
10
  export async function createServer() {
11
11
  const server = new Server(
12
- { name: 'elo-skills-mcp', version: '1.0.0' },
12
+ { name: 'elo-skills-mcp', version: '2.0.0' },
13
13
  { capabilities: { tools: {} } }
14
14
  )
15
15
 
@@ -26,12 +26,13 @@ export async function createServer() {
26
26
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
27
27
  const { name, arguments: args } = request.params
28
28
 
29
- // Valida licença antes de qualquer execução
30
- // Setup é liberado sem licença para facilitar o onboarding
29
+ // Checagem local de presença da chave antes de qualquer execução.
30
+ // A validação real (ativa/expirada/revogada) é server-side, na API de prompts.
31
+ // Setup é liberado sem licença para facilitar o onboarding.
31
32
  const FREE_TOOLS = ['elo_setup']
32
33
  if (!FREE_TOOLS.includes(name)) {
33
34
  try {
34
- await validateLicense()
35
+ assertLicensePresent()
35
36
  } catch (err) {
36
37
  return {
37
38
  content: [{ type: 'text', text: err.message }],
@@ -1,24 +1,11 @@
1
1
  // Skills de criação de conteúdo: post, reels, stories, legenda, hooks
2
+ // Tools "finas": só metadados. O template do método vem da API autenticada.
2
3
 
3
- import { getBrandConfig, formatBrandContext } from '../brand.js'
4
+ import { makeTool } from '../make-tool.js'
4
5
 
5
- function brandParam() {
6
- return {
7
- marca: { type: 'string', description: 'Slug da marca configurada (ex: minha-marca). Se omitido, usa a primeira marca salva.' }
8
- }
9
- }
10
-
11
- function resolveContext(marca) {
12
- const config = getBrandConfig(marca || 'minha-marca')
13
- if (!config) {
14
- return `⚠️ Marca "${marca || 'minha-marca'}" não configurada. Use elo_setup primeiro.`
15
- }
16
- return formatBrandContext(config)
17
- }
18
-
19
- // ─── POST ────────────────────────────────────────────────────────────────────
6
+ const marcaParam = { type: 'string', description: 'Slug da marca configurada (ex: minha-marca). Se omitido, usa a primeira marca salva.' }
20
7
 
21
- export const postTool = {
8
+ export const postTool = makeTool({
22
9
  name: 'elo_post',
23
10
  description: 'Cria um post completo para Instagram com estrutura ELO. Inclui: gancho, desenvolvimento, CTA, sugestão de formato, etapa C.R.I.A. e KPI.',
24
11
  inputSchema: {
@@ -27,64 +14,13 @@ export const postTool = {
27
14
  tema: { type: 'string', description: 'Tema ou assunto do post' },
28
15
  formato: { type: 'string', enum: ['carrossel', 'reels', 'foto', 'livre'], description: 'Formato preferido' },
29
16
  cria: { type: 'string', enum: ['C', 'R', 'I', 'A'], description: 'Etapa do C.R.I.A. (Conexão/Repertório/Identidade/Ação)' },
30
- ...brandParam()
17
+ marca: marcaParam
31
18
  },
32
19
  required: ['tema']
33
- },
34
- handler: async ({ tema, formato, cria, marca }) => {
35
- const ctx = resolveContext(marca)
36
- return {
37
- content: [{
38
- type: 'text',
39
- text: `${ctx}
40
-
41
- ---
42
-
43
- # INSTRUÇÃO — POST INSTAGRAM COM MÉTODO ELO
44
-
45
- Crie um post completo para Instagram sobre: **"${tema}"**
46
- ${formato ? `Formato preferido: ${formato}` : ''}
47
- ${cria ? `Etapa do C.R.I.A.: ${cria}` : ''}
48
-
49
- ## ESTRUTURA OBRIGATÓRIA
50
-
51
- ### 1. DIAGNÓSTICO ESTRATÉGICO (silencioso — não mostre para a usuária)
52
- - Qual etapa do C.R.I.A. este post serve?
53
- - Qual gatilho mental será ativado?
54
- - Qual nível de consciência do avatar?
55
-
56
- ### 2. GANCHO (primeira linha — máx 1 frase)
57
- Use a identidade verbal da marca. Provoque, gere identificação ou quebre expectativa.
58
- NUNCA comece com "Você sabia que..." ou "Hoje vou falar sobre..."
59
-
60
- ### 3. DESENVOLVIMENTO (3 a 5 parágrafos)
61
- - Alterne frases longas com frases curtíssimas de impacto
62
- - Inclua pelo menos 1 número, dado ou exemplo real
63
- - Use o vocabulário da marca
64
-
65
- ### 4. VIRADA
66
- Momento de reframe ou insight inesperado. A frase mais forte do post.
67
-
68
- ### 5. CTA (convite, não ordem)
69
- Alinhado com o objetivo de negócio. Nunca forçado.
70
-
71
- ### 6. LEGENDA TÉCNICA
72
- - Sugestão de formato visual
73
- - 3 a 5 hashtags estratégicas (não genéricas)
74
- - Melhor horário de publicação
75
-
76
- ### 7. MÉTRICAS
77
- - Etapa C.R.I.A.: [C/R/I/A]
78
- - KPI principal: [alcance / salvamentos / cliques / conversões]
79
- - Gatilho ativado: [nome do gatilho]`
80
- }]
81
- }
82
20
  }
83
- }
84
-
85
- // ─── REELS ───────────────────────────────────────────────────────────────────
21
+ })
86
22
 
87
- export const reelsTool = {
23
+ export const reelsTool = makeTool({
88
24
  name: 'elo_reels',
89
25
  description: 'Cria roteiro completo de Reels (30 segundos) com cortes, legenda e análise estratégica.',
90
26
  inputSchema: {
@@ -92,62 +28,13 @@ export const reelsTool = {
92
28
  properties: {
93
29
  tema: { type: 'string', description: 'Tema do Reels' },
94
30
  estilo: { type: 'string', enum: ['dialogo_interno', 'quebra_padrao', 'broll_narracao', 'livre'], description: 'Estilo visual' },
95
- ...brandParam()
31
+ marca: marcaParam
96
32
  },
97
33
  required: ['tema']
98
- },
99
- handler: async ({ tema, estilo, marca }) => {
100
- const ctx = resolveContext(marca)
101
- return {
102
- content: [{
103
- type: 'text',
104
- text: `${ctx}
105
-
106
- ---
107
-
108
- # INSTRUÇÃO — ROTEIRO DE REELS 30s
109
-
110
- Tema: **"${tema}"**
111
- ${estilo ? `Estilo: ${estilo}` : ''}
112
-
113
- ## ESTRUTURA DO ROTEIRO
114
-
115
- ### GANCHO (0–3s)
116
- A primeira frase dita ou o texto na tela. Deve parar o scroll. Use a voz da marca.
117
-
118
- ### CORTE 1 (3–8s)
119
- Desenvolvimento inicial. Problema ou provocação.
120
-
121
- ### CORTE 2 (8–16s)
122
- Tensão ou dado surpreendente.
123
-
124
- ### CORTE 3 (16–24s)
125
- Virada / insight principal.
126
-
127
- ### FECHAMENTO (24–30s)
128
- CTA falado + texto na tela. Simples, direto.
129
-
130
- ---
131
-
132
- ## LEGENDA DO REELS
133
- (máx 150 caracteres — continua nos comentários se necessário)
134
-
135
- ## TEXTO NA TELA (overlays)
136
- Liste cada texto que aparece na tela, em ordem.
137
-
138
- ## ANÁLISE ESTRATÉGICA
139
- - Avatar ativado
140
- - Etapa C.R.I.A.
141
- - Gatilho principal
142
- - KPI esperado: [alcance / compartilhamentos / salvamentos]`
143
- }]
144
- }
145
34
  }
146
- }
147
-
148
- // ─── STORIES ─────────────────────────────────────────────────────────────────
35
+ })
149
36
 
150
- export const storiesTool = {
37
+ export const storiesTool = makeTool({
151
38
  name: 'elo_stories',
152
39
  description: 'Cria sequência de 5 stories com 5 ângulos diferentes, análise neurológica e score de impacto.',
153
40
  inputSchema: {
@@ -155,58 +42,13 @@ export const storiesTool = {
155
42
  properties: {
156
43
  tema: { type: 'string', description: 'Tema ou objetivo da sequência' },
157
44
  objetivo: { type: 'string', enum: ['engajamento', 'autoridade', 'venda', 'bastidor', 'livre'], description: 'Objetivo da sequência' },
158
- ...brandParam()
45
+ marca: marcaParam
159
46
  },
160
47
  required: ['tema']
161
- },
162
- handler: async ({ tema, objetivo, marca }) => {
163
- const ctx = resolveContext(marca)
164
- return {
165
- content: [{
166
- type: 'text',
167
- text: `${ctx}
168
-
169
- ---
170
-
171
- # INSTRUÇÃO — SEQUÊNCIA DE 5 STORIES
172
-
173
- Tema: **"${tema}"**
174
- ${objetivo ? `Objetivo: ${objetivo}` : ''}
175
-
176
- ## PARA CADA STORY (1 a 5):
177
-
178
- **Story [N] — [Nome do ângulo]**
179
- - **Tipo:** (texto / foto / vídeo / enquete / caixinha)
180
- - **Ângulo:** (provocação / humor / bastidor / dado / virada)
181
- - **Texto principal:** (exatamente o que vai escrito)
182
- - **Elemento interativo:** (enquete, caixinha, contagem, link — se aplicável)
183
- - **Transição:** como leva para o próximo story
184
-
185
- ---
186
-
187
- ## OS 5 ÂNGULOS OBRIGATÓRIOS
188
- 1. Provocação / identificação imediata
189
- 2. Dado ou prova inesperada
190
- 3. Bastidor ou história real
191
- 4. Humor ou ironia com sentido
192
- 5. Virada + CTA
193
-
194
- ---
195
-
196
- ## ANÁLISE NEUROLÓGICA
197
- Para cada story: qual mecanismo psicológico ativa? (curiosidade, identidade, contraste, pertencimento, urgência)
198
-
199
- ## SCORE DE IMPACTO
200
- Dê uma nota de 0 a 10 para a sequência em: Retenção / Engajamento / Conversão
201
- Justifique cada nota em uma linha.`
202
- }]
203
- }
204
48
  }
205
- }
49
+ })
206
50
 
207
- // ─── LEGENDA ─────────────────────────────────────────────────────────────────
208
-
209
- export const legendaTool = {
51
+ export const legendaTool = makeTool({
210
52
  name: 'elo_legenda',
211
53
  description: 'Cria ou aprimora a legenda de um post pronto. Adiciona: linha de impacto, expansão, momento aha e CTA com sentido.',
212
54
  inputSchema: {
@@ -214,52 +56,13 @@ export const legendaTool = {
214
56
  properties: {
215
57
  conteudo: { type: 'string', description: 'O post ou conteúdo que precisa de legenda' },
216
58
  objetivo: { type: 'string', description: 'Objetivo da legenda: engajar, vender, salvar, compartilhar' },
217
- ...brandParam()
59
+ marca: marcaParam
218
60
  },
219
61
  required: ['conteudo']
220
- },
221
- handler: async ({ conteudo, objetivo, marca }) => {
222
- const ctx = resolveContext(marca)
223
- return {
224
- content: [{
225
- type: 'text',
226
- text: `${ctx}
227
-
228
- ---
229
-
230
- # INSTRUÇÃO — LEGENDA COM IMPACTO
231
-
232
- Conteúdo recebido:
233
- """
234
- ${conteudo}
235
- """
236
-
237
- ${objetivo ? `Objetivo: ${objetivo}` : ''}
238
-
239
- ## ESTRUTURA DA LEGENDA
240
-
241
- ### 1. LINHA DE IMPACTO (primeira linha — máx 1 frase)
242
- A frase que aparece antes do "ver mais". Deve parar o scroll. Use a voz da marca.
243
-
244
- ### 2. EXPANSÃO (2 a 4 parágrafos)
245
- Aprofunda o tema. Alterna frases longas com curtas. Inclui 1 dado ou exemplo real.
246
-
247
- ### 3. MOMENTO AHA
248
- A frase mais forte. O insight que a pessoa vai querer salvar ou copiar.
249
-
250
- ### 4. CTA COM SENTIDO
251
- Convite genuíno, não ordem. Alinhado ao objetivo de negócio.
252
-
253
- ### 5. HASHTAGS
254
- 5 hashtags: 2 de nicho + 2 de tema + 1 de comunidade. Sem hashtags genéricas (#vida, #amor).`
255
- }]
256
- }
257
62
  }
258
- }
259
-
260
- // ─── HOOKS ───────────────────────────────────────────────────────────────────
63
+ })
261
64
 
262
- export const hooksTool = {
65
+ export const hooksTool = makeTool({
263
66
  name: 'elo_hooks',
264
67
  description: 'Gera 20 hooks para um tema. Você escolhe os melhores e recebe 10 refinados com variações.',
265
68
  inputSchema: {
@@ -267,53 +70,8 @@ export const hooksTool = {
267
70
  properties: {
268
71
  tema: { type: 'string', description: 'Tema para gerar hooks' },
269
72
  formato: { type: 'string', enum: ['reels', 'carrossel', 'post', 'stories', 'qualquer'], description: 'Formato de destino' },
270
- ...brandParam()
73
+ marca: marcaParam
271
74
  },
272
75
  required: ['tema']
273
- },
274
- handler: async ({ tema, formato, marca }) => {
275
- const ctx = resolveContext(marca)
276
- return {
277
- content: [{
278
- type: 'text',
279
- text: `${ctx}
280
-
281
- ---
282
-
283
- # INSTRUÇÃO — GERADOR DE HOOKS VIRAIS
284
-
285
- Tema: **"${tema}"**
286
- ${formato ? `Formato: ${formato}` : ''}
287
-
288
- ## REGRAS DOS HOOKS
289
- - NUNCA use "descubra" — é fraco e genérico
290
- - Prefira perguntas desconfortáveis a afirmações seguras
291
- - Use a voz e os bordões da marca
292
- - Cada hook deve parar o scroll em 0,3 segundos
293
- - Varie os padrões: pergunta / afirmação ousada / dado / paradoxo / inversão
294
-
295
- ## FASE 1 — GERAÇÃO
296
-
297
- Crie 20 hooks para o tema. Organize em 4 categorias de 5:
298
-
299
- **Pergunta Desconfortável (5 hooks)**
300
- **Afirmação Polarizante (5 hooks)**
301
- **Dado ou Número Inesperado (5 hooks)**
302
- **Inversão ou Paradoxo (5 hooks)**
303
-
304
- ---
305
-
306
- Após apresentar os 20, peça à usuária para escolher os favoritos.
307
- Com as escolhas, refine os 10 melhores com variações (versão A e B para cada).
308
-
309
- ## FASE 2 — REFINAMENTO (após escolha da usuária)
310
-
311
- Para cada hook escolhido:
312
- - **Versão A:** mais provocativa
313
- - **Versão B:** mais intrigante
314
- - **Melhor formato de destino:** reels / carrossel / stories
315
- - **Gatilho ativado:** curiosidade / identidade / contraste / urgência`
316
- }]
317
- }
318
76
  }
319
- }
77
+ })