sapiens-mcp 1.35.0 → 1.37.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.
@@ -167,6 +167,18 @@ const requestTokenContext = new AsyncLocalStorage();
167
167
  export function runWithSessionToken(token, fn) {
168
168
  return requestTokenContext.run(token, fn);
169
169
  }
170
+ /**
171
+ * True quando a chamada corre dentro do transporte REMOTO (streamable HTTP):
172
+ * cada request remota roda embrulhada em runWithSessionToken, então o
173
+ * AsyncLocalStorage tem valor. No stdio (processo local, na máquina do dono do
174
+ * token) o contexto NUNCA é setado. É o sinal confiável pra recusar operações
175
+ * que tocam o disco do HOST (ex: ler um arquivo local pra usar como frame de
176
+ * vídeo): no servidor multi-tenant da Vercel, um caminho arbitrário seria
177
+ * leitura de arquivo do próprio servidor, nunca do PC do usuário.
178
+ */
179
+ export function isRemoteContext() {
180
+ return requestTokenContext.getStore() !== undefined;
181
+ }
170
182
  export function getSessionToken() {
171
183
  // Prioridade -1: token da REQUEST (transporte remoto, ver acima). Vence tudo:
172
184
  // identidade no remoto é sempre do bearer, nunca do estado do host.
package/dist/index.js CHANGED
@@ -6,6 +6,33 @@ import { MCP_VERSION } from "./version.js";
6
6
  import { buildToolList, callTool, SAPIENS_INSTRUCTIONS, TOOLS, } from "./registry.js";
7
7
  import { getCachedTier, onTierVisibilityChange, probeTierInBackground, } from "./tier.js";
8
8
  import { getPrompt, listPrompts } from "./prompts.js";
9
+ import { convexMutation, getSessionToken } from "./convexClient.js";
10
+ // Telemetria stdio (fire-and-forget, NUNCA derruba a chamada). O stdio sempre
11
+ // foi caixa-preta (cada cliente roda a própria cópia via npx), então o
12
+ // transporte que mais roda — Helen/Cursor/Gemini CLI — era invisível na
13
+ // mcpUsage, que só via o remoto. Loga o MESMO shape que o remote.ts, com
14
+ // transport:"stdio". Identidade do login local (getSessionToken), nunca args
15
+ // (só tool/action/ok/ms). Opt-out por SAPIENS_MCP_NO_TELEMETRY=1.
16
+ function logStdioUsage(fields) {
17
+ try {
18
+ if (process.env.SAPIENS_MCP_NO_TELEMETRY === "1")
19
+ return;
20
+ const token = getSessionToken();
21
+ if (!token)
22
+ return; // sem login não há identidade pra vincular
23
+ convexMutation("mcpUsage:logCall", {
24
+ sessionToken: token,
25
+ tool: fields.tool,
26
+ action: fields.action ?? undefined,
27
+ ok: fields.ok,
28
+ ms: Math.round(fields.ms),
29
+ transport: "stdio",
30
+ }).catch(() => { });
31
+ }
32
+ catch {
33
+ // telemetria nunca vira erro pro cliente
34
+ }
35
+ }
9
36
  // Transporte STDIO (o pacote npm/npx que os clients locais rodam). O catálogo,
10
37
  // as instructions e o dispatch vivem no registry.ts, compartilhados com o
11
38
  // transporte remoto (remote.ts). Aqui fica só o que é do stdio: o tier vem do
@@ -24,7 +51,19 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
24
51
  server.setRequestHandler(GetPromptRequestSchema, async (req) => {
25
52
  return getPrompt(req.params.name, (req.params.arguments ?? {}));
26
53
  });
27
- server.setRequestHandler(CallToolRequestSchema, async (req) => callTool(req.params.name, req.params.arguments));
54
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
55
+ const name = req.params.name;
56
+ const args = (req.params.arguments ?? {});
57
+ const t0 = Date.now();
58
+ const result = await callTool(name, args);
59
+ logStdioUsage({
60
+ tool: name,
61
+ action: typeof args.action === "string" ? args.action : null,
62
+ ok: !result.isError,
63
+ ms: Date.now() - t0,
64
+ });
65
+ return result;
66
+ });
28
67
  // Tier mudou de um jeito que altera a lista visível (login/logout/probe):
29
68
  // avisa o client pra re-listar. Best-effort: client que não suporta ignora.
