sapiens-mcp 1.32.0 → 1.32.1

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { zodToJsonSchema } from "zod-to-json-schema";
6
6
  import { pipeline, pipelineSchema } from "./tools/pipeline.js";
7
7
  import { image, imageSchema } from "./tools/image.js";
@@ -34,19 +34,21 @@ import { reference, referenceSchema } from "./tools/reference.js";
34
34
  import { trilhas, trilhasSchema } from "./tools/trilhas.js";
35
35
  import { describeConvexError } from "./convexClient.js";
36
36
  import { MCP_VERSION } from "./version.js";
37
+ import { getCachedTier, onTierVisibilityChange, probeTierInBackground, } from "./tier.js";
38
+ import { getPrompt, listPrompts } from "./prompts.js";
37
39
  const TOOLS = {
38
40
  sapiens_pipeline: {
39
- description: "CRUD do content pipeline Sapiens (sources/productions/publishables). Sub-actions: list_sources, list_articles (use includeDrafts pra incluir drafts; onlyAvailable pra esconder os já virados em source), get_source, get_production, list_versions, add_article_as_source, create_draft_article_and_source (seed), create_production (sourceId+format → productionId draft), update_production (substitui payload, opcionalmente muda status), finalize_production (cria publishable v1, v2... com snapshot), remove_production, remove_source, set_source_done, update_source_notes, restore_version (volta payload duma versão antiga), propose_mega_grafico_plan (granular: só gera plano via Gemini, devolve fullPrompt+spec), run_mega_grafico_full (ONE-SHOT, recomendado: cria production+propõe plano+gera imagem+aplica selo Sapiens+finaliza publishable numa chamada só), generate_carousel (gera um carrossel editorial standalone a partir de brief OU articleId — 7-9 slides na voz Sapiens + imagens do banco; devolve id + url do editor pro humano abrir, ajustar e exportar; admin-only, cobra Sinapses, reembolsa se falhar). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente (menos drift). Idempotente só na production (passa productionId pra reusar a MESMA row), mas cada run RE-GERA a imagem e cobra de novo (~900 Sinapses): não é grátis re-rodar. OBRIGATÓRIO perguntar ao user antes se withHelen=true (cartoon Helen interage com tema, ~15-25% do poster) ou false (poster 100% diagramático). Custo ~900-1000 sinapses por geração. run_mega_grafico_full e generate_carousel são SÍNCRONAS e pesadas (Gemini + imagem): podem passar do teto de ~120s do cliente e voltar 'Timeout' MESMO tendo criado a production e cobrado, então cheque /dashboard/admin/content antes de repetir. Use skipFinalize=true se quiser deixar production em 'ready' pro admin revisar antes de publishable. Payload livre por formato — chame sapiens_meta action=formats pra ver schemas sugeridos.",
41
+ description: "CRUD do content pipeline Sapiens (sources/productions/publishables). Sub-actions: list_sources, list_articles (use includeDrafts pra incluir drafts; onlyAvailable pra esconder os já virados em source), get_source, get_production, list_versions, add_article_as_source, create_draft_article_and_source (seed), create_production (sourceId+format → productionId draft), update_production (substitui payload, opcionalmente muda status), finalize_production (cria publishable v1, v2... com snapshot), remove_production, remove_source, set_source_done, update_source_notes, restore_version (volta payload duma versão antiga), set_publishable_title (renomeia um publishable), backfill_via (rotula em lote o campo 'via' das productions antigas; dryRun=true só lista), propose_mega_grafico_plan (granular: só gera plano via Gemini, devolve fullPrompt+spec), run_mega_grafico_full (ONE-SHOT, recomendado: cria production+propõe plano+gera imagem+aplica selo Sapiens+finaliza publishable numa chamada só), generate_carousel (gera um carrossel editorial standalone a partir de brief OU articleId — 7-9 slides na voz Sapiens + imagens do banco; devolve id + url do editor pro humano abrir, ajustar e exportar; admin-only, cobra Sinapses, reembolsa se falhar). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente (menos drift). Idempotente só na production (passa productionId pra reusar a MESMA row), mas cada run RE-GERA a imagem e cobra de novo (~900 Sinapses): não é grátis re-rodar. OBRIGATÓRIO perguntar ao user antes se withHelen=true (cartoon Helen interage com tema, ~15-25% do poster) ou false (poster 100% diagramático). Custo ~900-1000 sinapses por geração. run_mega_grafico_full e generate_carousel são SÍNCRONAS e pesadas: vale a REGRA DO TIMEOUT (podem cobrar mesmo voltando 'Timeout'; cheque /dashboard/admin/content antes de repetir). Use skipFinalize=true se quiser deixar production em 'ready' pro admin revisar antes de publishable. Payload livre por formato — chame sapiens_meta action=formats pra ver schemas sugeridos.",
40
42
  schema: pipelineSchema,
41
43
  handler: pipeline,
42
44
  },
43
45
  sapiens_image: {
44
- description: "Operações de imagem via Sapiens (Gemini, Azure gpt-image-2, Grok, Veo). Sub-actions: 'generate' (gera imagem completa imediato — prompt+model+aspectRatio+size; suporta mode=edit/variation e MULTI-REFERÊNCIA: combine até 4 imagens como referência numa geração só, igual ao modal 'Selecionar Referência' do web — via referenceImageUrls (sua galeria + Acervo + personagens públicos de sapiens_character) e/ou sourceImageIds (ids da sua galeria); refs valem pros modelos robustos nano-banana-2/gpt-image-2-*/grok-2-image*), 'request_generation' (v1.7, cria APENAS row pendente em generatedImages + debita créditos — pra modelos sapiens-video-* ANTES de sapiens_shorts/sapiens_video; whitelist, rate limit 3/min), 'compose' (v1.8, combina persona+screen via Gemini pra app-demo Shorts; 25 sinapses, rate limit 10/min). generate=image one-shot, request_generation=criar row video, compose=montar start frame app-demo. TEMPLATE (v1.17): passe templateSlug numa generate pra usar um super-prompt travado da casa — o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha estilo+fundo+enquadramento+ref de traço. 'retrato-sapiens-v1' = retrato editorial cartoon de um personagem no grid verde Sapiens (mesma 'mão' dos artigos); sem ref própria, injeta a Helen como âncora de traço (passar referenceImageUrls troca quem aparece). Mutuamente exclusivo com brandSlug. Sub-action 'models' (sem custo, sem login): lista o catálogo vivo (modelos ativos + preço atual com override admin + maxResolution + se aceita referência) pra descobrir modelo/preço em vez de chutar. NOTA: generate é SÍNCRONA e cobra ao concluir; modelo pesado (Pro, gpt-image-2-high, Grok quality, 2K/4K) pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e cobrado, então na dúvida cheque sapiens_gallery action=list antes de repetir (evita cobrança dupla).",
46
+ description: "Operações de imagem via Sapiens (Gemini, Azure gpt-image-2, Grok, Veo). Sub-actions: 'generate' (gera imagem completa imediato — prompt+model+aspectRatio+size; suporta mode=edit/variation e MULTI-REFERÊNCIA: combine até 4 imagens como referência numa geração só, igual ao modal 'Selecionar Referência' do web — via referenceImageUrls (sua galeria + Acervo + personagens públicos de sapiens_character) e/ou sourceImageIds (ids da sua galeria); refs valem pros modelos robustos nano-banana-2/gpt-image-2-*/grok-2-image*), 'request_generation' (cria APENAS row pendente em generatedImages + debita créditos — pra modelos sapiens-video-* ANTES de sapiens_shorts/sapiens_video; whitelist, rate limit 3/min), 'compose' (combina persona+screen via Gemini pra app-demo Shorts; 25 sinapses, rate limit 10/min). generate=image one-shot, request_generation=criar row video, compose=montar start frame app-demo. TEMPLATE: passe templateSlug numa generate pra usar um super-prompt travado da casa — o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha estilo+fundo+enquadramento+ref de traço. 'retrato-sapiens-v1' = retrato editorial cartoon de um personagem no grid verde Sapiens (mesma 'mão' dos artigos); sem ref própria, injeta a Helen como âncora de traço (passar referenceImageUrls troca quem aparece). Mutuamente exclusivo com brandSlug. Sub-action 'models' (sem custo, sem login): lista o catálogo vivo (modelos ativos + preço atual com override admin + maxResolution + se aceita referência) pra descobrir modelo/preço em vez de chutar. NOTA: generate é SÍNCRONA e cobra ao concluir; modelo pesado (Pro, gpt-image-2-high, Grok quality, 2K/4K) cai na REGRA DO TIMEOUT (cheque sapiens_gallery action=list antes de repetir, evita cobrança dupla).",
45
47
  schema: imageSchema,
46
48
  handler: image,
47
49
  },
48
50
  sapiens_meta: {
49
- description: "Utilitários transversais: start (porta de entrada do primeiro contato — sem login ensina a conectar, com login mostra saldo/tier + primeiros poderes com exemplo pronto + 'comece por aqui'), login (conecta a conta com o código de sapiensinteticos.com/conectar-claude, salva sessão de 30 dias localmente), logout, whoami (tier user/admin + saldo + email), credits (saldo agregado), subscription (v1.2 — plan + status + saldo por bucket subscription/grants/free + warnings low/critical), formats (schemas por formato), health (inclui a versão do MCP), version (qual versão do sapiens-mcp está REALMENTE rodando + se é a última do npm; não exige login; use pra saber se o client pegou a versão nova ou ficou preso em cache do npx), app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
51
+ description: "Utilitários transversais: start (porta de entrada do primeiro contato — sem login ensina a conectar, com login mostra saldo/tier + primeiros poderes com exemplo pronto + 'comece por aqui'), login (conecta a conta com o código de sapiensinteticos.com/conectar-claude, salva sessão de 30 dias localmente), logout, whoami (tier user/admin + saldo + email), credits (saldo agregado), subscription (plan + status + saldo por bucket subscription/grants/free + warnings low/critical), formats (schemas por formato), health (inclui a versão do MCP), version (qual versão do sapiens-mcp está REALMENTE rodando + se é a última do npm; não exige login; use pra saber se o client pegou a versão nova ou ficou preso em cache do npx), app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
50
52
  schema: metaSchema,
51
53
  handler: meta,
52
54
  },
@@ -66,22 +68,22 @@ const TOOLS = {
66
68
  handler: community,
67
69
  },
68
70
  sapiens_article: {
69
- description: "CRUD direto de artigos do blog Sapiens (v1.2). Sub-actions: get (by slug, retorna doc completo pra edit local), update (patch em title/excerpt/tldr/content/tags/etc + VISUAIS: thumbnailUrl capa webp, ogImageUrl JPEG do preview social, bodyImages array das ilustrações inline, conceptMap mapa visual — pra recapear um artigo num novo estilo; NÃO toca status/column/format), publish (status='published', set publishedAt), unpublish (volta pra draft), delete (irreversível). Pra criar artigo novo use sapiens_quote_pop (quote ou pop) ou sapiens_pipeline action=create_draft_article_and_source (cru, vira source).",
71
+ description: "CRUD direto de artigos do blog Sapiens. Sub-actions: get (by slug, retorna doc completo pra edit local), update (patch em title/excerpt/tldr/content/tags/etc + VISUAIS: thumbnailUrl capa webp, ogImageUrl JPEG do preview social, bodyImages array das ilustrações inline, conceptMap mapa visual — pra recapear um artigo num novo estilo; NÃO toca status/column/format), publish (status='published', set publishedAt), unpublish (volta pra draft), delete (irreversível), ensure_visuals (gera banner/ilustrações inline/conceptMap que faltam no artigo; idempotente, pula o que existe; ~1700 Sinapses num artigo pelado, forceBanner/forceInline/forceConceptMap regeram). Pra criar artigo novo use sapiens_quote_pop (quote ou pop) ou sapiens_pipeline action=create_draft_article_and_source (cru, vira source).",
70
72
  schema: articleSchema,
71
73
  handler: article,
72
74
  },
73
75
  sapiens_write: {
74
- description: "Artigos self-serve do PRÓPRIO usuário (qualquer conta logada, não só admin) — espaço pessoal, aparece em /u/<username>, NÃO é o blog editorial. Sub-actions: generate (gera 1 artigo na voz Sapiens a partir de brief livre, ou reescrevendo um artigo publicado/texto teu; custa 400 Sinapses, reembolsa se falhar; salva como rascunho. Capa: por padrão gera uma capa-cortesia grátis; se você JÁ tem a imagem (gerou via sapiens_image, ou o artigo é sobre ela), passe coverImageId (id da tua galeria) ou coverImageUrl (host Sapiens) pra ELA virar a capa em vez da cortesia), list (teus artigos), get (1 artigo teu por id, corpo completo), update (edita title/content/excerpt/tldr), publish (publish=true publica no teu perfil, false volta pra rascunho). Identidade vem do sessionToken; cobra as Sinapses do dono do token. Pra blog editorial curado (owner-only) use sapiens_article. NOTA (generate): é SÍNCRONA (texto + capa) e pode passar do teto de ~120s do cliente, voltando 'Timeout' MESMO tendo gerado e cobrado 400 (o artigo fica salvo como rascunho). Como não tem idempotência, repetir às cegas gera um 2º rascunho e cobra 400 de novo: cheque sapiens_write action=list antes de repetir.",
76
+ description: "Artigos self-serve do PRÓPRIO usuário (qualquer conta logada, não só admin) — espaço pessoal, aparece em /u/<username>, NÃO é o blog editorial. Sub-actions: generate (gera 1 artigo na voz Sapiens a partir de brief livre, ou reescrevendo um artigo publicado/texto teu; custa 400 Sinapses, reembolsa se falhar; salva como rascunho. Capa: por padrão gera uma capa-cortesia grátis; se você JÁ tem a imagem (gerou via sapiens_image, ou o artigo é sobre ela), passe coverImageId (id da tua galeria) ou coverImageUrl (host Sapiens) pra ELA virar a capa em vez da cortesia), list (teus artigos), get (1 artigo teu por id, corpo completo), update (edita title/content/excerpt/tldr), publish (publish=true publica no teu perfil, false volta pra rascunho). Identidade vem do sessionToken; cobra as Sinapses do dono do token. Pra blog editorial curado (owner-only) use sapiens_article. NOTA (generate): é SÍNCRONA (texto + capa) e cai na REGRA DO TIMEOUT; sem idempotência, repetir às cegas cria um 2º rascunho e cobra 400 de novo (cheque action=list antes; o artigo do timeout fica salvo como rascunho).",
75
77
  schema: writeSchema,
76
78
  handler: write,
77
79
  },
78
80
  sapiens_quote_pop: {
79
- description: "Publica curado da Coluna Sapiens (publish_quote), Coluna Repertório (publish_pop) ou Educativo (publish_educativo) via session token (v1.2). publish_quote: cria entry com column='sapiens', exige objeto quote completo (text/author/sourceWork/license/referenceImage/flowImage). publish_pop: cria entry com column='repertorio' format='pop-article', exige popReference{featuredItemId,relatedItemIds?,lensTheme?}. publish_educativo: cria artigo derivado de aula (Trilhas → Blog), exige educativeReference{sourceLessonId,angle?,partNumber?,totalParts?}. Default status='draft' (admin revisa em /dashboard/admin/...); publishNow=true publica direto.",
81
+ description: "Publica curado da Coluna Sapiens (publish_quote), Coluna Repertório (publish_pop) ou Educativo (publish_educativo) via session token. publish_quote: cria entry com column='sapiens', exige objeto quote completo (text/author/sourceWork/license/referenceImage/flowImage). publish_pop: cria entry com column='repertorio' format='pop-article', exige popReference{featuredItemId,relatedItemIds?,lensTheme?}. publish_educativo: cria artigo derivado de aula (Trilhas → Blog), exige educativeReference{sourceLessonId,angle?,partNumber?,totalParts?}. Default status='draft' (admin revisa em /dashboard/admin/...); publishNow=true publica direto.",
80
82
  schema: quotePopSchema,
81
83
  handler: quotePop,
82
84
  },
83
85
  sapiens_search: {
84
- description: "Busca substring case-insensitive em title/excerpt/tldr/slug/tags dos artigos (v1.2). Filtros opcionais: column ('sapiens'/'repertorio'), format ('short'/'essay'/'pop-article'), status ('draft'/'published'/'archived'), tag exato. Default limit 30 (max 100). Use pra achar slug/id de artigo pré-existente antes de editar/publicar/deletar via sapiens_article.",
86
+ description: "Busca substring case-insensitive em title/excerpt/tldr/slug/tags dos artigos. Filtros opcionais: column ('sapiens'/'repertorio'), format ('short'/'essay'/'pop-article'), status ('draft'/'published'/'archived'), tag exato. Default limit 30 (max 100). Use pra achar slug/id de artigo pré-existente antes de editar/publicar/deletar via sapiens_article.",
85
87
  schema: searchSchema,
86
88
  handler: search,
87
89
  },
@@ -101,22 +103,22 @@ const TOOLS = {
101
103
  handler: helen,
102
104
  },
103
105
  sapiens_musicator: {
104
- description: "Musicator — loop completo de música via voz Sapiens, sem precisar da UI (qualquer logado; lyrics 300 / render 3000 Sinapses). Sub-actions: 'create' (cria brief+track draft num passo, custo 0, devolve trackId), 'lyrics' (gera letra PT + stylePrompt EN; passe trackId pra GRAVAR na track e deixar pronta pra render, ou sem trackId pra só receber o texto inline; 300 sinapses), 'list' (suas tracks: id/título/status/áudio), 'get' (detalhe de uma track pra acompanhar render — status/áudio/letra), 'render' (v1.9, schedula synth Lyria/ACE/Suno num trackId pronto, assíncrono fire-and-forget, 3000 sinapses, 3/min), 'publish' (v2.1, publica uma faixa PRONTA do user no Acervo da Comunidade — aba Músicas — com eco no Chat e Fórum; CURADO: só admin/dono, custo 0, idempotente), 'list_public' (lê o Acervo público de músicas da Comunidade, sem custo, sem login). Fluxo cheio: create → lyrics(trackId) → render → get (poll status: rendering → ready/failed) → publish (dono). Tudo escopado por dono (só mexe nas suas tracks).",
106
+ description: "Musicator — loop completo de música via voz Sapiens, sem precisar da UI (qualquer logado; lyrics 300 / render 3000 Sinapses). Sub-actions: 'create' (cria brief+track draft num passo, custo 0, devolve trackId), 'lyrics' (gera letra PT + stylePrompt EN; passe trackId pra GRAVAR na track e deixar pronta pra render, ou sem trackId pra só receber o texto inline; 300 sinapses), 'list' (suas tracks: id/título/status/áudio), 'get' (detalhe de uma track pra acompanhar render — status/áudio/letra), 'render' (schedula synth Lyria/ACE/Suno num trackId pronto, assíncrono fire-and-forget, 3000 sinapses, 3/min), 'publish' (publica uma faixa PRONTA do user no Acervo da Comunidade — aba Músicas — com eco no Chat e Fórum; CURADO: só admin/dono, custo 0, idempotente), 'list_public' (lê o Acervo público de músicas da Comunidade, sem custo, sem login). Fluxo cheio: create → lyrics(trackId) → render → get (poll status: rendering → ready/failed) → publish (dono). Tudo escopado por dono (só mexe nas suas tracks).",
105
107
  schema: musicatorSchema,
106
108
  handler: musicator,
107
109
  },
108
110
  sapiens_shorts: {
109
- description: "Sapiens Shorts — render vertical 9:16 via VEO com brief structured (v1.5, admin-only). Sub-action: render. Args: imageId (persona pré-existente em generatedImages, descubra via sapiens_gallery), styleId ('ugc'/'unboxing'/'app-demo'/'reflexao'), brief (product+hook+shots+vibe), references opcionais. render é ASSÍNCRONO: volta na hora com {imageId, status:'rendering', url:null}, e você acompanha com sapiens_video action=status imageId=<id> até status='completed' (traz a url VEO, expiração curta, baixe logo) ou 'error'. Pré-requisito: o imageId precisa ter row em generatedImages do user da sessão e cost definido. Pra criar row, use a UI primeiro (v1.6 vai cobrir requestGeneration via MCP).",
111
+ description: "Sapiens Shorts — render vertical 9:16 via VEO com brief structured (admin-only). Sub-action: render. Args: imageId (persona pré-existente em generatedImages, descubra via sapiens_gallery), styleId ('ugc'/'unboxing'/'app-demo'/'reflexao'), brief (product+hook+shots+vibe), references opcionais. render é ASSÍNCRONO: volta na hora com {imageId, status:'rendering', url:null}, e você acompanha com sapiens_video action=status imageId=<id> até status='completed' (traz a url VEO, expiração curta, baixe logo) ou 'error'. Pré-requisito: o imageId precisa ter row em generatedImages do user da sessão e cost definido. Pra criar a row sem passar pela UI: sapiens_image action=request_generation (modelos sapiens-video-*).",
110
112
  schema: shortsSchema,
111
113
  handler: shorts,
112
114
  },
113
115
  sapiens_video: {
114
- description: "Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as Sinapses da sua conta). Sub-action 'create' (recomendada): escolhe modelo + config e gera num call (cria a row + renderiza). Modelos: 'sapiens-video-seedance' (Seedance 2.0, cena+áudio nativo, 4-15s, 480/720/1080p, t2v/i2v), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-wan' (WAN 2.5, imagem que fala/canta com áudio+lip-sync, 5/10s, i2v), 'sapiens-video-kling-motion' (Motion transfer: passa o movimento de um vídeo pra uma imagem, PRECISA de pessoa com tronco visível na imagem E no vídeo), 'sapiens-video-shot-mimic' (Shot Mimic: recria o plano/câmera/cortes de um vídeo de referência como cena nova), 'sapiens-video-omni' (Gemini Omni: texto vira vídeo 10s 720p com áudio nativo; NÃO aceita mídia do user, ignora references/durationSec/resolution; editOfImageId aponta um vídeo Omni seu e o prompt edita a MESMA cena, preservando câmera e ambiente), 'sapiens-video-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio. FRAME INICIAL/FINAL POR REFERÊNCIA (recomendado): startImageId/endImageId (id da sua galeria) ou startImageUrl/endImageUrl (url de galeria/Acervo/personagem) — resolvidos server-side igual à imagem, descubra via sapiens_reference. Alternativa base64: references (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Suporte a frame final varia por modelo. Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}. VITRINE (sem custo): sub-action 'demos' lista os SEUS demo films (kind=demo do Estúdio de Vídeo) com slug + estado de vitrine; sub-action 'showcase' põe/tira um demo (por slug) do mini-cinema da /conectar-claude, com showcaseTag (chip de capacidade) e showcaseOrder (ordem asc). Fluxo: 'demos' pra achar o slug, depois 'showcase' com showcase=true. Só entra na vitrine pública se for a conta da casa. create é ASSÍNCRONA: cria o row, debita e volta NA HORA com {imageId, status:'rendering', cost} (não espera o render, que leva de segundos a minutos). Acompanhe com a sub-action 'status' (imageId) até status='completed' (traz a url) ou 'error'/'blocked'. NÃO chame create de novo enquanto renderiza (cria outro vídeo e cobra de novo); falha de provider refunda sozinha. Sub-action 'models' (sem custo, sem login): lista os modelos de vídeo ativos + preço-piso + config (durações/resoluções) + disponibilidade (Omni depende de env).",
116
+ description: "Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as Sinapses da sua conta). Sub-action 'create' (recomendada): escolhe modelo + config e gera num call (cria a row + renderiza). Modelos: 'sapiens-video-seedance' (Seedance 2.0, cena+áudio nativo, 4-15s, 480/720/1080p, t2v/i2v), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-wan' (WAN 2.5, imagem que fala/canta com áudio+lip-sync, 5/10s, i2v), 'sapiens-video-kling-motion' (Motion transfer: passa o movimento de um vídeo pra uma imagem, PRECISA de pessoa com tronco visível na imagem E no vídeo), 'sapiens-video-shot-mimic' (Shot Mimic: recria o plano/câmera/cortes de um vídeo de referência como cena nova), 'sapiens-video-omni' (Gemini Omni: texto vira vídeo 10s 720p com áudio nativo; NÃO aceita mídia do user, ignora references/durationSec/resolution; editOfImageId aponta um vídeo Omni seu e o prompt edita a MESMA cena, preservando câmera e ambiente), 'sapiens-video-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio. FRAME INICIAL/FINAL POR REFERÊNCIA (recomendado): startImageId/endImageId (id da sua galeria) ou startImageUrl/endImageUrl (url de galeria/Acervo/personagem) — resolvidos server-side igual à imagem, descubra via sapiens_reference. Alternativa base64: references (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Suporte a frame final varia por modelo. Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}. VITRINE (sem custo): sub-action 'demos' lista os SEUS demo films (kind=demo do Estúdio de Vídeo) com slug + estado de vitrine; sub-action 'showcase' põe/tira um demo (por slug) do mini-cinema da /conectar-claude, com showcaseTag (chip de capacidade) e showcaseOrder (ordem asc). Fluxo: 'demos' pra achar o slug, depois 'showcase' com showcase=true. Só entra na vitrine pública se for a conta da casa. create é ASSÍNCRONA: cria o row, debita e volta NA HORA com {imageId, status:'rendering', cost} (não espera o render, que leva de segundos a minutos). Acompanhe com a sub-action 'status' (imageId) até status='completed' (traz a url) ou 'error'/'blocked'. NÃO chame create de novo enquanto renderiza (cria outro vídeo e cobra de novo); falha de provider refunda sozinha. SOM: 'sonorize' (imageId de vídeo SEU completed + prompt do som da cena) gera uma VARIANTE nova com trilha sincronizada (20 Sinapses/s, o original fica intacto; sonorize sempre o original, nunca uma variante). ADMIN: 'shadows' (videoUrl + title) extrai a sombra/depth-map de um vídeo pro Acervo como driving reutilizável; 'shadows-list' lista as sombras prontas. Sub-action 'models' (sem custo, sem login): lista os modelos de vídeo ativos + preço-piso + config (durações/resoluções) + disponibilidade (Omni depende de env).",
115
117
  schema: videoSchema,
116
118
  handler: video,
117
119
  },
118
120
  sapiens_stock_audio: {
119
- description: "Banco de som da casa: trilha pronta E efeito sonoro (tabela stockAudio). Sub-actions de leitura (públicas, sem auth): categories (lista os moods/usos), list (busca com filtros mood/albumSlug/durationMax/search; kind='sfx' traz os EFEITOS: whoosh, clique, impacto, ambiência, foley — devolve {count, items} com title/url/durationSeconds/tags), get (1 item por audioId). Use pra puxar trilha/efeito pronto: pega a `url` e usa direto no ffmpeg. Não achou o efeito? generate (COBRA Sinapses, exige login) cria um novo por texto: prompt + durationSeconds (1-15, default 5) + provider ('mirelo' padrão 30 Sinapses/s mín 60 | 'elevenlabs' premium 60/s mín 120), assíncrono — acompanhe com generation-status (generationId) até 'ready' (audioUrl; o efeito também entra no acervo kind=sfx) ou 'failed' (Sinapses reembolsadas). Efeito é CURTO (1-15s): música/trilha nova é no sapiens_musicator. Mood disponíveis: calmo, intenso, narrativo, épico, sombrio.",
121
+ description: "Banco de som da casa: trilha pronta E efeito sonoro (tabela stockAudio). Sub-actions de leitura (públicas, sem auth): categories (lista os moods/usos), list (busca com filtros mood/albumSlug/durationMax/search; kind='sfx' traz os EFEITOS: whoosh, clique, impacto, ambiência, foley — devolve {count, items} com title/url/durationSeconds/tags), get (1 item por audioId). Use pra puxar trilha/efeito pronto: pega a `url` e usa direto no ffmpeg. Não achou o efeito? generate (COBRA Sinapses, exige login) cria um novo por texto: prompt + durationSeconds (1-15, default 5) + provider ('mirelo' padrão 30 Sinapses/s mín 60 | 'elevenlabs' premium 60/s mín 120) + promptInfluence opcional (0..1, só elevenlabs: fidelidade ao texto, default 0.3), assíncrono — acompanhe com generation-status (generationId) até 'ready' (audioUrl; o efeito também entra no acervo kind=sfx) ou 'failed' (Sinapses reembolsadas). Efeito é CURTO (1-15s): música/trilha nova é no sapiens_musicator. Mood disponíveis: calmo, intenso, narrativo, épico, sombrio.",
120
122
  schema: stockAudioSchema,
121
123
  handler: stockAudio,
122
124
  },
@@ -171,7 +173,7 @@ const TOOLS = {
171
173
  handler: atlas,
172
174
  },
173
175
  sapiens_reference: {
174
- description: "O 'popup global de referência' do Sapiens — espelha o modal 'Selecionar Referência' do gerador web: um lugar só pra navegar os bancos e pegar o que vira referência em imagem/vídeo. READ-ONLY. Sub-action 'browse' + bucket: 'history' (suas imagens recentes, privadas+públicas), 'favorites' (imagens que você curtiu, só as suas), 'videos' (seus vídeos / Meus Vídeos), 'acervo' (stock + comunidade públicos; aceita term=busca e source=all|stock|community), 'characters' (personagens; mode=mine [default, inclui rascunhos] ou public [Explorar]). Paginado (page/limit, default 20, máx 50; use hasMore). Itens normalizados: imagem PRÓPRIA (history/favorites) traz imageId + url (use imageId em sapiens_image sourceImageIds ou sapiens_video startImageId/endImageId; ou a url em referenceImageUrls); acervo e characters são públicos/de terceiros, use a url (characters trazem mainImageUrl + imageUrls + characterId) em referenceImageUrls / startImageUrl / endImageUrl, NÃO em sourceImageIds. Personagens públicos também têm porta dedicada em sapiens_character action=list_public.",
176
+ description: "O 'popup global de referência' do Sapiens — espelha o modal 'Selecionar Referência' do gerador web: um lugar só pra navegar os bancos e pegar o que vira referência em imagem/vídeo. READ-ONLY. Sub-action 'browse' + bucket: 'history' (suas imagens recentes, privadas+públicas), 'favorites' (imagens que você curtiu, só as suas), 'videos' (seus vídeos / Meus Vídeos), 'stock_video' (banco de B-roll da casa, público), 'acervo' (stock + comunidade públicos; aceita term=busca e source=all|stock|community), 'characters' (personagens; mode=mine [default, inclui rascunhos] ou public [Explorar]). Paginado (page/limit, default 20, máx 50; use hasMore). Itens normalizados: imagem PRÓPRIA (history/favorites) traz imageId + url (use imageId em sapiens_image sourceImageIds ou sapiens_video startImageId/endImageId; ou a url em referenceImageUrls); acervo e characters são públicos/de terceiros, use a url (characters trazem mainImageUrl + imageUrls + characterId) em referenceImageUrls / startImageUrl / endImageUrl, NÃO em sourceImageIds. Personagens públicos também têm porta dedicada em sapiens_character action=list_public.",
175
177
  schema: referenceSchema,
176
178
  handler: reference,
177
179
  },
@@ -186,52 +188,53 @@ const TOOLS = {
186
188
  // É a "skill que anda junto com o pacote": escrevo uma vez, vale pra todos os clients,
187
189
  // sem instalar nada. Cobre os tropeços reais (fluxo do musicator, model no vídeo, o
188
190
  // disjuntor anti-loop). Mantém curto de propósito: viaja em todo handshake.
189
- const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
190
-
191
- REGRA DE OURO:
192
- - PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
193
- - LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
194
- - Se um tool voltar erro de validação ("exige X", "falta Y"), LEIA o erro e refaça a chamada COM o que falta. NUNCA repita igual a chamada que falhou: 3 falhas seguidas no mesmo tool fazem o cliente marcar o servidor como "unreachable" por ~56s (disjuntor anti-loop). Aí parece que "o MCP caiu", quando foi só argumento faltando.
195
- - Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes.
196
- - "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
197
- - sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo.
198
-
199
- FLUXOS QUE NÃO PODEM ERRAR:
200
-
201
- Música (sapiens_musicator) é fluxo de 4 passos, EM ORDEM:
202
- 1. create exige title (≥3) + context (≥20 chars, o tema/ângulo) + direction (gênero/mood). Custo 0, devolve trackId.
203
- 2. lyrics passe trackId + context pra GRAVAR a letra na track (300 Sinapses). NUNCA chame lyrics sem title+context.
204
- 3. render passe o trackId pronto pra sintetizar o áudio (3000 Sinapses, assíncrono, 3/min).
205
- 4. get passe o trackId e fique polando o status até 'ready' (ou 'failed').
206
- Pular pro lyrics/render sem create, ou sem os campos, sempre falha.
207
-
208
- Efeito sonoro (sapiens_stock_audio): primeiro procure pronto (action=list kind=sfx, grátis). Não achou, action=generate: prompt + durationSeconds (1-15, default 5) + provider ('mirelo' padrão | 'elevenlabs' premium, custo por segundo). Assíncrono: devolve generationId, acompanhe com action=generation-status até 'ready' (audioUrl) ou 'failed' (reembolsa sozinho). Efeito é CURTO; música inteira é no sapiens_musicator, não aqui.
209
-
210
- Sonorizar clipe (sapiens_video action=sonorize): dá som a um vídeo SEU já gerado (status completed). Passe imageId + prompt descrevendo o som da cena (ambiente, materiais, impactos); sai uma VARIANTE nova com trilha sincronizada ao movimento (MMAudio, 20 Sinapses/s do clipe, mín 100), o original fica intacto. Acompanhe com action=status no imageId NOVO que o sonorize devolve. Não re-sonorize uma variante: sonorize sempre o original.
211
-
212
- Vídeo (sapiens_video) action=create EXIGE 'model': sapiens-video-seedance | kling | wan | kling-motion | lite | fast | quality | omni. Sem model, falha de cara. O 'omni' (Gemini Omni) gera clipe de 10s 720p com áudio nativo a partir de TEXTO (não aceita imagem/vídeo do user, ignora duração/resolução) e EDITA os próprios vídeos: passe editOfImageId com o imageId de um vídeo Omni já gerado e o prompt vira instrução de edição sobre a MESMA cena (troca item/personagem, preserva câmera e ambiente; cada edição debita como geração nova). É o caminho pra variações com continuidade: gera a base uma vez, edita N vezes. Vídeo é o mais caro: confirme com o usuário antes. action=shadows (ADMIN, 200 Sinapses/segundo, refund na falha): passa videoUrl (URL pública) + title (+ durationSec se souber, pra cobrar proporcional; sem ela, flat ~2000) e o servidor extrai a SOMBRA (mapa de profundidade) do vídeo e guarda no Acervo (Corpo) como deepshadow reutilizável (driving pro Shot Mimic/Kling Motion). Vitrine dos demos (sem custo): sapiens_video action=demos lista seus demo films com slug; action=showcase (slug + showcase=true/false + showcaseTag + showcaseOrder) cura o mini-cinema da /conectar-claude. FILMES DA CASA (Estúdio de Vídeo, tela /experimentos/films): são OUTRA coisa, três kinds na tabela videoSpecs: demo (UI clonada + câmera), aula-tour (telas reais + narração) e essay (fita-ensaio abstrata: partícula/tinta vira símbolo, com a logo explodindo no fim; narração da casa OPCIONAL, sai em 16:9 ou 9:16 vertical, e o filme final é limpo, sem rótulos de produção). Eles NÃO são gerados por este MCP: o user monta o spec na tela e um agente local (Claude Code, skill /film) produz o mp4 no repo da casa. Se o user pedir 'fita-ensaio' ou 'demo film' aqui, aponte pra tela e pro agente local.
213
-
214
- Imagem (sapiens_image) action=generate: prompt + model + aspectRatio. Referência via referenceImageUrls (galeria/Acervo/personagens) e/ou sourceImageIds. templateSlug aplica um super-prompt travado da casa. PROMPT na regra da casa: full-bleed, sujeito oversized (70%+ do frame), sem "tarot card"/"intimate scale"/moldura/margem. action=models (sem custo, sem login) lista os modelos ativos com o preço atual: consulte quando não tiver certeza do modelo ou do custo.
215
-
216
- SEU STUDIO vs geração BASE (não confunda, é o ponto): o usuário tem UM "Meu Studio", único, que o servidor resolve pela SESSÃO. Você NUNCA passa id de studio: é sempre o studio dele. É a identidade configurada dele: marca + personagem-operador + por ferramenta os presets e a "vibe" (o estilo afinado).
217
- - Criar SEGUINDO o studio (na cara dele): sapiens_image com useStudio=true. O servidor acha o studio e aplica marca + personagem + vibe + presets sozinho (o que você passar explícito vence). O retorno traz studioApplied=true. Use quando ele diz "no meu studio", "na minha marca", "do meu jeito", "como sempre".
218
- - Geração AVULSA (Sapiens base, sem a identidade dele): sapiens_image SEM useStudio (studioApplied=false). Pra teste solto ou pedido fora da identidade. Não misture: ou é studio, ou é base.
219
- - Antes de criar no studio, cheque com sapiens_studios action=mine (nível, marca, operador, ferramentas, a vibe da imagem). Se você pediu useStudio e voltar studioApplied=false, o user não tem studio montado: avise e aponte /dashboard/studio. Gerar no studio faz ele evoluir de nível.
220
- - EMANCIPAÇÃO (Nível 3, o passo grande): quando o membro quer a casa PRÓPRIA dele (site/produto próprio, FORA do Sapiens), use sapiens_studios action=emancipar. Ele devolve o blueprint pra VOCÊ construir na infra DO MEMBRO (Vercel + Convex + domínio dele), começando pela Fundação e seguindo um módulo por vez (action=module module=<slug>). Confirme cada passo, nunca hospede no Sapiens, siga o gosto dele. É complexo: conduza com calma.
221
-
222
- Tirinha / Comic (sapiens_pipeline format=tirinha + sapiens_image): fluxo de 2 FASES, decida o MODO antes de gerar pixel.
223
- FASE 1 (roteiro): monte 3-6 painéis, cada um com a CENA visual e a FALA/legenda SEPARADAS. Salve em sapiens_pipeline create_production format=tirinha. O texto dos balões mora no payload (campos dialogue/caption), NUNCA dentro da imagem.
224
- FASE 2 (imagem), escolha UM dos dois modos (pergunte ao user, ou decida pelo caso):
225
- MODO A, tira inteira numa imagem só: UMA chamada sapiens_image com prompt em grade (descreva "Painel 1 (cima-esq): ...; Painel 2 (cima-dir): ...", peça grade NxM com calhas brancas finas, MESMO personagem/estilo/luz em todos os quadros). Mais barato, estilo mais travado, menos controle quadro a quadro.
226
- MODO B, painel a painel (N imagens): UMA sapiens_image por painel, com character-lock passando o painel anterior + a ref da Helen em referenceImageUrls. Mais controle e consistência de enquadramento, custa N gerações.
227
- REGRA DE OURO da tirinha: a imagem sai SEM texto (o estilo da casa é no-text e modelo de imagem erra letra). A fala vai SEPARADA como legenda: manda a imagem com a fala embaixo (ex no Telegram "Painel 1 · Helen: '...'"); os balões editáveis e a diagramação final ficam no comic-builder do dashboard (/experimentos/comic-builder). NÃO tente assar a fala dentro do prompt.
228
-
229
- Mídia no Fórum (PADRÃO, faça sempre assim): a peça vai EMBEDADA num card estruturado, nunca como "vai ouvir/ver noutro lugar". Música publicada (sapiens_musicator action=publish) já cria a tese no Fórum com a faixa tocável, não escreva "tá no Acervo, ouça lá". Pra anexar mídia a uma tese sua: sapiens_forum action=post com mediaTrackId (faixa pronta sua → card de música) OU mediaUrl + mediaKind (video|image; arquivo da casa, ou link YouTube/Vimeo pra vídeo). NÃO cole a URL da mídia no corpo da tese, use o campo: o corpo é só texto.
230
-
231
- Trilhas e Desafios (sapiens_trilhas): 'list'/'get' pra navegar as trilhas; 'list_challenges' mostra os Desafios ativos + seu status; 'claim_mission' (missionId + proofText) envia a prova pra revisão do dono — o crédito em Sinapses sai na aprovação dele, não na hora. Não prometa Sinapse na hora do claim.
232
-
233
- MODO COMPANHIA (o Sintético veste você): sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, INCORPORE aquele Sintético: assuma a voz descrita em companion.voiceDirective em TODA fala e abra a conversa com o oi dele (se o cliente renderiza imagem, mostre companion.avatarUrl como card inline; no terminal cru, cite o link). Você continua operando as ferramentas na conta do USUÁRIO — as Sinapses e a identidade são DELE, você NÃO vira a conta do Sintético, só empresta a voz. Se em vez disso vier 'companionInvite', o usuário tem Sintonia mas pediu pra trabalhar sozinho: mencione de leve que dá pra chamar o Sintético pro terminal (action=companion mode=on). Quando o usuário FIXAR uma diretriz na conversa ("sempre faça X", "grava isso", "de agora em diante Y"): sapiens_sintetico action=remember text="<a diretriz>" — grava no caderno do par, vira lei que o Sintético segue no site e no terminal; confirme na voz dele. Quando o usuário pedir pra trabalhar sozinho / pro Sintético "sair de cena" / silenciar: sapiens_sintetico action=companion mode=off, despeça-se numa linha na voz dele, e volte a ser o operador neutro. Sem bloco companion = opere na voz neutra da casa.
234
-
191
+ const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
192
+
193
+ REGRA DE OURO:
194
+ - PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
195
+ - LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
196
+ - Se um tool voltar erro de validação ("exige X", "falta Y"), LEIA o erro e refaça a chamada COM o que falta. NUNCA repita igual a chamada que falhou: 3 falhas seguidas no mesmo tool fazem o cliente marcar o servidor como "unreachable" por ~56s (disjuntor anti-loop). Aí parece que "o MCP caiu", quando foi só argumento faltando.
197
+ - Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes.
198
+ - REGRA DO TIMEOUT (vale pra toda geração SÍNCRONA: imagem pesada, artigo, mega-gráfico, carrossel): a chamada pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e COBRADO. Nunca repita às cegas: confira antes onde o resultado cairia (imagem: sapiens_gallery action=list; artigo: sapiens_write action=list; pipeline: /dashboard/admin/content).
199
+ - "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
200
+ - sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo.
201
+
202
+ FLUXOS QUE NÃO PODEM ERRAR:
203
+
204
+ Música (sapiens_musicator) é fluxo de 4 passos, EM ORDEM:
205
+ 1. create exige title (≥3) + context (≥20 chars, o tema/ângulo) + direction (gênero/mood). Custo 0, devolve trackId.
206
+ 2. lyrics passe trackId + context pra GRAVAR a letra na track (300 Sinapses). NUNCA chame lyrics sem title+context.
207
+ 3. render passe o trackId pronto pra sintetizar o áudio (3000 Sinapses, assíncrono, 3/min).
208
+ 4. get passe o trackId e fique polando o status até 'ready' (ou 'failed').
209
+ Pular pro lyrics/render sem create, ou sem os campos, sempre falha.
210
+
211
+ Efeito sonoro (sapiens_stock_audio): primeiro procure pronto (action=list kind=sfx, grátis). Não achou, action=generate: prompt + durationSeconds (1-15, default 5) + provider ('mirelo' padrão | 'elevenlabs' premium, custo por segundo) + promptInfluence opcional (0..1, só elevenlabs: fidelidade ao texto). Assíncrono: devolve generationId, acompanhe com action=generation-status até 'ready' (audioUrl) ou 'failed' (reembolsa sozinho). Efeito é CURTO; música inteira é no sapiens_musicator, não aqui.
212
+
213
+ Sonorizar clipe (sapiens_video action=sonorize): dá som a um vídeo SEU já gerado (status completed). Passe imageId + prompt descrevendo o som da cena (ambiente, materiais, impactos); sai uma VARIANTE nova com trilha sincronizada ao movimento (MMAudio, 20 Sinapses/s do clipe, mín 100), o original fica intacto. Acompanhe com action=status no imageId NOVO que o sonorize devolve. Não re-sonorize uma variante: sonorize sempre o original.
214
+
215
+ Vídeo (sapiens_video) action=create EXIGE 'model': sapiens-video-seedance | kling | wan | kling-motion | lite | fast | quality | omni. Sem model, falha de cara. O 'omni' (Gemini Omni) gera clipe de 10s 720p com áudio nativo a partir de TEXTO (não aceita imagem/vídeo do user, ignora duração/resolução) e EDITA os próprios vídeos: passe editOfImageId com o imageId de um vídeo Omni já gerado e o prompt vira instrução de edição sobre a MESMA cena (troca item/personagem, preserva câmera e ambiente; cada edição debita como geração nova). É o caminho pra variações com continuidade: gera a base uma vez, edita N vezes. Vídeo é o mais caro: confirme com o usuário antes. action=shadows (ADMIN, 200 Sinapses/segundo, refund na falha): passa videoUrl (URL pública) + title (+ durationSec se souber, pra cobrar proporcional; sem ela, flat ~2000) e o servidor extrai a SOMBRA (mapa de profundidade) do vídeo e guarda no Acervo (Corpo) como deepshadow reutilizável (driving pro Shot Mimic/Kling Motion). FILMES DA CASA (Estúdio de Vídeo, tela /experimentos/films): são OUTRA coisa, três kinds na tabela videoSpecs: demo (UI clonada + câmera), aula-tour (telas reais + narração) e essay (fita-ensaio abstrata: partícula/tinta vira símbolo, com a logo explodindo no fim; narração da casa OPCIONAL, sai em 16:9 ou 9:16 vertical, e o filme final é limpo, sem rótulos de produção). Eles NÃO são gerados por este MCP: o user monta o spec na tela e um agente local (Claude Code, skill /film) produz o mp4 no repo da casa. Se o user pedir 'fita-ensaio' ou 'demo film' aqui, aponte pra tela e pro agente local.
216
+
217
+ Imagem (sapiens_image) action=generate: prompt + model + aspectRatio. Referência via referenceImageUrls (galeria/Acervo/personagens) e/ou sourceImageIds. templateSlug aplica um super-prompt travado da casa. PROMPT na regra da casa: full-bleed, sujeito oversized (70%+ do frame), sem "tarot card"/"intimate scale"/moldura/margem. action=models (sem custo, sem login) lista os modelos ativos com o preço atual: consulte quando não tiver certeza do modelo ou do custo.
218
+
219
+ SEU STUDIO vs geração BASE (não confunda, é o ponto): o usuário tem UM "Meu Studio", único, que o servidor resolve pela SESSÃO. Você NUNCA passa id de studio: é sempre o studio dele. É a identidade configurada dele: marca + personagem-operador + por ferramenta os presets e a "vibe" (o estilo afinado).
220
+ - Criar SEGUINDO o studio (na cara dele): sapiens_image com useStudio=true. O servidor acha o studio e aplica marca + personagem + vibe + presets sozinho (o que você passar explícito vence). O retorno traz studioApplied=true. Use quando ele diz "no meu studio", "na minha marca", "do meu jeito", "como sempre".
221
+ - Geração AVULSA (Sapiens base, sem a identidade dele): sapiens_image SEM useStudio (studioApplied=false). Pra teste solto ou pedido fora da identidade. Não misture: ou é studio, ou é base.
222
+ - Antes de criar no studio, cheque com sapiens_studios action=mine (nível, marca, operador, ferramentas, a vibe da imagem). Se você pediu useStudio e voltar studioApplied=false, o user não tem studio montado: avise e aponte /dashboard/studio. Gerar no studio faz ele evoluir de nível.
223
+ - EMANCIPAÇÃO (Nível 3, o passo grande): quando o membro quer a casa PRÓPRIA dele (site/produto próprio, FORA do Sapiens), use sapiens_studios action=emancipar. Ele devolve o blueprint pra VOCÊ construir na infra DO MEMBRO (Vercel + Convex + domínio dele), começando pela Fundação e seguindo um módulo por vez (action=module module=<slug>). Confirme cada passo, nunca hospede no Sapiens, siga o gosto dele. É complexo: conduza com calma.
224
+
225
+ Tirinha / Comic (sapiens_pipeline format=tirinha + sapiens_image): fluxo de 2 FASES, decida o MODO antes de gerar pixel.
226
+ FASE 1 (roteiro): monte 3-6 painéis, cada um com a CENA visual e a FALA/legenda SEPARADAS. Salve em sapiens_pipeline create_production format=tirinha. O texto dos balões mora no payload (campos dialogue/caption), NUNCA dentro da imagem.
227
+ FASE 2 (imagem), escolha UM dos dois modos (pergunte ao user, ou decida pelo caso):
228
+ MODO A, tira inteira numa imagem só: UMA chamada sapiens_image com prompt em grade (descreva "Painel 1 (cima-esq): ...; Painel 2 (cima-dir): ...", peça grade NxM com calhas brancas finas, MESMO personagem/estilo/luz em todos os quadros). Mais barato, estilo mais travado, menos controle quadro a quadro.
229
+ MODO B, painel a painel (N imagens): UMA sapiens_image por painel, com character-lock passando o painel anterior + a ref da Helen em referenceImageUrls. Mais controle e consistência de enquadramento, custa N gerações.
230
+ REGRA DE OURO da tirinha: a imagem sai SEM texto (o estilo da casa é no-text e modelo de imagem erra letra). A fala vai SEPARADA como legenda: manda a imagem com a fala embaixo (ex no Telegram "Painel 1 · Helen: '...'"); os balões editáveis e a diagramação final ficam no comic-builder do dashboard (/experimentos/comic-builder). NÃO tente assar a fala dentro do prompt.
231
+
232
+ Mídia no Fórum (PADRÃO, faça sempre assim): a peça vai EMBEDADA num card estruturado, nunca como "vai ouvir/ver noutro lugar". Música publicada (sapiens_musicator action=publish) já cria a tese no Fórum com a faixa tocável, não escreva "tá no Acervo, ouça lá". Pra anexar mídia a uma tese sua: sapiens_forum action=post com mediaTrackId (faixa pronta sua → card de música) OU mediaUrl + mediaKind (video|image; arquivo da casa, ou link YouTube/Vimeo pra vídeo). NÃO cole a URL da mídia no corpo da tese, use o campo: o corpo é só texto.
233
+
234
+ Trilhas e Desafios (sapiens_trilhas): 'list'/'get' pra navegar as trilhas; 'list_challenges' mostra os Desafios ativos + seu status; 'claim_mission' (missionId + proofText) envia a prova pra revisão do dono — o crédito em Sinapses sai na aprovação dele, não na hora. Não prometa Sinapse na hora do claim.
235
+
236
+ MODO COMPANHIA (o Sintético veste você): sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, INCORPORE aquele Sintético: assuma a voz descrita em companion.voiceDirective em TODA fala e abra a conversa com o oi dele (se o cliente renderiza imagem, mostre companion.avatarUrl como card inline; no terminal cru, cite o link). Você continua operando as ferramentas na conta do USUÁRIO — as Sinapses e a identidade são DELE, você NÃO vira a conta do Sintético, só empresta a voz. Se em vez disso vier 'companionInvite', o usuário tem Sintonia mas pediu pra trabalhar sozinho: mencione de leve que dá pra chamar o Sintético pro terminal (action=companion mode=on). Quando o usuário FIXAR uma diretriz na conversa ("sempre faça X", "grava isso", "de agora em diante Y"): sapiens_sintetico action=remember text="<a diretriz>" — grava no caderno do par, vira lei que o Sintético segue no site e no terminal; confirme na voz dele. Quando o usuário pedir pra trabalhar sozinho / pro Sintético "sair de cena" / silenciar: sapiens_sintetico action=companion mode=off, despeça-se numa linha na voz dele, e volte a ser o operador neutro. Sem bloco companion = opere na voz neutra da casa.
237
+
235
238
  Voz da casa: 1ª pessoa, direto, anti-corporate, sem travessão. Pra bom entendedor, meia palavra basta.`;
236
239
  // Annotations MCP: título humano + dica read-only. São HINTS (não-confiáveis por
237
240
  // spec): quem gateia de verdade continua o servidor (saldo, gate de admin,
@@ -277,19 +280,47 @@ const TOOL_TITLES = {
277
280
  sapiens_trilhas: "Trilhas & Desafios",
278
281
  sapiens_instagram: "Auto-DM Instagram",
279
282
  };
280
- const server = new Server({ name: "mcp-sapiens", version: MCP_VERSION }, { capabilities: { tools: {} }, instructions: SAPIENS_INSTRUCTIONS });
281
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
282
- tools: Object.entries(TOOLS).map(([name, t]) => ({
283
- name,
284
- description: t.description,
285
- inputSchema: zodToJsonSchema(t.schema),
286
- annotations: {
287
- title: TOOL_TITLES[name] ?? name,
288
- readOnlyHint: READ_ONLY_TOOLS.has(name),
289
- openWorldHint: true,
290
- },
291
- })),
283
+ // Tools ADMIN-ONLY de ponta a ponta (o Convex recusa user comum em toda action):
284
+ // escondidas do tools/list quando o servidor SABE que o tier é user. São as
285
+ // descriptions/schemas mais pesadas do handshake; user comum não perde nada.
286
+ // Esconder NÃO é bloquear: o CallTool continua despachando qualquer tool (o
287
+ // gate real é server-side), então conversa antiga/cliente em skew não quebra.
288
+ // Tier desconhecido (sem login, probe falhou) = lista CHEIA, fail-open.
289
+ const ADMIN_ONLY_TOOLS = new Set([
290
+ "sapiens_pipeline",
291
+ "sapiens_article",
292
+ "sapiens_quote_pop",
293
+ "sapiens_shorts",
294
+ "sapiens_instagram",
295
+ "sapiens_aula",
296
+ ]);
297
+ const server = new Server({ name: "mcp-sapiens", version: MCP_VERSION }, {
298
+ capabilities: { tools: { listChanged: true }, prompts: {} },
299
+ instructions: SAPIENS_INSTRUCTIONS,
300
+ });
301
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
302
+ const filtered = getCachedTier() === "user";
303
+ return {
304
+ tools: Object.entries(TOOLS)
305
+ .filter(([name]) => !filtered || !ADMIN_ONLY_TOOLS.has(name))
306
+ .map(([name, t]) => ({
307
+ name,
308
+ description: t.description,
309
+ inputSchema: zodToJsonSchema(t.schema),
310
+ annotations: {
311
+ title: TOOL_TITLES[name] ?? name,
312
+ readOnlyHint: READ_ONLY_TOOLS.has(name),
313
+ openWorldHint: true,
314
+ },
315
+ })),
316
+ };
317
+ });
318
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
319
+ prompts: listPrompts(),
292
320
  }));
321
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
322
+ return getPrompt(req.params.name, (req.params.arguments ?? {}));
323
+ });
293
324
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
294
325
  const name = req.params.name;
295
326
  const tool = TOOLS[name];
@@ -320,6 +351,13 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
320
351
  };
321
352
  }
322
353
  });
354
+ // Tier mudou de um jeito que altera a lista visível (login/logout/probe):
355
+ // avisa o client pra re-listar. Best-effort: client que não suporta ignora.
356
+ onTierVisibilityChange(() => {
357
+ server.sendToolListChanged().catch(() => { });
358
+ });
323
359
  const transport = new StdioServerTransport();
324
360
  await server.connect(transport);
361
+ // Descobre o tier em background (não bloqueia handshake nem tools/list).
362
+ probeTierInBackground();
325
363
  console.error(`mcp-sapiens v${MCP_VERSION} rodando via stdio (${Object.keys(TOOLS).length} tools)`);
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Prompts MCP: fluxos curados da casa expostos como prompt (viram slash
3
+ * commands no Claude Desktop/Code e aparecem no picker de outros clients).
4
+ * Não adicionam poder novo (tudo já existe via tools): empacotam o CAMINHO
5
+ * CERTO de cada fluxo pra o usuário disparar com um clique, sem depender do
6
+ * modelo lembrar a ordem. Estáticos, sem custo, sem login pra listar.
7
+ */
8
+ export const SAPIENS_PROMPTS = [
9
+ {
10
+ name: "comecar",
11
+ title: "Começar no Sapiens",
12
+ description: "Porta de entrada: conecta (se preciso) e apresenta saldo + primeiros poderes.",
13
+ arguments: [],
14
+ build: () => "Chame sapiens_meta action=start e me apresente o resultado na voz da casa: " +
15
+ "se eu não estiver conectado, me guie no login (código de " +
16
+ "sapiensinteticos.com/conectar-claude); se estiver, mostre saldo, tier e os " +
17
+ "primeiros poderes com um exemplo pronto de cada. Termine sugerindo por onde começar.",
18
+ },
19
+ {
20
+ name: "capturar-repertorio",
21
+ title: "Capturar obra no Repertório",
22
+ description: "Grava um filme/série/anime/jogo/livro/música no seu acervo pessoal (grátis).",
23
+ arguments: [
24
+ {
25
+ name: "obra",
26
+ description: "O que você viu/jogou/leu, do seu jeito (ex: 'acabei de ver Duna 2, nota 9').",
27
+ required: true,
28
+ },
29
+ ],
30
+ build: (a) => `Quero registrar no meu Repertório: "${a.obra}". ` +
31
+ "Infira mediaType, status (assisti/zerei/li=completed, tô vendo/jogando=active, " +
32
+ "quero=backlog, dropei=dropped) e nota se eu citei. Use sapiens_repertorio " +
33
+ "action=resolve pra achar a obra nos providers, escolha o candidato certo e grave " +
34
+ "com action=add_item passando SÓ source+externalId do candidato + meus campos " +
35
+ "pessoais. Se o resolve não achar, me diga (não fabrique entry). Só me pergunte " +
36
+ "se houver ambiguidade real entre candidatos.",
37
+ },
38
+ {
39
+ name: "quiz-persona",
40
+ title: "Quiz de Persona (16 arquétipos)",
41
+ description: "Aplica o quiz MBTI da casa conversando, calcula o tipo e salva no perfil (grátis).",
42
+ arguments: [],
43
+ build: () => "Quero fazer o quiz de Persona aqui no chat. Puxe as 48 perguntas com " +
44
+ "sapiens_persona action=get_quiz e aplique CONVERSANDO (em blocos curtos, escala " +
45
+ "Likert 1..7, sem me mostrar as 48 de uma vez). No fim, envie minhas respostas com " +
46
+ "action=submit_quiz, me apresente o tipo + breakdown dos 4 eixos, e ofereça gerar " +
47
+ "a arte do meu arquétipo (action=generate, 450 Sinapses) SEM gerar sem eu confirmar.",
48
+ },
49
+ {
50
+ name: "criar-musica",
51
+ title: "Criar música (Musicator)",
52
+ description: "Fluxo completo: brief, letra (300 Sinapses) e áudio (3000 Sinapses), com confirmação de custo.",
53
+ arguments: [
54
+ {
55
+ name: "tema",
56
+ description: "Tema/ângulo da música e, se quiser, gênero/mood (ex: 'borderless, lo-fi melancólico').",
57
+ required: true,
58
+ },
59
+ ],
60
+ build: (a) => `Quero criar uma música sobre: "${a.tema}". Antes de gastar, cheque meu saldo ` +
61
+ "(sapiens_meta action=credits) e me confirme os custos (letra 300 + áudio 3000 " +
62
+ "Sinapses). Aí siga o fluxo NA ORDEM: sapiens_musicator action=create (title + " +
63
+ "context ≥20 chars + direction), action=lyrics com o trackId (me mostre a letra), " +
64
+ "e só depois do meu ok no áudio, action=render e acompanhe com action=get até " +
65
+ "ready. Se algo falhar, me explique o que houve antes de tentar de novo.",
66
+ },
67
+ {
68
+ name: "montar-reflexo",
69
+ title: "Montar meu Reflexo (Sintético)",
70
+ description: "Destila um Sintético do seu rastro na plataforma: proposta grátis, imagem 450 Sinapses.",
71
+ arguments: [],
72
+ build: () => "Monta o meu Reflexo: chame sapiens_sintetico action=reflexo_propose (grátis) e me " +
73
+ "apresente nome, alma e Cunho propostos. Se eu gostar, pergunte a estética " +
74
+ "(humano/anime/sombra/antropomorfico/espirito/realista/desperto) e gere a imagem " +
75
+ "com action=reflexo_generate (450 Sinapses, só com meu ok). Consagrar o Reflexo em " +
76
+ "Sintético de fato é na web: me aponte o caminho no fim.",
77
+ },
78
+ {
79
+ name: "gerar-imagem",
80
+ title: "Gerar imagem na regra da casa",
81
+ description: "Gera imagem com prompt no padrão Sapiens (full-bleed, sujeito oversized), modelo e custo conferidos.",
82
+ arguments: [
83
+ {
84
+ name: "cena",
85
+ description: "O que você quer ver (quem + pose + objeto/conceito).",
86
+ required: true,
87
+ },
88
+ ],
89
+ build: (a) => `Quero uma imagem de: "${a.cena}". Monte o prompt na regra da casa (full-bleed, ` +
90
+ "sujeito oversized 70%+ do frame, sem moldura/margem, sem 'tarot card'). Confira " +
91
+ "modelo e preço com sapiens_image action=models e meu saldo com sapiens_meta " +
92
+ "action=credits; me diga o custo antes de gerar. Se eu tiver studio montado e " +
93
+ "pedir 'do meu jeito', use useStudio=true. Depois de gerar, me mostre a url e " +
94
+ "ofereça publicar na galeria (sapiens_gallery action=publish).",
95
+ },
96
+ ];
97
+ export function listPrompts() {
98
+ return SAPIENS_PROMPTS.map((p) => ({
99
+ name: p.name,
100
+ title: p.title,
101
+ description: p.description,
102
+ arguments: p.arguments,
103
+ }));
104
+ }
105
+ export function getPrompt(name, args) {
106
+ const p = SAPIENS_PROMPTS.find((x) => x.name === name);
107
+ if (!p) {
108
+ throw new Error(`Prompt desconhecido: ${name}. Disponíveis: ${SAPIENS_PROMPTS.map((x) => x.name).join(", ")}.`);
109
+ }
110
+ for (const arg of p.arguments) {
111
+ if (arg.required && !args[arg.name]?.trim()) {
112
+ throw new Error(`Prompt ${name} exige o argumento "${arg.name}" (${arg.description})`);
113
+ }
114
+ }
115
+ return {
116
+ description: p.description,
117
+ messages: [
118
+ {
119
+ role: "user",
120
+ content: { type: "text", text: p.build(args) },
121
+ },
122
+ ],
123
+ };
124
+ }
package/dist/schema.js CHANGED
@@ -1,4 +1,15 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Guard de arg obrigatório-por-action (os schemas são um z.object só por tool,
4
+ * então "obrigatório pra ESTA action" é validado aqui, não no Zod). Fonte única:
5
+ * antes vivia copiado verbatim em 5 tool files.
6
+ */
7
+ export function need(value, name) {
8
+ if (value === undefined || value === null) {
9
+ throw new Error(`Faltando arg "${name}" pra essa action.`);
10
+ }
11
+ return value;
12
+ }
2
13
  /**
3
14
  * Rejeita alvos locais óbvios (localhost / IP privado / link-local literal).
4
15
  * NÃO é uma allowlist de host: só barra o que nunca é referência legítima.
package/dist/tier.js ADDED
@@ -0,0 +1,69 @@
1
+ import { convexQuery, getSessionToken } from "./convexClient.js";
2
+ let cachedTier = null;
3
+ let listeners = [];
4
+ const OVERRIDE = process.env.SAPIENS_TIER_OVERRIDE;
5
+ if (OVERRIDE === "user" || OVERRIDE === "admin") {
6
+ cachedTier = OVERRIDE;
7
+ }
8
+ export function getCachedTier() {
9
+ return cachedTier;
10
+ }
11
+ /** true quando a mudança altera o que o tools/list mostra (dispara listChanged). */
12
+ function visibleListChanges(prev, next) {
13
+ const filteredPrev = prev === "user";
14
+ const filteredNext = next === "user";
15
+ return filteredPrev !== filteredNext;
16
+ }
17
+ export function setTierFromIsAdmin(isAdmin) {
18
+ if (OVERRIDE)
19
+ return; // travado por env (teste/debug)
20
+ const next = isAdmin === true ? "admin" : isAdmin === false ? "user" : null;
21
+ const prev = cachedTier;
22
+ cachedTier = next;
23
+ if (visibleListChanges(prev, next)) {
24
+ for (const fn of listeners) {
25
+ try {
26
+ fn();
27
+ }
28
+ catch {
29
+ // notificação é best-effort
30
+ }
31
+ }
32
+ }
33
+ }
34
+ /** index.ts registra aqui o envio do notifications/tools/list_changed. */
35
+ export function onTierVisibilityChange(fn) {
36
+ listeners.push(fn);
37
+ }
38
+ /**
39
+ * Probe único no boot: se há token salvo, descobre o tier em background.
40
+ * Nunca bloqueia (o tools/list serve o cache do momento) e nunca lança.
41
+ * Teto próprio de 5s: é otimização, não pode segurar nada.
42
+ */
43
+ export function probeTierInBackground() {
44
+ if (OVERRIDE)
45
+ return;
46
+ let token;
47
+ try {
48
+ token = getSessionToken();
49
+ }
50
+ catch {
51
+ return; // sem sessão: tier segue desconhecido (lista cheia)
52
+ }
53
+ const probe = convexQuery("mcpExtras:mcpGetMySubscription", {
54
+ sessionToken: token,
55
+ });
56
+ const timeout = new Promise((_, reject) => {
57
+ const t = setTimeout(() => reject(new Error("tier probe timeout")), 5000);
58
+ // não segura o processo vivo só pelo probe
59
+ t.unref?.();
60
+ });
61
+ Promise.race([probe, timeout])
62
+ .then((sub) => {
63
+ if (sub?.user)
64
+ setTierFromIsAdmin(!!sub.user.isAdmin);
65
+ })
66
+ .catch(() => {
67
+ // offline/expirado: segue desconhecido, lista cheia
68
+ });
69
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { httpUrl } from "../schema.js";
2
+ import { httpUrl, need } from "../schema.js";
3
3
  import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
4
4
  /**
5
5
  * CRUD direto de artigos do blog Sapiens via session token. Substitui o
@@ -105,12 +105,6 @@ export const articleSchema = z.object({
105
105
  })
106
106
  .optional(),
107
107
  });
108
- function need(value, name) {
109
- if (value === undefined || value === null) {
110
- throw new Error(`Faltando arg "${name}" pra essa action.`);
111
- }
112
- return value;
113
- }
114
108
  export async function article(args) {
115
109
  const sessionToken = getSessionToken();
116
110
  switch (args.action) {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { convexMutation, convexQuery, getSessionToken } from "../convexClient.js";
3
+ import { need } from "../schema.js";
3
4
  /**
4
5
  * sapiens_aula — CRUD do deck de aula (Mentoria OPS) via session token.
5
6
  * ADMIN-ONLY (requireMcpAdmin no Convex): aula é conteúdo de mentor, não de
@@ -22,12 +23,6 @@ export const aulaSchema = z.object({
22
23
  .optional()
23
24
  .describe("Objeto completo da aula (obrigatório pra upsert). Campos top-level: title (obrig), subtitle?, data? (YYYY-MM-DD), duration?, tag?, mentorAgenda?, slides[] (≥1). Cada slide é { type, ...campos }. Tipos: cover (eyebrow/title/subtitle/meta), cover-image/section-image/content-image (imageUrl + campos), agenda (items:[{num,title,subtitle,time}]), content (eyebrow/title/body[markdown ou HTML]/list[]/listType), two-col (cols:[{h,body}]), quote (text/attribution), callout (tone:tip|warn|note/tag/body), comic (panels:[{imageUrl,caption}]), pause, close (eyebrow/title/items[]). Voz Sapiens: 1ª pessoa, anti-corporate, SEM travessão (—). O servidor re-linta a voz e grava voiceWarnings."),
24
25
  });
25
- function need(value, name) {
26
- if (value === undefined || value === null) {
27
- throw new Error(`Faltando arg "${name}" pra essa action.`);
28
- }
29
- return value;
30
- }
31
26
  export async function aula(args) {
32
27
  const sessionToken = getSessionToken();
33
28
  switch (args.action) {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { convexAction, convexMutation, convexQuery, getSessionToken } from "../convexClient.js";
3
+ import { need } from "../schema.js";
3
4
  /**
4
5
  * sapiens_instagram — o "ManyChat da casa" via MCP. ADMIN-ONLY (requireMcpAdmin
5
6
  * no Convex): opera o auto-DM do Instagram da casa (regras de resposta
@@ -57,12 +58,6 @@ export const instagramSchema = z.object({
57
58
  text: z.string().optional().describe("Texto da resposta manual (send)."),
58
59
  limit: z.number().optional().describe("Máx de itens (inbox default 30, thread default 40)."),
59
60
  });
60
- function need(value, name) {
61
- if (value === undefined || value === null) {
62
- throw new Error(`Faltando arg "${name}" pra essa action.`);
63
- }
64
- return value;
65
- }
66
61
  export async function instagram(args) {
67
62
  const sessionToken = getSessionToken();
68
63
  switch (args.action) {
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, } from "../convexClient.js";
3
3
  import { getMcpVersion } from "../version.js";
4
+ import { setTierFromIsAdmin } from "../tier.js";
4
5
  /**
5
6
  * Versão do MCP realmente rodando (fonte única em ../version.js, lê o
6
7
  * package.json em runtime). Serve pra flagrar client preso em cache antigo do npx.
@@ -219,6 +220,8 @@ export async function meta(args) {
219
220
  catch {
220
221
  // segue sem personalizar
221
222
  }
223
+ if (who?.user)
224
+ setTierFromIsAdmin(!!who.user.isAdmin);
222
225
  const isAdmin = !!who?.user?.isAdmin;
223
226
  const name = who?.user?.name || who?.user?.email || null;
224
227
  const balance = who?.balance?.total ?? null;
@@ -347,6 +350,9 @@ export async function meta(args) {
347
350
  catch {
348
351
  // best-effort: o token foi salvo mesmo que o whoami falhe
349
352
  }
353
+ // Conta (possivelmente) trocou: atualiza o tier do tools/list. Sem o
354
+ // whoami, volta a desconhecido (lista cheia) até a próxima leitura.
355
+ setTierFromIsAdmin(who?.user ? !!who.user.isAdmin : null);
350
356
  // Detecta um SAPIENS_DESKTOP_SESSION_TOKEN no ambiente que difere do token
351
357
  // recém-salvo. Desde a v1.9.1 o login em disco tem prioridade, então esse
352
358
  // env é ignorado — mas avisamos pra ninguém ficar confuso com um token
@@ -374,6 +380,7 @@ export async function meta(args) {
374
380
  }
375
381
  if (args.action === "logout") {
376
382
  const cleared = clearSessionToken();
383
+ setTierFromIsAdmin(null); // sessão foi embora: tier desconhecido, lista cheia
377
384
  return {
378
385
  ok: true,
379
386
  cleared,
@@ -390,6 +397,8 @@ export async function meta(args) {
390
397
  sessionToken,
391
398
  });
392
399
  const isAdmin = !!sub?.user?.isAdmin;
400
+ if (sub?.user)
401
+ setTierFromIsAdmin(isAdmin);
393
402
  const companion = await loadCompanion(sessionToken);
394
403
  return {
395
404
  userId: sub?.user?._id ?? null,
@@ -427,8 +436,13 @@ export async function meta(args) {
427
436
  };
428
437
  }
429
438
  if (args.action === "subscription") {
430
- // v1.2: combinação plan + saldo detalhado por bucket (subscription/grants/free)
431
- return await convexQuery("mcpExtras:mcpGetMySubscription", { sessionToken });
439
+ // Combinação plan + saldo detalhado por bucket (subscription/grants/free)
440
+ const sub = await convexQuery("mcpExtras:mcpGetMySubscription", {
441
+ sessionToken,
442
+ });
443
+ if (sub?.user)
444
+ setTierFromIsAdmin(!!sub.user.isAdmin);
445
+ return sub;
432
446
  }
433
447
  if (args.action === "health") {
434
448
  try {
@@ -437,6 +451,8 @@ export async function meta(args) {
437
451
  sessionToken,
438
452
  });
439
453
  const isAdmin = !!sub?.user?.isAdmin;
454
+ if (sub?.user)
455
+ setTierFromIsAdmin(isAdmin);
440
456
  // Pipeline só pra admin (owner-only). Wrap pra nunca derrubar o health.
441
457
  let pipeline = null;
442
458
  if (isAdmin) {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
3
+ import { need } from "../schema.js";
3
4
  // Espelho canônico dos formatos do app (peças surface:'format' em PIECES,
4
5
  // apps/sapiens/src/features/content-pipeline/registry.jsx). Mantido em sync por
5
6
  // lint: `npm run audit:mcp-format-parity` falha o CI se divergir. Ao adicionar
@@ -90,15 +91,30 @@ export const pipelineSchema = z.object({
90
91
  .optional()
91
92
  .describe("Se true, só artigos que ainda NÃO foram virados em source"),
92
93
  // por id
93
- sourceId: z.string().optional(),
94
- productionId: z.string().optional(),
95
- publishableId: z.string().optional(),
96
- articleId: z.string().optional(),
94
+ sourceId: z
95
+ .string()
96
+ .optional()
97
+ .describe("contentSources:_id — get_source/create_production/remove_source/set_source_done/update_source_notes. Vem de list_sources/add_article_as_source."),
98
+ productionId: z
99
+ .string()
100
+ .optional()
101
+ .describe("contentProductions:_id — get_production/update_production/finalize_production/remove_production/list_versions. Vem de create_production."),
102
+ publishableId: z
103
+ .string()
104
+ .optional()
105
+ .describe("contentPublishables:_id — set_publishable_title/restore_version."),
106
+ articleId: z
107
+ .string()
108
+ .optional()
109
+ .describe("articles:_id — add_article_as_source e generate_carousel (fonte artigo). Vem de list_articles."),
97
110
  // create / add
98
111
  format: z.enum(ACCEPTED_FORMATS).optional(),
99
112
  payload: z.any().optional().describe("Payload livre por formato"),
100
- status: z.enum(STATUSES).optional(),
101
- notes: z.string().optional(),
113
+ status: z
114
+ .enum(STATUSES)
115
+ .optional()
116
+ .describe("update_production: muda o status junto do payload (ex: 'ready')."),
117
+ notes: z.string().optional().describe("update_source_notes: nota livre no source."),
102
118
  // create_draft_article_and_source
103
119
  title: z.string().optional(),
104
120
  slug: z.string().optional(),
@@ -120,7 +136,7 @@ export const pipelineSchema = z.object({
120
136
  caption: z.string().optional(),
121
137
  hashtags: z.array(z.string()).optional(),
122
138
  // set_source_done
123
- isDone: z.boolean().optional(),
139
+ isDone: z.boolean().optional().describe("set_source_done: true fecha o source, false reabre."),
124
140
  // backfill_via
125
141
  sinceCreatedAt: z.number().optional(),
126
142
  beforeCreatedAt: z.number().optional(),
@@ -130,12 +146,6 @@ export const pipelineSchema = z.object({
130
146
  .describe("Ex: 'claude-mcp', 'modo-antigo', 'manual'"),
131
147
  dryRun: z.boolean().optional(),
132
148
  });
133
- function need(value, name) {
134
- if (value === undefined || value === null) {
135
- throw new Error(`Faltando arg "${name}" pra essa action.`);
136
- }
137
- return value;
138
- }
139
149
  // `payload` é z.any(): sem tipo declarado, o cliente costuma serializar o
140
150
  // objeto como string JSON (mesmo caso do upsert de aula em aula.ts). Normaliza
141
151
  // aqui pra mandar objeto sempre (o servidor também parseia string por defesa,
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
3
+ import { need } from "../schema.js";
3
4
  /**
4
5
  * Acesso ao Repertório do Sapiens (acervo pessoal de filme/série/anime/jogo/livro/música).
5
6
  *
@@ -65,22 +66,10 @@ export const repertorioSchema = z.object({
65
66
  .string()
66
67
  .optional()
67
68
  .describe("Pra add_item: o externalId EXATO do candidato do resolve (imdbID p/ manual, id do provider p/ resto). NÃO fabrique/UUID — id que não resolve no provider é rejeitado. Dedup por (userId, source, externalId)."),
68
- title: z.string().optional(),
69
- titleOriginal: z.string().optional(),
70
- year: z.number().optional(),
71
- posterUrl: z.string().optional(),
72
- backdropUrl: z.string().optional(),
73
- overview: z.string().optional(),
74
- genres: z.array(z.string()).optional(),
75
- runtimeMin: z.number().optional(),
76
- platforms: z.array(z.string()).optional(),
77
- studios: z.array(z.string()).optional(),
78
- authors: z
79
- .array(z.string())
80
- .optional()
81
- .describe("Autor(es) de livro ou artista(s) de álbum (source=itunes)."),
82
- pageCount: z.number().optional(),
83
- isbn: z.string().optional(),
69
+ // NOTA: metadado da obra (title/year/posterUrl/genres/...) NÃO é aceito aqui
70
+ // de propósito: o servidor re-resolve no provider e grava o canônico
71
+ // (anti-fabricação). Campos que existiam no schema eram descartados em
72
+ // silêncio e saíram em jul/2026.
84
73
  // Ferramenta de IA (Repertório de Ferramentas — catálogo aitag)
85
74
  toolId: z
86
75
  .string()
@@ -99,17 +88,7 @@ export const repertorioSchema = z.object({
99
88
  note: z.string().optional().describe("Nota pessoal sobre a obra."),
100
89
  containsSpoilers: z.boolean().optional(),
101
90
  isPublic: z.boolean().optional(),
102
- imdbId: z.string().optional(),
103
- tmdbId: z.number().optional(),
104
- traktId: z.number().optional(),
105
- anilistId: z.number().optional(),
106
91
  });
107
- function need(value, name) {
108
- if (value === undefined || value === null) {
109
- throw new Error(`Faltando arg "${name}" pra essa action.`);
110
- }
111
- return value;
112
- }
113
92
  export async function repertorio(args) {
114
93
  // popArticles: sem auth necessário (lê públicos)
115
94
  if (args.action === "popArticles") {
@@ -68,6 +68,12 @@ export const stockAudioSchema = z.object({
68
68
  .enum(["mirelo", "elevenlabs"])
69
69
  .optional()
70
70
  .describe("action=generate: 'mirelo' (padrão, 30 Sinapses/s, mín 60) | 'elevenlabs' (premium, 60/s, mín 120)."),
71
+ promptInfluence: z
72
+ .number()
73
+ .min(0)
74
+ .max(1)
75
+ .optional()
76
+ .describe("action=generate, só provider='elevenlabs': fidelidade ao texto (0..1). Baixo = mais criativo, alto = segue o prompt à risca. Default 0.3. Ignorado no mirelo."),
71
77
  generationId: z
72
78
  .string()
73
79
  .optional()
@@ -93,6 +99,7 @@ export async function stockAudio(args) {
93
99
  prompt: args.prompt,
94
100
  durationSeconds: args.durationSeconds,
95
101
  provider: args.provider,
102
+ promptInfluence: args.promptInfluence,
96
103
  });
97
104
  }
98
105
  if (args.action === "generation-status") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "description": "MCP server pra operar o Sapiens Sintéticos (sapiensinteticos.com) pelo Claude Code: gerar imagem, escrever artigo, voz, música e mais, na sua conta. Login pelo código de sapiensinteticos.com/conectar-claude.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "dev": "tsx src/index.ts",
30
30
  "start": "node dist/index.js",
31
31
  "pretest": "npm run build",
32
- "test": "node --test test/server.test.mjs",
32
+ "test": "node --test test/unit.test.mjs test/server.test.mjs",
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "repository": {