30
69
  onTierVisibilityChange(() => {
package/dist/registry.js CHANGED
@@ -38,7 +38,7 @@ import { describeConvexError } from "./convexClient.js";
38
38
  */
39
39
  export const TOOLS = {
40
40
  sapiens_pipeline: {
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). CARROSSEL FINO (edição sem tela, admin-only): list_carousels (seus carrosséis standalone), get_carousel (payload completo + catálogo de templates com slots/limites + paletas — tudo pra VOCÊ escrever os slides), update_carousel (payload inteiro de volta, sanitizado no servidor; preserve os campos image dos slides que não mexeu), carousel_auto_images (IA escolhe imagens do banco pros slides de foto, 60 Sinapses; makeVisual=true converte slides de texto pra layouts de foto antes — ritmo visual; generateMissing=true gera imagem nova pros que o banco não cobrir, ~450 Sinapses cada, até 4, avise o custo antes), carousel_generate_image (imagem nova pra UM slide, ~450 Sinapses, customPrompt opcional). Fluxo confortável: generate_carousel → get_carousel → update_carousel (afia os textos) → carousel_auto_images → humano abre a url pra exportar. 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, generate_carousel, carousel_auto_images (com generateMissing) e carousel_generate_image são SÍNCRONAS e pesadas: vale a REGRA DO TIMEOUT (podem cobrar mesmo voltando 'Timeout'; cheque get_carousel/dashboard 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). generate_carousel_production (o IRMÃO PIPELINE do generate_carousel: recebe um sourceId de artigo, CRIA a production carrossel_ig e a preenche pelo MESMO motor da casa, já no shape editorial (templateId+slots) que o editor de pipeline renderiza — prefira ESTE a create_production+payload cru pro carrossel, que abre VAZIO no editor; devolve productionId + url do editor de pipeline; admin-only, cobra Sinapses, reembolsa se falhar). CARROSSEL FINO (edição sem tela, admin-only): list_carousels (seus carrosséis standalone), get_carousel (payload completo + catálogo de templates com slots/limites + paletas — tudo pra VOCÊ escrever os slides), update_carousel (payload inteiro de volta, sanitizado no servidor; preserve os campos image dos slides que não mexeu), carousel_auto_images (IA escolhe imagens do banco pros slides de foto, 60 Sinapses; makeVisual=true converte slides de texto pra layouts de foto antes — ritmo visual; generateMissing=true gera imagem nova pros que o banco não cobrir, ~450 Sinapses cada, até 4, avise o custo antes), carousel_generate_image (imagem nova pra UM slide, ~450 Sinapses, customPrompt opcional). Fluxo confortável (standalone): generate_carousel → get_carousel → update_carousel (afia os textos) → carousel_auto_images → humano abre a url pra exportar. Fluxo pipeline (carrossel atrelado a um artigo/source): generate_carousel_production (sourceId) → humano abre a url do editor de pipeline pra revisar e finalizar (finalize_production). 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, generate_carousel, generate_carousel_production, carousel_auto_images (com generateMissing) e carousel_generate_image são SÍNCRONAS e pesadas: vale a REGRA DO TIMEOUT (podem cobrar mesmo voltando 'Timeout'; cheque get_carousel/dashboard 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.",
42
42
  schema: pipelineSchema,
43
43
  handler: pipeline,
44
44
  },
@@ -53,7 +53,7 @@ export const TOOLS = {
53
53
  handler: meta,
54
54
  },
55
55
  sapiens_repertorio: {
56
- description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles, resolve (busca capa/ano/id nos providers server-side: OMDb/IGDB-Twitch/AniList/Google Books/iTunes). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item, update_item (status/rating/tags/note/isPublic), remove_item. CAPTURA ONE-SHOT travada na lista de providers: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), (1) infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped) e rating se citado; (2) chame action=resolve {mediaType, query}, escolha o candidato certo e faça add_item passando SÓ o source + externalId DELE + os campos pessoais (status/rating/tags/note). O servidor re-resolve no provider e grava título/capa/ano canônicos — você NÃO manda título/capa nem inventa externalId. (3) Se o resolve não achar (lista vazia/providerKeyMissing), NÃO dá pra adicionar: diga ao user que não encontrou nos providers (não fabrique entry manual). Upsert/dedup por (userId, source, externalId). Só pergunte se ambíguo entre candidatos. FERRAMENTAS DE IA (Repertório de Ferramentas): fluxo separado (vêm do catálogo aitag, não dos providers de mídia). action=search_tools {query} acha a ferramenta no catálogo e devolve o toolId; action=add_tool {toolId, favorite?, rating?, note?} grava como mediaType 'tool' (estar no acervo já é 'usei'; favorite=true liga a estrela). Use quando o user fala 'adiciona o Midjourney/Cursor no meu repertório de ferramentas' ou 'uso tal ferramenta de IA'. Cada ferramenta aponta pra página dela no aitag.",
56
+ description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), popArticles, resolve (busca capa/ano/id nos providers server-side: OMDb/IGDB-Twitch/AniList/Google Books/iTunes). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item, update_item (status/rating/tags/note/isPublic), remove_item. CAPTURA ONE-SHOT travada na lista de providers: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), (1) infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped) e rating se citado; (2) chame action=resolve {mediaType, query}, escolha o candidato certo e faça add_item passando SÓ o source + externalId DELE + os campos pessoais (status/rating/tags/note). O servidor re-resolve no provider e grava título/capa/ano canônicos — você NÃO manda título/capa nem inventa externalId. (3) Se o resolve não achar (lista vazia/providerKeyMissing), NÃO dá pra adicionar: diga ao user que não encontrou nos providers (não fabrique entry manual). Upsert/dedup por (userId, source, externalId). Só pergunte se ambíguo entre candidatos. FERRAMENTAS DE IA (Repertório de Ferramentas): fluxo separado (vêm do catálogo aitag, não dos providers de mídia). action=search_tools {query} acha a ferramenta no catálogo e devolve o toolId; action=add_tool {toolId, favorite?, rating?, note?} grava como mediaType 'tool' (estar no acervo já é 'usei'; favorite=true liga a estrela). Use quando o user fala 'adiciona o Midjourney/Cursor no meu repertório de ferramentas' ou 'uso tal ferramenta de IA'. Cada ferramenta aponta pra página dela no aitag.",
57
57
  schema: repertorioSchema,
58
58
  handler: repertorio,
59
59
  },
@@ -113,7 +113,7 @@ export const TOOLS = {
113
113
  handler: shorts,
114
114
  },
115
115
  sapiens_video: {
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. VÍDEOS PROGRAMÁTICOS (ADMIN, sem custo): a mesa do Estúdio de Vídeo (/experimentos/films, tabela videoSpecs, 5 kinds: demo | aula-tour | essay | tipografia-musical | dataviz) opera por aqui sem browser — 'film-list' (todos os kinds; filtros filmKind/filmStatus), 'film-get' (spec inteiro por slug), 'film-upsert' (cria/atualiza por slug, idempotente; spec = objeto JSON no shape do 'Copiar spec' da tela, validação no servidor), 'film-status' (produção por slug: filmStatus + videoUrl + durationSecMeasured; o fecho do render é os três num call), 'film-publish' (Acervo aba Fitas + portfólio; exige pronto+URL), 'film-delete' (limpar rascunho). O RENDER do filme segue no agente local (skill /film, repo da casa): o MCP registra e fecha o ciclo, não renderiza. 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).",
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. FRAME POR ARQUIVO LOCAL (só no MCP instalado/stdio, não no remoto): startImagePath/endImagePath = caminho absoluto de uma imagem no seu PC (PNG/JPEG/WebP até 8MB); o processo lê o arquivo e sobe como frame inicial/final, igual a subir no gerador do site — 1 imagem inicial + 1 final por vídeo, então pra vários vídeos rode create uma vez por imagem. No remoto use id/url. 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. VÍDEOS PROGRAMÁTICOS (ADMIN, sem custo): a mesa do Estúdio de Vídeo (/experimentos/films, tabela videoSpecs, 5 kinds: demo | aula-tour | essay | tipografia-musical | dataviz) opera por aqui sem browser — 'film-list' (todos os kinds; filtros filmKind/filmStatus), 'film-get' (spec inteiro por slug), 'film-upsert' (cria/atualiza por slug, idempotente; spec = objeto JSON no shape do 'Copiar spec' da tela, validação no servidor), 'film-status' (produção por slug: filmStatus + videoUrl + durationSecMeasured; o fecho do render é os três num call), 'film-publish' (Acervo aba Fitas + portfólio; exige pronto+URL), 'film-delete' (limpar rascunho). O RENDER do filme segue no agente local (skill /film, repo da casa): o MCP registra e fecha o ciclo, não renderiza. 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).",
117
117
  schema: videoSchema,
118
118
  handler: video,
119
119
  },
@@ -133,12 +133,12 @@ export const TOOLS = {
133
133
  handler: brand,
134
134
  },
135
135
  sapiens_character: {
136
- description: "Personagens (character sheets) do Sapiens — a tabela `influencers`: personagem reutilizável com imagens (pra character-lock em geração) + alma (systemPrompt), tudo amarrado à conta do dono do token (sem admin). Sub-actions: list_public (catálogo global de personagens públicos do Explorar; cada um traz mainImageUrl/imageUrls usáveis direto como referenceImageUrls em sapiens_image; sem custo, sem login), get (detalhe de 1 por characterId — público+ativo qualquer um vê, draft/privado só o dono; systemPrompt só volta pro dono), list_mine (os personagens do próprio user, inclui drafts/privados), create (cria rascunho na conta: name + gender + opcional title/systemPrompt), add_image (adiciona imagem ao próprio personagem via imageUrl público OU sourceImageId da galeria; 1ª vira principal), set_card (edita alma/título/nome do próprio), activate (publica, sai de draft, exige ≥1 imagem), set_visibility (isPublic true=Explorar+slug / false=privado). Fluxo de criação: create → add_image (1+) → set_card (opcional) → activate → set_visibility isPublic=true. Pra usar um personagem público como referência numa geração, pegue mainImageUrl em list_public/get e passe em sapiens_image referenceImageUrls.",
136
+ description: "Personagens (character sheets) do Sapiens — a tabela `influencers`: personagem reutilizável com imagens (pra character-lock em geração) + alma (systemPrompt), tudo amarrado à conta do dono do token (sem admin). Sub-actions: list_public (catálogo global de personagens públicos do Explorar; cada um traz mainImageUrl/imageUrls usáveis direto como referenceImageUrls em sapiens_image; sem custo, sem login), get (detalhe de 1 por characterId — público+ativo qualquer um vê, draft/privado só o dono; systemPrompt só volta pro dono), list_mine (os personagens do próprio user, inclui drafts/privados), create (cria rascunho na conta: name + gender + opcional title/systemPrompt), add_image (adiciona imagem ao próprio personagem via imageUrl público OU sourceImageId da galeria; 1ª vira principal), set_card (edita alma/título/nome do próprio), activate (publica, sai de draft, exige ≥1 imagem), set_visibility (isPublic true=Explorar+slug / false=privado). GESTÃO de imagem (por url, pegue as urls atuais em action=get campo imageUrls): remove_image (tira uma), set_main_image (define a principal), reorder_images (nova ordem via orderedUrls, posição 0=principal), e delete (apaga o personagem, permanente). Fluxo de criação: create → add_image (1+) → set_card (opcional) → activate → set_visibility isPublic=true. Pra usar um personagem público como referência numa geração, pegue mainImageUrl em list_public/get e passe em sapiens_image referenceImageUrls.",
137
137
  schema: characterSchema,
138
138
  handler: character,
139
139
  },
140
140
  sapiens_profile: {
141
- description: "O 'tudo junto' do perfil do user (/u/<username>), leitura user-tier. Agrega o que mora no perfil mas estava fora do MCP: identidade + nível/XP + saldo, badges (conquistas) e golden tools (favoritos do aitag). Sub-actions: get (card completo: identidade + nível + saldo + badges + golden tools), badges (só as conquistas, lista cheia), golden_tools (só os favoritos do aitag, lista cheia), notifications (suas notificações recentes do sino + contagem de não-lidas), mark_read (marca uma notificationId ou TODAS as não-lidas como lidas). As partes grandes do perfil NÃO são duplicadas aqui, têm tool própria: imagens geradas/publicadas=sapiens_gallery, repertório (filmes/séries/jogos/livros/música)=sapiens_repertorio, personagens=sapiens_character, persona/arquétipo MBTI=sapiens_persona action=my_profile, saldo detalhado por bucket=sapiens_meta action=subscription. Monta a partir de queries já em prod (sem custo).",
141
+ description: "O 'tudo junto' do perfil do user (/u/<username>), user-tier. Agrega o que mora no perfil mas estava fora do MCP: identidade + nível/XP + saldo, badges (conquistas) e golden tools (favoritos do aitag). Sub-actions de LEITURA: get (card completo: identidade + nível + saldo + badges + golden tools), badges (só as conquistas, lista cheia), golden_tools (só os favoritos do aitag, lista cheia), notifications (suas notificações recentes do sino + contagem de não-lidas), mark_read (marca uma notificationId ou TODAS as não-lidas como lidas). Sub-actions de ESCRITA (mexem na SUA conta; identidade sempre da sessão): follow/unfollow (seguir/deixar de seguir outro user por followingId=users:_id, descoberto via sapiens_community participants/search_users), update_bio (edita a sua bio), update_username (troca o seu @; inválido/tomado volta {success:false,error}). As partes grandes do perfil NÃO são duplicadas aqui, têm tool própria: imagens geradas/publicadas=sapiens_gallery, repertório (filmes/séries/jogos/livros/música)=sapiens_repertorio, personagens=sapiens_character, persona/arquétipo MBTI=sapiens_persona action=my_profile, saldo detalhado por bucket=sapiens_meta action=subscription. Monta a partir de queries já em prod (sem custo).",
142
142
  schema: profileSchema,
143
143
  handler: profile,
144
144
  },
@@ -21,6 +21,10 @@ import { convexQuery, convexMutation, getSessionToken } from "../convexClient.js
21
21
  * - set_card: edita a alma (systemPrompt), título e/ou nome do próprio.
22
22
  * - activate: publica (sai de draft). Exige ≥1 imagem.
23
23
  * - set_visibility: público (entra no Explorar, ganha slug) ou privado.
24
+ * - remove_image: tira UMA imagem do próprio personagem (por url).
25
+ * - set_main_image: define a principal (por url, entre as que já existem).
26
+ * - reorder_images: reordena as imagens (posição 0 = principal).
27
+ * - delete: apaga o próprio personagem (permanente).
24
28
  *
25
29
  * Fluxo típico de criação: create → add_image (1+) → set_card (opcional) →
26
30
  * activate → set_visibility isPublic=true.
@@ -35,6 +39,10 @@ export const characterSchema = z.object({
35
39
  "set_card",
36
40
  "activate",
37
41
  "set_visibility",
42
+ "remove_image",
43
+ "set_main_image",
44
+ "reorder_images",
45
+ "delete",
38
46
  ]),
39
47
  characterId: z
40
48
  .string()
@@ -63,7 +71,11 @@ export const characterSchema = z.object({
63
71
  .describe("Pra create/set_card: a 'alma' do personagem (personalidade, jeito de falar, contexto). Usado no chat e como guia de geração."),
64
72
  imageUrl: httpUrl()
65
73
  .optional()
66
- .describe("Pra add_image: URL pública da imagem (Bunny CDN / Convex storage). Use a `url` que sapiens_image/sapiens_gallery devolvem."),
74
+ .describe("Pra add_image: URL pública da imagem (Bunny CDN / Convex storage). Use a `url` que sapiens_image/sapiens_gallery devolvem. Pra remove_image/set_main_image: a url da imagem JÁ no personagem (pegue via action=get, campo imageUrls)."),
75
+ orderedUrls: z
76
+ .array(z.string())
77
+ .optional()
78
+ .describe("Pra reorder_images: as urls das imagens do personagem na nova ordem (posição 0 = principal). url ausente vai pro fim, nenhuma se perde. Pegue as urls atuais via action=get."),
67
79
  sourceImageId: z
68
80
  .string()
69
81
  .optional()
@@ -188,4 +200,54 @@ export async function character(args) {
188
200
  isPublic: args.isPublic,
189
201
  });
190
202
  }
203
+ // -------- remove_image: tira UMA imagem (por url) --------
204
+ if (args.action === "remove_image") {
205
+ if (!args.characterId)
206
+ throw new Error("action=remove_image exige characterId.");
207
+ if (!args.imageUrl)
208
+ throw new Error("action=remove_image exige imageUrl (a url da imagem no personagem; veja em action=get).");
209
+ const sessionToken = getSessionToken();
210
+ return await convexMutation("influencers:mcpRemoveCharacterImage", {
211
+ sessionToken,
212
+ characterId: args.characterId,
213
+ imageUrl: args.imageUrl,
214
+ });
215
+ }
216
+ // -------- set_main_image: define a principal (por url) --------
217
+ if (args.action === "set_main_image") {
218
+ if (!args.characterId)
219
+ throw new Error("action=set_main_image exige characterId.");
220
+ if (!args.imageUrl)
221
+ throw new Error("action=set_main_image exige imageUrl (uma das imagens já no personagem; veja em action=get).");
222
+ const sessionToken = getSessionToken();
223
+ return await convexMutation("influencers:mcpSetCharacterMainImage", {
224
+ sessionToken,
225
+ characterId: args.characterId,
226
+ imageUrl: args.imageUrl,
227
+ });
228
+ }
229
+ // -------- reorder_images: nova ordem (posição 0 = principal) --------
230
+ if (args.action === "reorder_images") {
231
+ if (!args.characterId)
232
+ throw new Error("action=reorder_images exige characterId.");
233
+ if (!args.orderedUrls || args.orderedUrls.length === 0) {
234
+ throw new Error("action=reorder_images exige orderedUrls (as urls na nova ordem; veja em action=get).");
235
+ }
236
+ const sessionToken = getSessionToken();
237
+ return await convexMutation("influencers:mcpReorderCharacterImages", {
238
+ sessionToken,
239
+ characterId: args.characterId,
240
+ orderedUrls: args.orderedUrls,
241
+ });
242
+ }
243
+ // -------- delete: apaga o próprio personagem (permanente) --------
244
+ if (args.action === "delete") {
245
+ if (!args.characterId)
246
+ throw new Error("action=delete exige characterId.");
247
+ const sessionToken = getSessionToken();
248
+ return await convexMutation("influencers:mcpDeleteCharacter", {
249
+ sessionToken,
250
+ characterId: args.characterId,
251
+ });
252
+ }
191
253
  }
@@ -37,18 +37,22 @@ const FORMAT_GUIDE = {
37
37
  },
38
38
  },
39
39
  carrossel_ig: {
40
- description: "Carrossel Instagram com múltiplos slides texto + imagem.",
40
+ description: "Carrossel Instagram editorial (7-9 slides). O editor da pipeline renderiza por TEMPLATE: cada slide é { templateId, slots } (não { title, body } cru — esse shape genérico abre VAZIO no editor). NÃO monte o payload na mão: use sapiens_pipeline action=generate_carousel_production (sourceId), que cria a production E preenche pelo motor da casa, no shape certo, seguindo o artigo. Depois afine com get_carousel/update_carousel (mesmo shape editorial). O create_production com payload cru é legado pra quem já tem o shape editorial pronto.",
41
41
  payloadHint: {
42
+ meta: {
43
+ caption: "string (legenda do post)",
44
+ hashtags: ["string"],
45
+ keyword: "string MAIÚSCULA (gatilho do comment-to-DM, ex: QUERO)",
46
+ },
47
+ theme: { palette: "editorial-ink | editorial-dark | editorial-cream | editorial-bone" },
42
48
  slides: [
43
49
  {
44
- title: "string opcional",
45
- body: "string",
46
- imagePrompt: "string (pra gerador de imagem)",
47
- imageUrl: "string (preenche depois de gerar)",
50
+ id: "slide-1",
51
+ templateId: "capa-tipografica-b | corpo-texto-puro-a | corpo-citacao-a | corpo-lista-a | corpo-dado-a | corpo-versus-a | corpo-foto-metade-a | cta-artigo-a (catálogo completo + slots por template vem de get_carousel)",
52
+ slots: { "...": "slots do template escolhido (ex: kicker, title, body)" },
53
+ image: "null, ou objeto { url, stockImageId, focal, zoom } nos templates de foto",
48
54
  },
49
55
  ],
50
- caption: "string",
51
- hashtags: ["string"],
52
56
  },
53
57
  },
54
58
  tirinha: {
@@ -35,15 +35,17 @@ const MBTI_GROUPS = {
35
35
  ENFP: "NF", INFP: "NF", ENFJ: "NF", INFJ: "NF",
36
36
  ENTP: "NT", INTP: "NT", ENTJ: "NT", INTJ: "NT",
37
37
  };
38
+ // Nomes autorais da casa (renome 2026-07-18, sync com o site:
39
+ // apps/sapiens/src/app/experimentos/persona-sapiens/data/personas.ts).
38
40
  const MBTI_NAMES = {
39
- INTJ: "O Estrategista", INTP: "O Lógico",
40
- ENTJ: "O Comandante", ENTP: "O Inovador",
41
- INFJ: "O Defensor", INFP: "O Mediador",
42
- ENFJ: "O Protagonista", ENFP: "O Ativista",
43
- ISTJ: "O Logístico", ISFJ: "O Defensor (concreto)",
44
- ESTJ: "O Executivo", ESFJ: "O Cônsul",
45
- ISTP: "O Virtuoso", ISFP: "O Aventureiro",
46
- ESTP: "O Empreendedor", ESFP: "O Animador",
41
+ INTJ: "Cientista", INTP: "Cypherpunk",
42
+ ENTJ: "Fundador", ENTP: "Inventor",
43
+ INFJ: "Editor", INFP: "Poeta",
44
+ ENFJ: "Maestro", ENFP: "Faísca",
45
+ ISTJ: "Arquivista", ISFJ: "Guardião",
46
+ ESTJ: "Produtor", ESFJ: "Anfitrião",
47
+ ISTP: "Mecânico", ISFP: "Artesão",
48
+ ESTP: "Negociador", ESFP: "Streamer",
47
49
  };
48
50
  // Escala Likert 1..7 (mesma do quiz no site).
49
51
  const LIKERT_SCALE = {
@@ -126,6 +128,28 @@ const QUIZ_QUESTIONS = [
126
128
  { id: "jp-11", axis: "JP", text: "Programas de viagem detalhados me deixam tranquilo, não entediado." },
127
129
  { id: "jp-12", axis: "JP", text: "Eu tendo a deixar várias abas mentais abertas ao mesmo tempo." },
128
130
  ];
131
+ // ============================================================
132
+ // BLOCO DA CASA: par de jogo (8 perguntas de vínculo, OPCIONAIS).
133
+ // Não mudam o tipo; medem COMO a pessoa quer ser acompanhada (2 eixos:
134
+ // Sparring↔Acolhimento, Direção↔Execução) → 1 de 4 modos de parceiro
135
+ // (Mestre de Jogo, Sparring, Farol, Copiloto), que calibra o Sintético dela.
136
+ // SYNC: espelho de data/vinculoQuestions.ts (site) e VINCULO_CONFIG
137
+ // (convex/personalityProfiles.ts, scoring server-side).
138
+ // ============================================================
139
+ const VINCULO_QUESTIONS = [
140
+ { id: "vn-sa-1", axis: "SA", text: "Rendo mais quando alguém discorda de mim com força." },
141
+ { id: "vn-sa-2", axis: "SA", text: "Quando travo, o que me destrava é escuta, não cobrança." },
142
+ { id: "vn-sa-3", axis: "SA", text: "Prefiro um 'isso tá fraco' na cara a um elogio morno." },
143
+ { id: "vn-sa-4", axis: "SA", text: "Ideia recém-nascida precisa de espaço seguro antes de aguentar pancada." },
144
+ { id: "vn-de-1", axis: "DE", text: "Quero alguém que aponte o caminho e me deixe caminhar sozinho." },
145
+ { id: "vn-de-2", axis: "DE", text: "Plano bom é plano que alguém constrói comigo, mão na massa." },
146
+ { id: "vn-de-3", axis: "DE", text: "Um bom mapa me serve mais que companhia na trilha." },
147
+ { id: "vn-de-4", axis: "DE", text: "Ideia boa minha morre por falta de alguém segurando a ponta prática." },
148
+ ];
149
+ const VINCULO_AXES_INFO = {
150
+ SA: "Sparring vs Acolhimento — a pessoa quer que a confrontem ou que a sustentem.",
151
+ DE: "Direção vs Execução — quer quem aponta o caminho ou quem senta e faz junto.",
152
+ };
129
153
  export const personaSchema = z.object({
130
154
  action: z.enum([
131
155
  "my_profile",
@@ -146,6 +170,13 @@ export const personaSchema = z.object({
146
170
  }))
147
171
  .optional()
148
172
  .describe("Pra action=submit_quiz: as 48 respostas { questionId, value 1..7 }. Pegue os IDs/perguntas com action=get_quiz e colete tudo antes."),
173
+ vinculoAnswers: z
174
+ .array(z.object({
175
+ questionId: z.string(),
176
+ value: z.number().int().min(1).max(7),
177
+ }))
178
+ .optional()
179
+ .describe("Pra action=submit_quiz: as 8 respostas do bloco da casa (par de jogo, ids vn-*). Opcional, mas recomendado: destrava o modo de parceiro e a calibração do Sintético."),
149
180
  nome: z
150
181
  .string()
151
182
  .optional()
@@ -155,15 +186,21 @@ export async function persona(args) {
155
186
  // -------- get_quiz: as 48 perguntas pra aplicar conversando (estático) --------
156
187
  if (args.action === "get_quiz") {
157
188
  return {
158
- totalQuestions: QUIZ_QUESTIONS.length,
189
+ totalQuestions: QUIZ_QUESTIONS.length + VINCULO_QUESTIONS.length,
159
190
  scale: LIKERT_SCALE,
160
191
  axes: AXES_INFO,
161
192
  instructions: "Aplique conversando: apresente as perguntas (pode ir em blocos por eixo) e " +
162
193
  "peça pra pessoa responder de 1 (Discordo totalmente) a 7 (Concordo totalmente). " +
163
- "Junte TODAS as 48 respostas e chame action=submit_quiz com answers=[{questionId, value}]. " +
194
+ "Junte TODAS as 48 respostas de `questions` e chame action=submit_quiz com answers=[{questionId, value}]. " +
195
+ "Depois das 48, aplique também o BLOCO DA CASA (`vinculoQuestions`, 8 itens, mesma escala): " +
196
+ "ele descobre o par de jogo da pessoa (como ela quer ser acompanhada) e calibra o Sintético dela. " +
197
+ "Essas 8 vão SEPARADAS, no arg vinculoAnswers. " +
164
198
  "Não calcule o resultado você mesmo: o scoring é server-side.",
165
199
  questions: QUIZ_QUESTIONS,
166
- howToSubmit: "sapiens_persona action=submit_quiz answers=[{questionId:'ei-1', value:5}, ...] (48 itens, value 1..7).",
200
+ vinculoQuestions: VINCULO_QUESTIONS,
201
+ vinculoAxes: VINCULO_AXES_INFO,
202
+ howToSubmit: "sapiens_persona action=submit_quiz answers=[{questionId:'ei-1', value:5}, ...] (48 itens) " +
203
+ "vinculoAnswers=[{questionId:'vn-sa-1', value:6}, ...] (8 itens, opcional).",
167
204
  };
168
205
  }
169
206
  // -------- submit_quiz: salva o resultado no perfil do user (de graça) --------
@@ -177,6 +214,10 @@ export async function persona(args) {
177
214
  const res = await convexMutation("mcpExtras:mcpSubmitPersonaQuiz", {
178
215
  sessionToken,
179
216
  answers: answers.map((a) => ({ questionId: a.questionId, value: a.value })),
217
+ // Bloco da casa (par de jogo): opcional; só vai se coletado completo.
218
+ ...(args.vinculoAnswers?.length
219
+ ? { vinculoAnswers: args.vinculoAnswers.map((a) => ({ questionId: a.questionId, value: a.value })) }
220
+ : {}),
180
221
  nome: args.nome,
181
222
  });
182
223
  return {
@@ -205,10 +246,10 @@ export async function persona(args) {
205
246
  group: MBTI_GROUPS[c],
206
247
  })),
207
248
  groups: {
208
- NT: "Analistas pensa em sistemas",
209
- NF: "Diplomatas pensa em pessoas",
210
- SJ: "Sentinelas pensa em ordem",
211
- SP: "Exploradores pensa em sensação",
249
+ NT: "Direção (traça o rumo, pensa em sistemas)",
250
+ NF: "Roteiro (escreve o sentido, pensa em pessoas)",
251
+ SJ: "Produção (faz rodar, pensa em ordem)",
252
+ SP: "Cena (joga em tempo real, pensa em sensação)",
212
253
  },
213
254
  };
214
255
  }
@@ -43,6 +43,7 @@ export const pipelineSchema = z.object({
43
43
  "propose_mega_grafico_plan",
44
44
  "run_mega_grafico_full",
45
45
  "generate_carousel",
46
+ "generate_carousel_production",
46
47
  "list_carousels",
47
48
  "get_carousel",
48
49
  "update_carousel",
@@ -124,7 +125,7 @@ export const pipelineSchema = z.object({
124
125
  sourceId: z
125
126
  .string()
126
127
  .optional()
127
- .describe("contentSources:_id — get_source/create_production/remove_source/set_source_done/update_source_notes. Vem de list_sources/add_article_as_source."),
128
+ .describe("contentSources:_id — get_source/create_production/generate_carousel_production/remove_source/set_source_done/update_source_notes. Vem de list_sources/add_article_as_source."),
128
129
  productionId: z
129
130
  .string()
130
131
  .optional()
@@ -383,6 +384,22 @@ export async function pipeline(args) {
383
384
  useSintetico: args.useSintetico,
384
385
  });
385
386
  }
387
+ case "generate_carousel_production": {
388
+ // Gera um carrossel EDITORIAL como PRODUCTION da pipeline, a partir do
389
+ // artigo de um source (sourceId de list_sources/add_article_as_source):
390
+ // cria a production carrossel_ig e a preenche pelo MESMO motor do editor
391
+ // (shape templateId+slots), então o resultado JÁ abre pronto no editor de
392
+ // pipeline, ao contrário do create_production cru. ADMIN-ONLY, cobra
393
+ // Sinapses (reembolsa se falhar). Devolve productionId + url do editor.
394
+ // SÍNCRONA e pesada (Gemini + imagens): vale a REGRA DO TIMEOUT — se voltar
395
+ // Timeout, confira em get_production/list_sources antes de repetir.
396
+ return await convexAction("carouselAutofill:mcpFillCarouselFromArticle", {
397
+ sessionToken,
398
+ sourceId: need(args.sourceId, "sourceId"),
399
+ autoPickImages: args.autoPickImages,
400
+ useSintetico: args.useSintetico,
401
+ });
402
+ }
386
403
  case "list_carousels":
387
404
  // Carrosséis standalone do dono (id + título + url do editor).
388
405
  return await convexQuery("carouselMcp:mcpListCarousels", { sessionToken });
@@ -22,6 +22,9 @@ import { convexQuery, convexMutation, getSessionToken } from "../convexClient.js
22
22
  * - golden_tools: só os favoritos do aitag (lista cheia).
23
23
  * - notifications: suas notificações recentes (sino) + contagem de não-lidas.
24
24
  * - mark_read: marca uma (notificationId) ou TODAS as suas não-lidas como lidas.
25
+ * - follow/unfollow: segue/deixa de seguir outro usuário (followingId).
26
+ * - update_bio: edita a SUA bio.
27
+ * - update_username: troca o SEU @username.
25
28
  */
26
29
  export const profileSchema = z.object({
27
30
  action: z.enum([
@@ -30,6 +33,10 @@ export const profileSchema = z.object({
30
33
  "golden_tools",
31
34
  "notifications",
32
35
  "mark_read",
36
+ "follow",
37
+ "unfollow",
38
+ "update_bio",
39
+ "update_username",
33
40
  ]),
34
41
  limit: z
35
42
  .number()
@@ -39,6 +46,18 @@ export const profileSchema = z.object({
39
46
  .string()
40
47
  .optional()
41
48
  .describe("Pra mark_read: o id de UMA notificação (vem de action=notifications). Omita pra marcar TODAS as não-lidas."),
49
+ followingId: z
50
+ .string()
51
+ .optional()
52
+ .describe("Pra follow/unfollow: users:_id de QUEM seguir/deixar de seguir. Você (o seguidor) é sempre o dono da sessão. Descubra o id via sapiens_community participants/search_users ou sapiens_search."),
53
+ bio: z
54
+ .string()
55
+ .optional()
56
+ .describe("Pra update_bio: o novo texto da sua bio (perfil /u/<username>)."),
57
+ newUsername: z
58
+ .string()
59
+ .optional()
60
+ .describe("Pra update_username: o novo @ (3-20 chars, alfanumérico + underscore). Já em uso ou inválido volta {success:false,error}."),
42
61
  });
43
62
  const APP = "https://sapiensinteticos.com";
44
63
  async function resolveUser(sessionToken) {
@@ -92,6 +111,40 @@ export async function profile(args) {
92
111
  : `Marquei ${res.updated} notificação(ões) como lida(s).`,
93
112
  };
94
113
  }
114
+ // Escritas do perfil (autenticam pelo sessionToken; identidade sempre do
115
+ // token, o cliente só escolhe alvo/valor). Despacham antes do resolveUser.
116
+ if (args.action === "follow" || args.action === "unfollow") {
117
+ if (!args.followingId?.trim()) {
118
+ throw new Error(`action=${args.action} exige followingId (users:_id de quem seguir). Descubra via sapiens_community participants/search_users.`);
119
+ }
120
+ const fn = args.action === "follow" ? "users:mcpFollowUser" : "users:mcpUnfollowUser";
121
+ const res = await convexMutation(fn, {
122
+ sessionToken,
123
+ followingId: args.followingId.trim(),
124
+ });
125
+ return { ...res, action: args.action };
126
+ }
127
+ if (args.action === "update_bio") {
128
+ if (typeof args.bio !== "string") {
129
+ throw new Error("action=update_bio exige bio (o novo texto do perfil).");
130
+ }
131
+ const res = await convexMutation("users:mcpUpdateBio", {
132
+ sessionToken,
133
+ bio: args.bio,
134
+ });
135
+ return { ...res, note: "Bio atualizada." };
136
+ }
137
+ if (args.action === "update_username") {
138
+ if (!args.newUsername?.trim()) {
139
+ throw new Error("action=update_username exige newUsername (3-20 chars).");
140
+ }
141
+ const res = await convexMutation("users:mcpUpdateUsername", {
142
+ sessionToken,
143
+ newUsername: args.newUsername.trim(),
144
+ });
145
+ // doUpdateUsername devolve {success:false,error} quando inválido/tomado.
146
+ return res;
147
+ }
95
148
  const user = await resolveUser(sessionToken);
96
149
  const userId = user._id;
97
150
  if (args.action === "badges") {
@@ -4,8 +4,10 @@ import { need } from "../schema.js";
4
4
  /**
5
5
  * Acesso ao Repertório do Sapiens (acervo pessoal de filme/série/anime/jogo/livro/música).
6
6
  *
7
- * Reads (v1.0): list, search, get, lists, popArticles. Sem auth necessária —
8
- * só vê items públicos (isPublic !== false).
7
+ * Reads (v1.0): list, search, get, popArticles. Sem auth necessária —
8
+ * só vê items públicos (isPublic !== false). (A action `lists` saiu em jul/2026:
9
+ * as listas curadas foram removidas do produto em 2026-06-22, e a query
10
+ * repertorio:listLists não existe mais no backend.)
9
11
  *
10
12
  * Mutations (v1.1+): add_item, update_item, remove_item. Usam sessionToken
11
13
  * e valem pra QUALQUER conta logada (`requireMcpUser`) — cada um mexe no
@@ -36,7 +38,6 @@ export const repertorioSchema = z.object({
36
38
  "list",
37
39
  "search",
38
40
  "get",
39
- "lists",
40
41
  "popArticles",
41
42
  "resolve",
42
43
  "add_item",
@@ -197,21 +198,6 @@ export async function repertorio(args) {
197
198
  if (!args.userId) {
198
199
  throw new Error(`action=${args.action} exige userId. Use sapiens_meta action=whoami pra descobrir o user atual.`);
199
200
  }
200
- if (args.action === "lists") {
201
- const lists = await convexQuery("repertorio:listLists", {
202
- userId: args.userId,
203
- });
204
- return {
205
- count: Array.isArray(lists) ? lists.length : 0,
206
- lists: (lists || []).map((l) => ({
207
- _id: l._id,
208
- name: l.name,
209
- slug: l.slug,
210
- description: l.description,
211
- isPublic: l.isPublic,
212
- })),
213
- };
214
- }
215
201
  if (args.action === "get") {
216
202
  const item = await convexQuery("repertorio:getById", {
217
203
  itemId: need(args.itemId, "itemId"),
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { convexAction, convexQuery, convexMutation, getSessionToken } from "../convexClient.js";
2
+ import { convexAction, convexQuery, convexMutation, getSessionToken, isRemoteContext, } from "../convexClient.js";
3
3
  import { httpUrl } from "../schema.js";
4
4
  /**
5
5
  * Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as
@@ -41,9 +41,12 @@ import { httpUrl } from "../schema.js";
41
41
  * - sapiens-video-kling Kling 3.0 Pro — dá vida a uma imagem, 3-15s, sound opcional (i2v/t2v)
42
42
  * - sapiens-video-wan WAN 2.5 — imagem que fala/canta (áudio+lip-sync nativo), 5/10s (i2v)
43
43
  * - sapiens-video-kling-motion Kling Motion — transfere o movimento de um vídeo pra uma imagem
44
- * (PRECISA de pessoa com tronco visível na imagem E no vídeo)
44
+ * (PRECISA de pessoa com tronco visível na imagem E no vídeo;
45
+ * vídeo de referência MÁX 10s — o provider recusa acima disso —
46
+ * e cobra pela duração do clipe; corte antes de mandar)
45
47
  * - sapiens-video-shot-mimic Shot Mimic — recria o plano do vídeo de referência (câmera,
46
48
  * cortes, blocking) como cena nova; personagem via role 'start'
49
+ * (vídeo de referência MÁX 15s — acima o provider corta em 15s)
47
50
  * - sapiens-video-lite/fast/quality Veo 3.1 (2000/5000/25000 sinapses)
48
51
  * - sapiens-video-omni Gemini Omni — texto -> vídeo 10s 720p com áudio nativo embutido.
49
52
  * t2v + EDIÇÃO conversacional: `editOfImageId` aponta um vídeo Omni
@@ -150,8 +153,8 @@ export const videoSchema = z.object({
150
153
  .optional()
151
154
  .describe("action=create: modelo de vídeo. 'sapiens-video-seedance' (cinematográfico+áudio, t2v/i2v), " +
152
155
  "'sapiens-video-kling' (anima imagem, i2v/t2v), 'sapiens-video-wan' (imagem que fala, i2v), " +
153
- "'sapiens-video-kling-motion' (motion transfer, precisa pessoa na imagem E no vídeo de movimento), " +
154
- "'sapiens-video-shot-mimic' (recria o plano do vídeo de referência com seu personagem: mesma câmera, mesmos cortes; 'driving' = previs/clipe do plano, 'start' = personagem), " +
156
+ "'sapiens-video-kling-motion' (motion transfer, precisa pessoa na imagem E no vídeo de movimento; vídeo de referência MÁX 10s, cobra pela duração do clipe), " +
157
+ "'sapiens-video-shot-mimic' (recria o plano do vídeo de referência com seu personagem: mesma câmera, mesmos cortes; 'driving' = previs/clipe do plano MÁX 15s, 'start' = personagem), " +
155
158
  "'sapiens-video-lite/fast/quality' (Veo 3.1), " +
156
159
  "'sapiens-video-omni' (Gemini Omni: texto -> vídeo 10s 720p com áudio nativo; t2v + EDIÇÃO conversacional via editOfImageId; não aceita imagem/vídeo do user, ignora duração/resolução)."),
157
160
  durationSec: z
@@ -202,7 +205,7 @@ export const videoSchema = z.object({
202
205
  .describe("'start' = imagem inicial (i2v), 'end' = frame final, 'driving' = vídeo de movimento (Motion)"),
203
206
  }))
204
207
  .optional()
205
- .describe("References em base64 (escape hatch / Motion / Shot Mimic). i2v: role 'start' (imagem). Motion: 'start' (pessoa) + 'driving' (vídeo de movimento, <=5MB). Shot Mimic: 'start' (personagem) + 'driving' (previs ou clipe do plano a imitar, <=5MB). Pra frame inicial/final a partir do seu acervo, prefira start/endImage* abaixo (sem precisar de base64)."),
208
+ .describe("References em base64 (escape hatch / Motion / Shot Mimic). i2v: role 'start' (imagem). Motion: 'start' (pessoa) + 'driving' (vídeo de movimento, <=5MB, MÁX 10s — o Kling Motion recusa referência acima de 10s e cobra pela duração do clipe; corte o trecho antes). Shot Mimic: 'start' (personagem) + 'driving' (previs ou clipe do plano a imitar, <=5MB, MÁX 15s — acima o provider corta em 15s). Pra frame inicial/final a partir do seu acervo, prefira start/endImage* abaixo (sem precisar de base64)."),
206
209
  // Frame inicial/final por REFERÊNCIA (resolvido server-side, igual à imagem):
207
210
  // id da própria galeria OU url allowlist (galeria/Acervo/personagens). Mais
208
211
  // simples que mandar base64. Descubra via sapiens_reference / sapiens_gallery.
@@ -220,7 +223,91 @@ export const videoSchema = z.object({
220
223
  endImageUrl: httpUrl()
221
224
  .optional()
222
225
  .describe("Frame FINAL: url pública de galeria/Acervo/personagem. Vira reference role 'end' (suporte varia por modelo)."),
226
+ // Frame inicial/final por ARQUIVO LOCAL (paridade com o upload do gerador do
227
+ // site). Só no MCP instalado (stdio): o processo lê o arquivo do disco e sobe
228
+ // como reference role 'start'/'end', sem o base64 passar pelo contexto do
229
+ // modelo. No remoto é recusado (o path seria do servidor, não do usuário).
230
+ startImagePath: z
231
+ .string()
232
+ .optional()
233
+ .describe("Frame INICIAL (i2v) a partir de um ARQUIVO LOCAL do seu PC — só no MCP instalado (stdio), não na conexão remota. " +
234
+ "Passe o caminho absoluto (ex: 'C:\\\\Users\\\\voce\\\\HERO\\\\1.png'); o processo lê o arquivo e sobe como frame inicial, igual a subir a imagem no gerador do site. PNG/JPEG/WebP, até 8MB. " +
235
+ "É 1 imagem inicial por vídeo (o modelo do site): pra vários vídeos, rode create uma vez por imagem. Mutuamente exclusivo com startImageId/startImageUrl."),
236
+ endImagePath: z
237
+ .string()
238
+ .optional()
239
+ .describe("Frame FINAL a partir de um ARQUIVO LOCAL do seu PC — só no MCP instalado (stdio). Caminho absoluto; PNG/JPEG/WebP até 8MB. Vira reference role 'end' (suporte varia por modelo). 1 imagem por vídeo. Mutuamente exclusivo com endImageId/endImageUrl."),
223
240
  });
241
+ // Teto do arquivo local que vira frame de vídeo. Frame inicial/final não precisa
242
+ // ser pesado; 8MB cobre um PNG/JPEG grande com folga e evita estourar o payload
243
+ // da action (base64 infla ~33%).
244
+ const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
245
+ // Detecta o mime por MAGIC BYTES (não confia na extensão). Frame de vídeo aceita
246
+ // só PNG/JPEG/WebP; qualquer outra coisa é recusada com erro claro.
247
+ export function detectImageMime(buf) {
248
+ if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
249
+ return "image/png";
250
+ }
251
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
252
+ return "image/jpeg";
253
+ }
254
+ if (buf.length >= 12 &&
255
+ buf.toString("ascii", 0, 4) === "RIFF" &&
256
+ buf.toString("ascii", 8, 12) === "WEBP") {
257
+ return "image/webp";
258
+ }
259
+ return null;
260
+ }
261
+ // Lê um arquivo de imagem do disco e devolve uma reference base64 pro role dado.
262
+ // Guarda de segurança: só no stdio (o processo roda na máquina do dono do token).
263
+ // No remoto recusa ANTES de tocar o disco — ler um path arbitrário lá seria
264
+ // leitura de arquivo do servidor multi-tenant.
265
+ async function readLocalImageAsReference(filePath, role) {
266
+ if (isRemoteContext()) {
267
+ throw new Error(`${role}ImagePath (arquivo local) só funciona no MCP instalado na sua máquina (stdio). ` +
268
+ `Na conexão remota, suba a imagem no site e passe ${role}ImageId (id da galeria) ou ${role}ImageUrl (host Sapiens).`);
269
+ }
270
+ const [{ default: fs }, { default: path }] = await Promise.all([
271
+ import("node:fs/promises"),
272
+ import("node:path"),
273
+ ]);
274
+ const abs = path.resolve(filePath);
275
+ let buf;
276
+ try {
277
+ buf = await fs.readFile(abs);
278
+ }
279
+ catch {
280
+ throw new Error(`Não achei/li o arquivo em ${role}ImagePath: "${filePath}". Use o caminho absoluto do arquivo (ex: C:\\Users\\voce\\HERO\\1.png).`);
281
+ }
282
+ if (buf.length > MAX_LOCAL_IMAGE_BYTES) {
283
+ const mb = (buf.length / (1024 * 1024)).toFixed(1);
284
+ throw new Error(`Imagem de ${role} muito grande (${mb}MB; teto ${Math.round(MAX_LOCAL_IMAGE_BYTES / (1024 * 1024))}MB). Exporte menor e tente de novo.`);
285
+ }
286
+ const mimeType = detectImageMime(buf);
287
+ if (!mimeType) {
288
+ throw new Error(`O arquivo de ${role} não parece PNG/JPEG/WebP ("${filePath}"). O frame inicial/final aceita só imagem nesses formatos.`);
289
+ }
290
+ return { mimeType, data: buf.toString("base64"), role };
291
+ }
292
+ // Monta as references de frame inicial/final a partir de arquivos locais,
293
+ // barrando o conflito com id/url (o site usa UMA via por frame: ou o arquivo,
294
+ // ou a imagem já hospedada). Devolve [] quando nenhum path foi passado.
295
+ export async function localFrameReferences(args) {
296
+ const refs = [];
297
+ if (args.startImagePath) {
298
+ if (args.startImageId || args.startImageUrl) {
299
+ throw new Error("Frame inicial: escolha UMA via — startImagePath (arquivo local) OU startImageId/startImageUrl (galeria/Acervo), não as duas.");
300
+ }
301
+ refs.push(await readLocalImageAsReference(args.startImagePath, "start"));
302
+ }
303
+ if (args.endImagePath) {
304
+ if (args.endImageId || args.endImageUrl) {
305
+ throw new Error("Frame final: escolha UMA via — endImagePath (arquivo local) OU endImageId/endImageUrl (galeria/Acervo), não as duas.");
306
+ }
307
+ refs.push(await readLocalImageAsReference(args.endImagePath, "end"));
308
+ }
309
+ return refs;
310
+ }
224
311
  export async function video(args) {
225
312
  // models: catálogo VIVO dos modelos de vídeo (ativos + preço-piso/config +
226
313
  // override admin + disponibilidade). Público, sem custo e sem login — antes de
@@ -357,6 +444,13 @@ export async function video(args) {
357
444
  if (!args.model) {
358
445
  throw new Error("action=create exige model (ex: sapiens-video-seedance, sapiens-video-kling, sapiens-video-wan, sapiens-video-kling-motion).");
359
446
  }
447
+ // Frame inicial/final por arquivo local (paridade com o upload do site):
448
+ // lê do disco e injeta em references role start/end. O backend
449
+ // (buildVideoReferences) já mescla references + start/end e valida o teto.
450
+ const localRefs = await localFrameReferences(args);
451
+ const references = localRefs.length
452
+ ? [...(args.references ?? []), ...localRefs]
453
+ : args.references;
360
454
  return await convexAction("mcpExtrasActions:mcpVideoCreateAndRender", {
361
455
  sessionToken,
362
456
  model: args.model,
@@ -365,7 +459,7 @@ export async function video(args) {
365
459
  durationSec: args.durationSec,
366
460
  resolution: args.resolution,
367
461
  audio: args.audio,
368
- references: args.references,
462
+ references,
369
463
  startImageId: args.startImageId,
370
464
  startImageUrl: args.startImageUrl,
371
465
  endImageId: args.endImageId,
@@ -397,12 +491,16 @@ export async function video(args) {
397
491
  if (!args.imageId) {
398
492
  throw new Error("action=generate exige imageId (vídeo já criado no site).");
399
493
  }
494
+ const localRefs = await localFrameReferences(args);
495
+ const references = localRefs.length
496
+ ? [...(args.references ?? []), ...localRefs]
497
+ : args.references;
400
498
  return await convexAction("mcpExtrasActions:mcpVideoGenerate", {
401
499
  sessionToken,
402
500
  imageId: args.imageId,
403
501
  prompt: args.prompt,
404
502
  aspectRatio: args.aspectRatio,
405
- references: args.references,
503
+ references,
406
504
  startImageId: args.startImageId,
407
505
  startImageUrl: args.startImageUrl,
408
506
  endImageId: args.endImageId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
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": {