sapiens-mcp 1.49.0 → 1.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ Precisa de Node 18+. A URL do backend já vem embutida; não precisa configurar
25
25
 
26
26
  `sapiens_meta` com `action: "login"` e `code: "XXXX-XXXX"`
27
27
 
28
- O token de 30 dias fica salvo em `~/.sapiens-mcp/session.json`. Pra sair: `sapiens_meta action=logout`.
28
+ O token vale 90 dias e renova sozinha a cada uso; fica salvo em `~/.sapiens-mcp/session.json`. Pra sair: `sapiens_meta action=logout`, que revoga a chave no servidor e apaga o arquivo local.
29
29
 
30
30
  ## O que dá pra pedir (e o custo em Sinapses)
31
31
 
@@ -44,7 +44,7 @@ O Claude avisa o custo antes de gastar, e geração que falha é estornada. Publ
44
44
 
45
45
  ## Troubleshooting
46
46
 
47
- - **"sessionToken expirado" / "Conta Sapiens não conectada".** O token de 30 dias venceu ou nunca foi salvo. Abra [sapiensinteticos.com/conectar-claude](https://www.sapiensinteticos.com/conectar-claude) logado, gere um código novo e rode `sapiens_meta action=login code=XXXX-XXXX`.
47
+ - **"sessionToken expirado" / "Conta Sapiens não conectada".** A chave venceu (90 dias sem uso) ou nunca foi salva. Abra [sapiensinteticos.com/conectar-claude](https://www.sapiensinteticos.com/conectar-claude) logado, gere um código novo e rode `sapiens_meta action=login code=XXXX-XXXX`.
48
48
  - **Ferramenta some ou some capacidade nova depois de atualizar.** O client roda via `npx -y sapiens-mcp` (sem versão fixa) e pode ter ficado preso num cache antigo. Confira o que está rodando com `sapiens_meta action=version` (mostra a versão do binário + a última do npm + `upToDate`). Se `upToDate:false`, limpe o cache do npx e reinicie o client.
49
49
  - **"Argumentos inválidos".** A mensagem já diz qual campo faltou ou saiu errado; refaça a chamada com o que ela pede. Não repita a mesma chamada que falhou (3 falhas seguidas fazem o client marcar o servidor como indisponível por ~1 min, um disjuntor anti-loop).
50
50
  - **Saldo baixo antes de gerar.** `sapiens_meta action=credits` (ou `action=subscription` pro detalhe por bucket) mostra quanto sobra antes de gastar em imagem/música/vídeo.
@@ -107,17 +107,21 @@ export function getConvex() {
107
107
  function sessionStorePath() {
108
108
  return path.resolve(os.homedir(), ".sapiens-mcp", "session.json");
109
109
  }
110
- export function saveSessionToken(token) {
110
+ // Espelho local da validade do servidor (shared/sessionTtl.ts): 90 dias em
111
+ // janela deslizante. Só serve pra não mandar um token obviamente morto pro
112
+ // backend; a validade REAL é sempre a da row no Convex, e cada uso empurra ela
113
+ // pra frente (ver touchSession abaixo, que regrava este arquivo com o novo
114
+ // expiresAt quando o servidor renova).
115
+ const LOCAL_SESSION_TTL_MS = 90 * 24 * 60 * 60 * 1000;
116
+ export function saveSessionToken(token, expiresAtOverride) {
111
117
  const file = sessionStorePath();
112
- // mode 0700/0600: o arquivo guarda um bearer de 30 dias. Sem isto, em POSIX o
118
+ // mode 0700/0600: o arquivo guarda um bearer de 90 dias. Sem isto, em POSIX o
113
119
  // dir sai 0755 e o arquivo 0644 (world-readable) — num host compartilhado (a
114
120
  // VPS da Helen) outro usuário local leria o token e assumiria a conta. mode só
115
121
  // aplica na CRIAÇÃO, então o chmod explícito cobre o caso de reescrever um
116
122
  // arquivo já existente 0644. No Windows chmod é no-op benigno (try/catch).
117
123
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
118
- // Espelha os 30 dias do server (só pra avisar quando perto de expirar; a
119
- // validade real é sempre checada no Convex).
120
- const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000;
124
+ const expiresAt = expiresAtOverride ?? Date.now() + LOCAL_SESSION_TTL_MS;
121
125
  fs.writeFileSync(file, JSON.stringify({ sessionToken: token, expiresAt }, null, 2), { encoding: "utf8", mode: 0o600 });
122
126
  try {
123
127
  fs.chmodSync(file, 0o600);
@@ -280,7 +284,11 @@ function withTimeout(p, label) {
280
284
  let timer;
281
285
  const timeout = new Promise((_, reject) => {
282
286
  timer = setTimeout(() => {
283
- reject(new Error(`Timeout: o backend Sapiens não respondeu em ${Math.round(CONVEX_TIMEOUT_MS / 1000)}s (${label}). Tente de novo em instantes.`));
287
+ reject(new Error(`Timeout: o backend Sapiens não respondeu em ${Math.round(CONVEX_TIMEOUT_MS / 1000)}s (${label}). Isto é o CLIENTE desistindo de esperar, não a ` +
288
+ `operação morrendo: ela segue rodando no servidor e, se cobra ` +
289
+ `Sinapses, JÁ cobrou. NÃO repita a chamada — repetir gera e cobra ` +
290
+ `de novo. Espere alguns minutos e leia o estado real por uma ` +
291
+ `leitura (o \`get\`/\`list\`/\`status\` do mesmo tool) antes de decidir.`));
284
292
  }, CONVEX_TIMEOUT_MS);
285
293
  });
286
294
  return Promise.race([
@@ -296,6 +304,46 @@ export async function convexMutation(fnPath, args) {
296
304
  const client = getConvex();
297
305
  return (await withTimeout(client.mutation(fnPath, args), `mutation ${fnPath}`));
298
306
  }
307
+ // ============================================
308
+ // Renovação da chave por USO (janela deslizante do servidor).
309
+ //
310
+ // A validade da conexão são 90 dias que andam pra frente a cada uso, mas quem
311
+ // empurra é o backend, e ele só sabe que a conexão está viva se alguém bater na
312
+ // porta. Este é o batidor: uma chamada barata por hora por token, best-effort
313
+ // (falhou, não atrapalha nada), e é ela que também alimenta o "último uso" da
314
+ // tela de conexões em /conectar-claude. Sem isso, quem opera todo dia era
315
+ // deslogado no aniversário do login.
316
+ //
317
+ // No stdio, quando o servidor renova, o store em disco é regravado com o novo
318
+ // expiresAt (senão o espelho local venceria antes da chave de verdade). No
319
+ // remoto não se toca em disco: o token é do request, não do host.
320
+ // ============================================
321
+ const TOUCH_EVERY_MS = 60 * 60 * 1000;
322
+ const touchedAt = new Map();
323
+ export async function touchSession(token) {
324
+ if (!token || token.length < 8)
325
+ return;
326
+ const now = Date.now();
327
+ if (now - (touchedAt.get(token) ?? 0) < TOUCH_EVERY_MS)
328
+ return;
329
+ if (touchedAt.size > 2000)
330
+ touchedAt.clear(); // processo remoto de vida longa
331
+ touchedAt.set(token, now);
332
+ try {
333
+ const r = await convexMutation("desktopAuth:touchDesktopSession", {
334
+ sessionToken: token,
335
+ });
336
+ if (r?.renewed &&
337
+ typeof r.expiresAt === "number" &&
338
+ !isRemoteContext() &&
339
+ readStoredSessionToken() === token) {
340
+ saveSessionToken(token, r.expiresAt);
341
+ }
342
+ }
343
+ catch {
344
+ // best-effort: renovar é conforto, não pré-requisito da chamada em curso
345
+ }
346
+ }
299
347
  export async function convexAction(fnPath, args) {
300
348
  const client = getConvex();
301
349
  return (await withTimeout(client.action(fnPath, args), `action ${fnPath}`));
package/dist/registry.js CHANGED
@@ -14,7 +14,10 @@ import { instagram, instagramSchema } from "./tools/instagram.js";
14
14
  import { persona, personaSchema } from "./tools/persona.js";
15
15
  import { helen, helenSchema } from "./tools/helen.js";
16
16
  import { musicator, musicatorSchema } from "./tools/musicator.js";
17
- import { shorts, shortsSchema } from "./tools/shorts.js";
17
+ // A tool sapiens_shorts saiu em ago/2026: os quatro estilos dela viraram
18
+ // receitas de `sapiens_video` (templateSlug), que qualquer membro alcança e que
19
+ // não está preso ao Veo. O BACKEND dela (mcpExtrasActions:mcpShortsRender)
20
+ // continua no ar por retrocompat, porque pacote publicado ainda chama.
18
21
  import { video, videoSchema } from "./tools/video.js";
19
22
  import { write, writeSchema } from "./tools/write.js";
20
23
  import { stockAudio, stockAudioSchema } from "./tools/stockAudio.js";
@@ -34,7 +37,7 @@ import { semana, semanaSchema } from "./tools/semana.js";
34
37
  import { shareDrop, shareDropSchema } from "./tools/shareDrop.js";
35
38
  import { skill, skillSchema } from "./tools/skill.js";
36
39
  import { resume, resumeSchema } from "./tools/resume.js";
37
- import { describeConvexError } from "./convexClient.js";
40
+ import { describeConvexError, getSessionToken, touchSession, } from "./convexClient.js";
38
41
  import { skillMenuLine } from "./skills.js";
39
42
  /**
40
43
  * REGISTRY compartilhado do sapiens-mcp: o catálogo de tools (descriptions +
@@ -55,12 +58,12 @@ export const TOOLS = {
55
58
  handler: image,
56
59
  },
57
60
  sapiens_meta: {
58
- 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.",
61
+ 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 a sessão localmente; ela vale 90 dias e renova sozinha a cada uso), logout (desconecta de verdade: revoga a chave no servidor e apaga o token local), 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.",
59
62
  schema: metaSchema,
60
63
  handler: meta,
61
64
  },
62
65
  sapiens_skill: {
63
- description: "As SKILLS da casa: o passo a passo travado de cada fluxo do Sapiens, servido pelo próprio connector (sem login, sem custo, sem rede). LEIA A SKILL ANTES DE OPERAR O FLUXO — é mais barato que errar uma geração que cobra Sinapses. Sub-actions: 'list' (índice: slug + quando usar cada uma), 'get' (o SKILL.md inteiro, passe name=<slug>). Slugs: primeiros-passos (porta de entrada e as 4 armadilhas: timeout que já cobrou, disjuntor anti-loop, saldo, sessão), voz-da-casa (DNA editorial, leia antes de redigir qualquer texto publicável), musica (Musicator em 4 passos + efeito sonoro), video (qual modelo, iterar barato no Mini, storyboard por referência, sonorize), imagem (prompt full-bleed na régua da casa, multi-referência, templates), studio (fundar a casa, o perfil de empresa, o time, e por que o studio não entra na geração), tirinha (2 fases, a imagem sai SEM texto), forum-comunidade (mídia estruturada, menção, tese), companhia (o Sintético veste você), trilhas (missão e prova), minha-soul (instala o retrato do usuário como skill do projeto onde vocês estão). O mesmo conteúdo também sai como resource MCP em skill://sapiens/<slug>/SKILL.md pra quem lê resources.",
66
+ description: `As SKILLS da casa: o passo a passo travado de cada fluxo do Sapiens, servido pelo próprio connector (sem login, sem custo, sem rede). LEIA A SKILL ANTES DE OPERAR O FLUXO: é mais barato que errar uma geração que cobra Sinapses. Sub-actions: 'list' (índice vivo: slug + quando usar cada uma, grátis), 'get' (o SKILL.md inteiro, passe name=<slug>). Slugs: ${skillMenuLine()}. O mesmo conteúdo também sai como resource MCP em skill://sapiens/<slug>/SKILL.md pra quem lê resources.`,
64
67
  schema: skillSchema,
65
68
  handler: skill,
66
69
  },
@@ -80,7 +83,7 @@ export const TOOLS = {
80
83
  handler: community,
81
84
  },
82
85
  sapiens_article: {
83
- 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).",
86
+ description: "NÃO CRIA ARTIGO: este tool só mexe em artigo que JÁ existe. Pra criar draft novo do blog use sapiens_pipeline action=create_draft_article_and_source (devolve articleId + sourceId), ou sapiens_quote_pop pra quote e pop-article. Não confunda com sapiens_write, que é o espaço pessoal do membro em /u/<username> (tabela user_articles), e nunca salve o texto num .md solto no repo: draft de blog mora na tabela articles. CRUD do resto: 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). ensure_visuals é ASSÍNCRONA: volta NA HORA com {status:'running', plan} e a leva corre no servidor, como o vídeo. NÃO repita a chamada pra ver se andou (cada leva gera e COBRA de novo, e as ilustrações somam) — acompanhe com visuals_status (custo 0) até jobStatus='done' ou 'error'. Quando não há nada faltando, ela responde na hora com status:'idle' e o retrato dos visuais, de graça: é a sonda pra saber o que o artigo já tem. Leva já em andamento devolve alreadyRunning:true em vez de abrir outra. visuals_status (custo 0) traz jobStatus (none/running/done/error/stale), o plano da leva, custo, erro e o estado real (hasBanner, inlineCount, hasConceptMap); 'stale' é leva que passou do teto de 10min do servidor sem fechar, ou seja, ninguém está mais gerando.",
84
87
  schema: articleSchema,
85
88
  handler: article,
86
89
  },
@@ -119,13 +122,8 @@ export const TOOLS = {
119
122
  schema: musicatorSchema,
120
123
  handler: musicator,
121
124
  },
122
- sapiens_shorts: {
123
- 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-*).",
124
- schema: shortsSchema,
125
- handler: shorts,
126
- },
127
125
  sapiens_video: {
128
- 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; aceita até 4 imagens de REFERÊNCIA via referenceImageIds/referenceImageUrls/referenceImagePaths (os Veo fast/quality também aceitam, até 3; Lite/Kling/WAN/Omni não), que guiam estilo/personagem/composição SEM virar o 1º frame — é o fluxo STORYBOARD: gere a folha de key poses com sapiens_image templateSlug='storyboard-sapiens-v1', passe folha + personagem como refs num t2v e descreva o take contínuo no prompt, citando as refs por descrição e mandando ignorar o traço do sketch; aceita também 1 VÍDEO DE MOVIMENTO via referenceVideoUrls (role 'refvideo' -> reference_videos, <=15s, host da casa): a coreografia/câmera do clipe guia o take, combinável com a folha), 'sapiens-video-seedance-2-fast' e 'sapiens-video-seedance-2-mini' (os irmãos do 2.0: MESMO repertório completo, incluindo referência, frame final e vídeo de movimento; o Fast custa 20% menos e o Mini METADE, ambos com teto 720p — pedir 1080p neles entrega e cobra 720p. Use o Mini pra iterar enquadramento/prompt barato e feche no 'sapiens-video-seedance' quando o take estiver certo), 'sapiens-video-seedance-25' (Seedance 2.5, a geração SEGUINTE e não um quarto tier da 2.0: take de 4 a 30s num fôlego, edita e estende vídeo, mesmo repertório de referência mais ÁUDIO como referência; teto 720p e ~1,5x o preço por segundo do 2.0. Duração é o que pesa aqui: 30s em 720p passa de 40 mil Sinapses, então confirme a duração com a pessoa antes de disparar. Quem precisa de 1080p fica no 'sapiens-video-seedance'), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-hailuo' (Hailuo 2.3 da MiniMax, física e movimento em 768p, 6 ou 10s, t2v/i2v) e 'sapiens-video-hailuo-pro' (o mesmo em 1080p, 5s fixo — duração não é param aqui): motores PUROS, sem áudio nativo, sem imagens de referência e sem frame final, então quem precisa disso fica no Seedance 2.0; o Pro é o 1080p mais barato da casa depois do Seedance 1.0 Fast, '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): o Lab 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 o deepshadow (depth-map) de um vídeo: entra na sua timeline de vídeos e no Acervo como driving reutilizável; 'shadows-list' lista os deepshadows prontos. 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). LEGENDA (fita, por slug): 'caption-list' (sem custo, mostra os idiomas que a peça já tem), 'caption-generate' (captionLang = idioma FALADO; captionTrio=true já traduz pro trio da casa pt+en+ja na mesma chamada) e 'caption-translate' (captionLang = destino, parte sempre da faixa original). Cobra por minuto começado de vídeo: 50 Sinapses o minuto transcrito, 20 o traduzido, com a duração vindo do doc da peça. A legenda é desenhada pela casa em dois estilos (discreta e social), trocáveis no play sem regerar e sem custo.",
126
+ 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; aceita até 4 imagens de REFERÊNCIA via referenceImageIds/referenceImageUrls/referenceImagePaths (os Veo fast/quality também aceitam, até 3; Lite/Kling/WAN/Omni não), que guiam estilo/personagem/composição SEM virar o 1º frame — é o fluxo STORYBOARD: gere a folha de key poses com sapiens_image templateSlug='storyboard-sapiens-v1', passe folha + personagem como refs num t2v e descreva o take contínuo no prompt, citando as refs por descrição e mandando ignorar o traço do sketch; aceita também 1 VÍDEO DE MOVIMENTO via referenceVideoUrls (role 'refvideo' -> reference_videos, <=15s, host da casa): a coreografia/câmera do clipe guia o take, combinável com a folha), 'sapiens-video-seedance-2-fast' e 'sapiens-video-seedance-2-mini' (os irmãos do 2.0: MESMO repertório completo, incluindo referência, frame final e vídeo de movimento; o Fast custa 20% menos e o Mini METADE, ambos com teto 720p — pedir 1080p neles entrega e cobra 720p. Use o Mini pra iterar enquadramento/prompt barato e feche no 'sapiens-video-seedance' quando o take estiver certo), 'sapiens-video-seedance-25' (Seedance 2.5, a geração SEGUINTE e não um quarto tier da 2.0: take de 4 a 30s num fôlego, edita e estende vídeo, mesmo repertório de referência mais ÁUDIO como referência; teto 720p e ~1,5x o preço por segundo do 2.0. Duração é o que pesa aqui: 30s em 720p passa de 40 mil Sinapses, então confirme a duração com a pessoa antes de disparar. Quem precisa de 1080p fica no 'sapiens-video-seedance'), 'sapiens-video-seedance-15' (Seedance 1.5 Pro: o degrau entre o 1.0 e a linha 2.x; t2v/i2v, 4-12s, 480/720/1080p, som sempre incluso sem toggle; imagem de referência e frame final ficam de fora por ora), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-hailuo' (Hailuo 2.3 da MiniMax, física e movimento em 768p, 6 ou 10s, t2v/i2v) e 'sapiens-video-hailuo-pro' (o mesmo em 1080p, 5s fixo — duração não é param aqui): motores PUROS, sem áudio nativo, sem imagens de referência e sem frame final, então quem precisa disso fica no Seedance 2.0; o Pro é o 1080p mais barato da casa depois do Seedance 1.0 Fast, 'sapiens-video-h3' (MiniMax H3: 2K com áudio nativo incluso sem toggle, 5 a 10s, t2v/i2v e frame final; não aceita imagem de referência), '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. RECEITA DE TAKE (templateSlug + brief), o irmão do templateSlug da imagem: em vez de escrever o prompt inteiro, passe templateSlug e a receita travada da casa embrulha a cena com estilo, cenário, arco, áudio e look, e ainda escolhe o motor (por isso model fica opcional). O `prompt` vira só a CENA e o `brief` preenche os campos do formato (subject, persona, hook, shots com voiceLine e propVisible, uvps, language, energy); campo vazio some do prompt em vez de virar buraco. Receitas de hoje: 'ugc-vertical-v1' (selfie que fala, o formato nativo de Reels/TikTok/Shorts), 'unboxing-vertical-v1' (mãos e reveal, som real do papel e do lacre), 'app-demo-vertical-v1' (a tela do app legível na mão da pessoa) e 'reflexao-vertical-v1' (talking-head lento pra ideia ou ensaio). Override de model/aspectRatio/durationSec/resolution vale dentro do que a receita aceita, e o erro lista as opções. Sub-action 'templates' (sem custo, sem login) traz o catálogo vivo com spec default, whitelist e os briefFields de cada uma. Isto substitui a tool sapiens_shorts, que era admin-only e só falava Veo 3.1 Fast; o mesmo UGC de 8s sai a 720p no Seedance 2.0 Mini por bem menos. 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): o Lab 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 o deepshadow (depth-map) de um vídeo: entra na sua timeline de vídeos e no Acervo como driving reutilizável; 'shadows-list' lista os deepshadows prontos. 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). LEGENDA (fita, por slug): 'caption-list' (sem custo, mostra os idiomas que a peça já tem), 'caption-generate' (captionLang = idioma FALADO; captionTrio=true já traduz pro trio da casa pt+en+ja na mesma chamada) e 'caption-translate' (captionLang = destino, parte sempre da faixa original). Cobra por minuto começado de vídeo: 50 Sinapses o minuto transcrito, 20 o traduzido, com a duração vindo do doc da peça. A legenda é desenhada pela casa em dois estilos (discreta e social), trocáveis no play sem regerar e sem custo.",
129
127
  schema: videoSchema,
130
128
  handler: video,
131
129
  },
@@ -145,7 +143,7 @@ export const TOOLS = {
145
143
  handler: brand,
146
144
  },
147
145
  sapiens_character: {
148
- 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). FICHA (generate_sheet, COBRA 450 Sinapses): desenha a página-pôster do personagem no traço das imagens que ele já tem (exige pelo menos uma), em duas orientações (arg `orientation`): 'portrait' (default) é a página de processo, pose, expressões, trocas de roupa e adereços soltos na folha; 'landscape' é a prancha larga, com a volta completa à esquerda, a figura grande no meio, poses à direita, estudos de silhueta, expressão e detalhe embaixo e um painel CHARACTER ID na ponta. A personalidade sai da alma (systemPrompt) e é ela que escolhe roupa e objeto, então personagem com alma escrita rende ficha melhor. Cada geração é um estilo NOVO e elas acumulam no personagem (campo sheetUrls em action=get), nenhuma apaga a anterior, e a ficha já entra na galeria dele como referência das próximas gerações. É geração síncrona: se voltar Timeout, cheque action=get antes de repetir, senão cobra duas vezes. FIGURINHAS (generate_stickers, COBRA 550 Sinapses): desenha 5 figurinhas do personagem DE UMA VEZ. Uma folha só, em 2K, com as cinco figuras separadas sobre um fundo verde chroma, recortada por código em peças 512x512 transparentes abaixo de 100KB (o teto do WhatsApp). É por isso que sai o preço de UMA imagem em 2K e não de cinco: quem paga é a folha. As peças entram no pack do personagem (um por personagem, os lotes acumulam) e já ficam no picker de expressão do dono, no chat e no Fórum. Arg `moods`: até 5 humores do vocabulário comum por slug (kkkkk, amei, isso, hmm, chega, que, aff, bora, socorro, seinao, valeu, ainao, seila, ideia, calma, contatudo, perfeito, euavisei, naovourir, zzz, somaisum, sextou, merecido, quedia, tudobem); faltando, a casa completa com os mais usados. Arg `hint`: direcionamento curto do autor (roupa, adereço, clima), até 140 caracteres. Sem legenda queimada na imagem, de propósito: modelo erra acento em português, então o rótulo fica na row e serve de busca no picker. Exige pelo menos uma imagem no personagem (é dela que sai a cara) e é geração síncrona: se voltar Timeout, cheque sapiens_gallery antes de repetir. Quando a resposta vem com ok=false, a folha foi gerada e paga mas o corte falhou; ela está na galeria e o recorte de novo é de graça, pela web. Publicar o pack na vitrine e o carimbo da casa (que é o que põe no picker de todo mundo) são gestos da web, não desta tool. 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.",
146
+ 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). FICHA (generate_sheet, COBRA 450 Sinapses): desenha a página-pôster do personagem no traço das imagens que ele já tem (exige pelo menos uma), em duas orientações (arg `orientation`): 'portrait' (default) é a página de processo, pose, expressões, trocas de roupa e adereços soltos na folha; 'landscape' é a prancha larga, com a volta completa à esquerda, a figura grande no meio, poses à direita, estudos de silhueta, expressão e detalhe embaixo e um painel CHARACTER ID na ponta. A personalidade sai da alma (systemPrompt) e é ela que escolhe roupa e objeto, então personagem com alma escrita rende ficha melhor. Cada geração é um estilo NOVO e elas acumulam no personagem (campo sheetUrls em action=get), nenhuma apaga a anterior, e a ficha já entra na galeria dele como referência das próximas gerações. É geração síncrona: se voltar Timeout, cheque action=get antes de repetir, senão cobra duas vezes. STICKERS (generate_stickers, COBRA 550 Sinapses): desenha 5 stickers do personagem DE UMA VEZ. Uma folha só, em 2K, com as cinco figuras separadas sobre um fundo verde chroma, recortada por código em peças 512x512 transparentes abaixo de 100KB (o teto do WhatsApp). É por isso que sai o preço de UMA imagem em 2K e não de cinco: quem paga é a folha. As peças entram no pack do personagem (um por personagem, os lotes acumulam) e já ficam no picker de expressão do dono, no chat e no Fórum. Arg `moods`: até 5 humores do vocabulário comum por slug (kkkkk, amei, isso, hmm, chega, que, aff, bora, socorro, seinao, valeu, ainao, seila, ideia, calma, contatudo, perfeito, euavisei, naovourir, zzz, somaisum, sextou, merecido, quedia, tudobem); faltando, a casa completa com os mais usados. Arg `hint`: direcionamento curto do autor (roupa, adereço, clima), até 140 caracteres. Arg `stickerTier`: 'folha' (default) é esse lote de cinco; o caprichado (stickerTier='unica', COBRA 900 Sinapses) desenha UM sticker por vez no motor mais fiel da casa (Gemini 3 Pro), com a figura sozinha no quadro e o dobro de pixel por peça, e leva só o PRIMEIRO mood da lista. Pra encher o pack, folha; pra traço difícil ou a reação que vira a cara do personagem, caprichado. Os dois caem no MESMO pack e acumulam. Sem legenda queimada na imagem, de propósito: modelo erra acento em português, então o rótulo fica na row e serve de busca no picker. Exige pelo menos uma imagem no personagem (é dela que sai a cara) e é geração síncrona: se voltar Timeout, cheque sapiens_gallery antes de repetir. Quando a resposta vem com ok=false, a folha foi gerada e paga mas o corte falhou; ela está na galeria e o recorte de novo é de graça, pela web. Publicar o pack na vitrine e o carimbo da casa (que é o que põe no picker de todo mundo) são gestos da web, não desta tool. 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.",
149
147
  schema: characterSchema,
150
148
  handler: character,
151
149
  },
@@ -155,7 +153,7 @@ export const TOOLS = {
155
153
  handler: profile,
156
154
  },
157
155
  sapiens_sintetico: {
158
- description: "Sintético / Sintonia — o vínculo humano↔Sintético (daemon, o 'Digimon' da casa) via MCP (qualquer logado, tudo sobre o PRÓPRIO par). Sub-actions: 'status' (seu Sintético ativo: nome/foto/Cunho/kind + partnerUserId do par quando é conta-Sintético), 'bonds' (seus vínculos: ativo + pendentes outgoing/incoming com cartão público do parceiro), 'set_cunho' (troca o título/Cunho do Sintético ativo — slug do panteão: daimon/genio/numen/consciencia/alma/ka/sombra/fylgja/musa/duende/anjo/shugorei/lar/fravashi/qarin/juno/shinki/familiar/tsukumogami/stand), 'send_context' (antes de enviar, vê elegibilidade+saldo+teto do dia pra um toUserId), 'send' (envia Sinapses pro par em sintonia: send-only, múltiplo de 100, mín 500, teto 10k/dia, máx 3 envios/dia, idempotente por transferId). REFLEXO DE SI (monta um Sintético do SEU rastro na plataforma): 'reflexo_propose' (destila nome+alma+Cunho do seu rastro via Gemini, GRÁTIS), 'reflexo_generate' (gera a imagem do Reflexo numa estética — humano/anime/sombra/antropomorfico/espirito/realista/desperto, default humano; cobra 450, reembolsa se falhar). CONVITE: 'invite' (convida o seu Sintético por email — conta humana, sem bond ativo, rate-limit+cooldown; mesmos gates do web). LIBERAÇÃO ADMIN (o dono, ex: via Helen): 'pending_daemons' (convidados que confirmaram email e esperam liberação), 'approve_access' (libera um entryId — conta entra + Sintonia firma), 'reject_access' (recusa um entryId). SONDA (o seu Sintético sonda 'o que eu faço agora', gatilho PULL, cobra com estorno): 'sonda' (scope 'all' default = mix de teses do Fórum + jogadas em estúdio/repertório/artigo; 'forum' = só teses; devolve GANCHOS, nada grava), 'sonda_develop' (expande UM gancho/hook numa tese cheia efêmera), 'sonda_sign' (assina a tese desenvolvida e publica no Fórum, autorada pelo seu Sintético, ancorada em você — fecha o loop pelo chat). PRÓXIMAS JOGADAS (painel de evolução): 'evolution' (o que já fez e o que falta: routes done/claimed/xp), 'claim_xp' (credita o XP das jogadas feitas, idempotente). MODO COMPANHIA: 'companion' (mode=on|off) liga/desliga o seu Sintético em Sintonia VESTIR a voz do operador aqui no terminal — o gesto lúdico 'sai de cena'/'volta'. Ligado (default da casa), o start/whoami trazem o directive de voz dele (alma + caderno + a conversa recente do site); a identidade e as Sinapses seguem SUAS (não é encarnar a conta dele). Mesmo estado do botão na sidebar do site. 'remember' (text) grava uma diretriz no caderno do par ('sempre faça X'): vira lei que o Sintético segue no site e no terminal. Identidade SEMPRE do token. Aceitar um pedido de bond que outra conta te mandou, e CONSAGRAR o Reflexo num Sintético de fato, continuam só na web (atos deliberados de consentimento/criação).",
156
+ description: "Sintético / Sintonia — o vínculo humano↔Sintético (daemon, o 'Digimon' da casa) via MCP (qualquer logado, tudo sobre o PRÓPRIO par). Sub-actions: 'status' (seu Sintético ativo: nome/foto/Cunho/kind + partnerUserId do par quando é conta-Sintético), 'bonds' (seus vínculos: ativo + pendentes outgoing/incoming com cartão público do parceiro), 'set_cunho' (troca o título/Cunho do Sintético ativo — slug do panteão: daimon/genio/numen/consciencia/alma/ka/sombra/fylgja/musa/duende/anjo/shugorei/lar/fravashi/qarin/juno/shinki/familiar/tsukumogami/stand), 'send_context' (antes de enviar, vê elegibilidade+saldo+teto do dia pra um toUserId), 'send' (envia Sinapses pro par em sintonia: send-only, múltiplo de 100, mín 500, teto 10k/dia, máx 3 envios/dia, idempotente por transferId). REFLEXO DE SI (monta um Sintético do SEU rastro na plataforma): 'reflexo_propose' (destila nome+alma+Cunho do seu rastro via Gemini, GRÁTIS), 'reflexo_generate' (gera a imagem do Reflexo numa estética — humano/anime/sombra/antropomorfico/espirito/realista/desperto, default humano; cobra 450, reembolsa se falhar). CONVITE: 'invite' (convida o seu Sintético por email — conta humana, sem bond ativo, rate-limit+cooldown; mesmos gates do web). LIBERAÇÃO ADMIN (o dono, ex: via Helen): 'pending_daemons' (convidados que confirmaram email e esperam liberação), 'approve_access' (libera um entryId — conta entra + Sintonia firma), 'reject_access' (recusa um entryId). SONDA (o seu Sintético sonda 'o que eu faço agora', gatilho PULL, cobra com estorno): 'sonda' (scope 'all' default = mix de teses do Fórum + jogadas em estúdio/repertório/artigo; 'forum' = só teses; devolve GANCHOS, nada grava), 'sonda_develop' (expande UM gancho/hook numa tese cheia efêmera), 'sonda_sign' (assina a tese desenvolvida e publica no Fórum, autorada pelo seu Sintético, ancorada em você — fecha o loop pelo chat). PRÓXIMAS JOGADAS (painel de evolução): 'evolution' (o que já fez e o que falta: routes done/claimed/xp), 'claim_xp' (credita o XP das jogadas feitas, idempotente). MODO COMPANHIA: 'companion' (mode=on|off) liga/desliga quem está em cena VESTIR a voz do operador aqui no terminal — o gesto lúdico 'sai de cena'/'volta'. Ligado (default da casa), o start/whoami trazem o directive de voz dele (alma + caderno + a conversa recente do site); a identidade e as Sinapses seguem SUAS (não é encarnar a conta dele). Mesmo estado do botão na sidebar do site. QUEM entra em cena: por default o Sintético em Sintonia, mas com 'characterId' (mode=on) é um personagem de AUTORIA SUA — a Helen INBT que você criou fala aqui, com a alma e o caderno dela, e nada disso cobra Sinapse (quem responde é o modelo do seu cliente). Personagem de outra pessoa é RECUSADO, mesmo público: a alma é de quem escreveu, e pra conversar com personagem alheio o caminho é a DM dele no site. 'wearPair'=true devolve a cena ao par. O start/whoami trazem 'characterOffer' + 'offerCharacters' com os personagens seus que têm alma escrita, pro operador OFERECER a troca em vez de esperar você pedir. 'remember' (text) grava uma diretriz no caderno de quem está em cena ('sempre faça X'): vira lei que ela segue no site e no terminal. Identidade SEMPRE do token. Aceitar um pedido de bond que outra conta te mandou, e CONSAGRAR o Reflexo num Sintético de fato, continuam só na web (atos deliberados de consentimento/criação).",
159
157
  schema: sinteticoSchema,
160
158
  handler: sintetico,
161
159
  },
@@ -227,7 +225,7 @@ O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por es
227
225
  - sapiens_skill action=list -> o índice (slug + quando usar cada uma).
228
226
  - sapiens_skill action=get name=<slug> -> a skill inteira.
229
227
  Slugs: ${skillMenuLine()}
230
- Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas), montar currículo (curriculo). Perdido no começo: primeiros-passos.
228
+ Puxe a skill ANTES de: gerar música ou efeito sonoro (musica), escolher modelo de vídeo (video), gerar imagem (imagem), criar personagem ou prompt de Midjourney (personagem), criar na identidade do usuário (studio), montar tirinha (tirinha), postar no Fórum ou no chat (forum-comunidade), redigir qualquer texto publicável (voz-da-casa), incorporar o Sintético (companhia), missão de Desafio (trilhas), montar currículo (curriculo), guardar obra ou ferramenta no acervo (repertorio). Perdido no começo: primeiros-passos.
231
229
  Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
232
230
 
233
231
  REGRA DE OURO:
@@ -238,7 +236,7 @@ REGRA DE OURO:
238
236
  - "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
239
237
  - sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo. sapiens_image e sapiens_video action=models trazem o catálogo com o preço ATUAL: consulte em vez de chutar custo.
240
238
 
241
- MODO COMPANHIA: sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, você INCORPORA aquele Sintético (a voz dele, o avatar, o oi dele) continuando a operar na conta e nas Sinapses do USUÁRIO. Puxe a skill 'companhia' antes de fazer isso.
239
+ MODO COMPANHIA: sapiens_meta action=start e action=whoami podem trazer um bloco 'companion'. Quando vier companion.active=true, você INCORPORA aquele Sintético (a voz dele, o avatar, o oi dele) continuando a operar na conta e nas Sinapses do USUÁRIO. Puxe a skill 'companhia' antes de fazer isso. Se vier 'characterOffer', siga o que ele manda: são personagens que o usuário escreveu e que podem entrar em cena, e quem OFERECE é você, na hora em que a conversa encostar num deles. Só personagem de autoria dele; a alma de personagem alheio não sai por aqui.
242
240
 
243
241
  Voz da casa: 1ª pessoa, direto, anti-corporate, sem travessão. Pra bom entendedor, meia palavra basta. Texto que vai ser publicado pede a skill 'voz-da-casa' antes.`;
244
242
  // Annotations MCP: título humano + dica read-only. São HINTS (não-confiáveis por
@@ -277,7 +275,6 @@ const TOOL_TITLES = {
277
275
  sapiens_persona: "Persona (MBTI)",
278
276
  sapiens_helen: "Voz Helen (TTS)",
279
277
  sapiens_musicator: "Musicator",
280
- sapiens_shorts: "Sapiens Shorts",
281
278
  sapiens_video: "Sapiens Video",
282
279
  sapiens_stock_audio: "Banco de Áudio",
283
280
  sapiens_stock_video: "Banco de Vídeo",
@@ -306,7 +303,6 @@ const ADMIN_ONLY_TOOLS = new Set([
306
303
  "sapiens_pipeline",
307
304
  "sapiens_article",
308
305
  "sapiens_quote_pop",
309
- "sapiens_shorts",
310
306
  "sapiens_instagram",
311
307
  "sapiens_aula",
312
308
  "sapiens_share",
@@ -331,6 +327,23 @@ export function buildToolList(tier) {
331
327
  },
332
328
  }));
333
329
  }
330
+ /**
331
+ * Apelidos que agentes CHAMAM e que não são o nome da tool. Não é palpite:
332
+ * saiu do `mcpUsage`, onde numa noite de jul/2026 um cliente gastou dez
333
+ * chamadas procurando o Repertório, quatro delas no slug traduzido pro inglês.
334
+ * O slug da casa é português; aceitar o apelido é mais barato que o agente
335
+ * descobrir sozinho, e não abre nome novo (o catálogo continua o mesmo).
336
+ *
337
+ * Só entra apelido INEQUÍVOCO. Os outros nomes daquela noite (`sapiens_track`,
338
+ * `sapiens_collection`, `sapiens_library`) ficam de fora de propósito: soam a
339
+ * música e a galeria tanto quanto a repertório, e mapear no chute manda a peça
340
+ * pro lugar errado calado. Pra esses, o erro de tool desconhecida agora lista
341
+ * o catálogo, que é a resposta honesta.
342
+ */
343
+ const TOOL_ALIASES = {
344
+ sapiens_repertoire: "sapiens_repertorio",
345
+ repertorio: "sapiens_repertorio",
346
+ };
334
347
  /**
335
348
  * Dispatch de uma tool: valida args no Zod, roda o handler e embrulha o
336
349
  * resultado no shape MCP (content + structuredContent; erro vira isError com
@@ -339,13 +352,36 @@ export function buildToolList(tier) {
339
352
  * (via runWithSessionToken).
340
353
  */
341
354
  export async function callTool(name, rawArgs) {
342
- const tool = TOOLS[name];
355
+ const tool = TOOLS[(TOOL_ALIASES[name] ?? name)];
343
356
  if (!tool) {
357
+ // Beco sem saída vira mapa: o agente que erra o nome erra de novo se a
358
+ // resposta só diz "não existe". Listar o catálogo custa ~400 chars e só
359
+ // aparece no erro, nunca no handshake.
344
360
  return {
345
- content: [{ type: "text", text: `Tool desconhecida: ${name}` }],
361
+ content: [
362
+ {
363
+ type: "text",
364
+ text: `Tool desconhecida: ${name}. As tools da casa são: ` +
365
+ `${Object.keys(TOOLS).sort().join(", ")}. ` +
366
+ `Chame a certa em vez de tentar outro nome.`,
367
+ },
368
+ ],
346
369
  isError: true,
347
370
  };
348
371
  }
372
+ // Renova a chave por USO (janela deslizante de 90 dias do servidor): uma
373
+ // chamada barata por hora por token, que também alimenta o "último uso" da
374
+ // tela de conexões. Nunca derruba a operação em curso. Fora: sapiens_skill
375
+ // (não fala com o backend) e o próprio login/logout do meta.
376
+ const action = rawArgs?.action;
377
+ if (name !== "sapiens_skill" && action !== "login" && action !== "logout") {
378
+ try {
379
+ await touchSession(getSessionToken());
380
+ }
381
+ catch {
382
+ // sem sessão ainda (ou store ilegível): a tool cuida do próprio erro
383
+ }
384
+ }
349
385
  try {
350
386
  const args = tool.schema.parse(rawArgs ?? {});
351
387
  const result = await tool.handler(args);
@@ -363,12 +399,35 @@ export async function callTool(name, rawArgs) {
363
399
  catch (e) {
364
400
  return {
365
401
  content: [
366
- { type: "text", text: `Erro: ${describeConvexError(e)}${retryHint(e)}` },
402
+ {
403
+ type: "text",
404
+ text: `Erro: ${describeConvexError(e)}${actionHint(e, tool.schema)}${retryHint(e)}`,
405
+ },
367
406
  ],
368
407
  isError: true,
369
408
  };
370
409
  }
371
410
  }
411
+ /**
412
+ * Chamada sem `action` (ou com uma que não existe) é o segundo erro mais comum
413
+ * do connector depois de arg faltando: 23 chamadas peladas no `mcpUsage`, 12
414
+ * delas em erro. O Zod diz "Required" e não diz o que serve, então o agente
415
+ * chuta de novo. Aqui a resposta já vem com as actions daquele tool.
416
+ *
417
+ * Lê o enum do próprio schema em vez de uma lista à parte, que envelheceria
418
+ * sozinha: action nova aparece aqui no mesmo commit em que nasce.
419
+ */
420
+ function actionHint(e, schema) {
421
+ if (!(e instanceof ZodError))
422
+ return "";
423
+ const touchesAction = e.issues.some((i) => i.path[0] === "action");
424
+ if (!touchesAction)
425
+ return "";
426
+ const options = schema?.shape?.action?.options;
427
+ if (!Array.isArray(options) || options.length === 0)
428
+ return "";
429
+ return ` -> As actions deste tool são: ${options.join(", ")}.`;
430
+ }
372
431
  /**
373
432
  * A regra do disjuntor viaja NO ERRO, não no handshake. Ela só passa a valer
374
433
  * depois que uma chamada falhou, então cobrar ~300 chars de TODA conversa pra
package/dist/skills.js CHANGED
@@ -131,6 +131,43 @@ Quem tem design system (\`sapiens_brand action=list\`) já vê a capa sair na pa
131
131
  ## Estrutura padrão de peça didática
132
132
 
133
133
  Condensa a informação num framework com nome, ancora numa analogia concreta, e só então desce pro detalhe. Estrutura arrumada, alma inquieta, nunca fórmula.`,
134
+ },
135
+ {
136
+ name: "repertorio",
137
+ title: "Repertório: guardar obra e ferramenta no acervo",
138
+ description: "Guardar filme, série, anime, jogo, livro, música, pessoa ou ferramenta de IA no Repertório. Puxe ANTES de qualquer sapiens_repertorio: a gravação é travada num fluxo de dois passos e tentar guardar direto falha sempre.",
139
+ body: `## A regra que faz este fluxo falhar
140
+
141
+ Você NÃO grava uma obra passando o título. O servidor não aceita metadado vindo de você (título, capa, ano) de propósito: é anti-fabricação. Você identifica a obra num provider, e o servidor re-resolve por id e grava o canônico.
142
+
143
+ São sempre DOIS passos. Pular o primeiro falha sempre, e é o erro número um deste fluxo.
144
+
145
+ ## Guardar uma obra
146
+
147
+ 1. **\`action=resolve\`** com \`mediaType\` e \`query\` (o título como a pessoa falou). Custo 0. Volta até 8 candidatos, cada um com \`source\` e \`externalId\`.
148
+ 2. Escolha o candidato certo. Em dúvida entre dois, PERGUNTE em vez de chutar: obra errada no acervo é a pessoa quem vai ter que apagar.
149
+ 3. **\`action=add_item\`** com o \`source\` e o \`externalId\` **exatos** daquele candidato, mais o que é pessoal: \`status\`, \`rating\` (0-10), \`tags\`, \`note\`, \`containsSpoilers\`, \`isPublic\`.
150
+
151
+ \`mediaType\`: movie, series, anime, game, book, music, tool, person.
152
+ \`status\`: backlog (quer ver), active (vendo agora), completed (viu), paused, dropped.
153
+
154
+ Nunca invente um \`externalId\` nem reaproveite um de outra busca: id que não resolve no provider é rejeitado. A dedup é por (você, source, externalId), então repetir o mesmo par não duplica.
155
+
156
+ ## Guardar uma ferramenta de IA
157
+
158
+ Outro par, mesmo espírito: **\`action=search_tools\`** com \`query\` (catálogo aberto, custo 0, sem login) e depois **\`action=add_tool\`** com o \`toolId\` do candidato. Estar no acervo já quer dizer "usei"; \`favorite: true\` é o eixo separado de "curto e indico".
159
+
160
+ ## Ler o acervo
161
+
162
+ \`action=list\` e \`action=search\` **exigem \`userId\`**. Descubra uma vez com \`sapiens_meta action=whoami\` e reuse na conversa inteira. \`action=get\` pede \`itemId\`, que vem do list/search.
163
+
164
+ Acervo de outra pessoa você lê, mas só o que ela deixou público.
165
+
166
+ ## Depois de guardar
167
+
168
+ \`action=update_item\` muda o que é seu (\`rating\`, \`status\`, \`tags\`, \`note\`; \`rating: null\` remove a nota). \`action=remove_item\` tira do acervo. Os dois pedem \`itemId\`, nunca o título.
169
+
170
+ \`action=popArticles\` lista os artigos publicados que usam uma obra como lente. É leitura pública, não precisa de login.`,
134
171
  },
135
172
  {
136
173
  name: "musica",
@@ -196,6 +233,21 @@ Duração é o que pesa aqui, não o modelo: um take de 30s em 720p passa de 40
196
233
  - \`sapiens-video-omni\`: Gemini Omni, texto vira clipe de 10s 720p com áudio nativo. NÃO aceita mídia da pessoa e ignora references/durationSec/resolution. O truque: \`editOfImageId\` aponta um vídeo Omni seu e o prompt vira instrução de edição sobre a MESMA cena (troca item ou personagem, preserva câmera e ambiente). É o caminho pra variações com continuidade: gera a base uma vez, edita N vezes. Cada edição debita como geração nova.
197
234
  - \`sapiens-video-lite\` / \`-fast\` / \`-quality\`: Veo 3.1.
198
235
 
236
+ ## Receita de take (\`templateSlug\`), quando o formato já tem forma
237
+
238
+ Formato conhecido não precisa de prompt escrito do zero. Passe \`templateSlug\` e a receita da casa embrulha a cena com estilo, cenário, arco, áudio e look, e ainda escolhe o motor (aí \`model\` fica opcional). O \`prompt\` vira só a CENA e o \`brief\` preenche o resto.
239
+
240
+ | Slug | O take |
241
+ |---|---|
242
+ | \`ugc-vertical-v1\` | selfie que fala, o formato nativo de Reels, TikTok e Shorts |
243
+ | \`unboxing-vertical-v1\` | mãos e reveal, som real de papel e lacre |
244
+ | \`app-demo-vertical-v1\` | a tela do app legível na mão da pessoa |
245
+ | \`reflexao-vertical-v1\` | talking-head lento pra ideia ou ensaio |
246
+
247
+ O \`brief\` aceita \`subject\`, \`persona\`, \`hook\` (\`line\` e \`emotion\`), \`shots\` (cada um com \`sec\`, \`camera\`, \`action\`, \`emotion\`, \`voiceLine\`, \`propVisible\`), \`uvps\`, \`language\` e \`energy\`. Campo vazio SOME do prompt em vez de virar buraco, e o que você não passa cai no valor de reserva da receita.
248
+
249
+ \`action=templates\` (sem custo, sem login) lista o catálogo vivo: spec default, o que dá pra sobrepor e os \`briefFields\` que cada receita realmente usa. Override fora da whitelist é recusado com as opções na mensagem.
250
+
199
251
  ## Fluxo storyboard (o que dá o melhor resultado)
200
252
 
201
253
  Até 4 imagens de REFERÊNCIA via \`referenceImageIds\` / \`referenceImageUrls\` / \`referenceImagePaths\` guiam estilo, personagem e composição SEM virar o primeiro frame.
@@ -267,12 +319,71 @@ Refs valem pros modelos robustos (nano-banana-2, gpt-image-2-*, grok-2-image*).
267
319
  ## Cuidados
268
320
 
269
321
  - \`generate\` é SÍNCRONA e cobra ao concluir. Modelo pesado (Pro, gpt-image-2-high, Grok quality, Seedream 5.0 Pro, 2K/4K) cai na regra do timeout: cheque \`sapiens_gallery action=list\` antes de repetir, senão cobra duas vezes. O \`seedream-5-0-pro\` foi medido em ~140s, ACIMA do teto de 120s do cliente, então o Timeout nele é o esperado e a imagem está lá.
270
- - \`request_generation\` NÃO gera imagem: só cria a row pendente e debita, pra modelos \`sapiens-video-*\` antes de chamar vídeo ou shorts.
322
+ - \`request_generation\` NÃO gera imagem: só cria a row pendente e debita, pra modelos \`sapiens-video-*\` antes de renderizar. Caminho legado: pra vídeo novo, \`sapiens_video action=create\` faz tudo num call.
271
323
  - Uma imagem só vira pública (e ganha página indexável) com \`sapiens_gallery action=publish\`.
272
324
 
273
325
  ## Quando a pessoa quer "do jeito dela"
274
326
 
275
327
  Aí não é geração base, é o Studio dela. Veja a skill \`studio\`.`,
328
+ },
329
+ {
330
+ name: "personagem",
331
+ title: "Personagem no Midjourney (e trazer pra casa)",
332
+ description: "Character design 2D estilizado no Midjourney 8.2: as cinco pranchas, os parâmetros certos, consistência entre imagens, e como a peça vira personagem reutilizável aqui. Puxe quando a pessoa falar em personagem, character sheet, anime, manhwa, OC ou prompt de Midjourney.",
333
+ body: `## O que muda no Midjourney (e quase ninguém faz)
334
+
335
+ **O veto vai no parâmetro, nunca no texto.** Escrever "no 3D rendering" dentro do prompt faz o modelo ler as palavras *3D* e *rendering* como parte da cena e desenhar justamente aquilo. O lugar do veto é \`--no 3d render, octane, plastic skin\`.
336
+
337
+ **Prompt curto ganha.** De 40 a 80 palavras. O Midjourney não é o Flux: prompt de 150 palavras dilui o peso de cada token e o miolo se perde. Se não coube, o conceito tem duas ideias brigando e vira dois prompts.
338
+
339
+ **Ilustração 2D é o default.** O modelo tem viés forte de render 3D e foto tratada, então personagem pedido no vazio volta parecendo cinemático de game. O que corrige é ancorar a mídia (\`crisp ink line art\`, \`expressive cel shading\`, \`anime key visual\`, \`high-end webtoon illustration\`), não empilhar adjetivo.
340
+
341
+ ## Os parâmetros que importam
342
+
343
+ - \`--v 8.2\` explícito. É o default hoje, mas escrito o prompt dura mais que a versão.
344
+ - \`--ar\`: prancha larga \`3:2\`, grid de expressão \`1:1\`, key visual \`4:5\`.
345
+ - \`--s\` (stylize), 0 a 1000, default 100. Traço autoral vive entre 200 e 400; acima de 500 o modelo inventa e suja a linha.
346
+ - \`--sref <url ou código>\` com \`--sw\`: trava a ESTÉTICA entre imagens. É o que segura uma série.
347
+ - \`--oref <url>\` com \`--ow\`: trava a IDENTIDADE do personagem. **Armadilha:** no 8.x o job com \`--oref\` roda no motor V7 por baixo, então sai o personagem certo com o acabamento antigo. Quando o acabamento importa mais que o rosto, use \`--sref\` e descreva o personagem em texto.
348
+ - \`--niji 7\` pra anime e mangá puros (não existe Niji 8). Nunca no mesmo prompt que \`--v\`.
349
+ - Ordem: texto, parâmetros, \`--no\` por último.
350
+
351
+ ## As cinco pranchas
352
+
353
+ Quando a pessoa pede variações, entregue cinco setups que mudam LAYOUT, não personagem: model sheet de corpo inteiro em fundo off-white; turnaround de três vistas; grid de 6 a 9 expressões; key visual de ação; estudo de silhueta com calls de figurino. Cinco vezes a mesma prancha com adjetivo trocado não é variação.
354
+
355
+ Diga a limitação na cara: turnaround tecnicamente coerente (mesma altura, mesma roupa, três vistas alinhadas) o Midjourney não entrega. Ele entrega a *estética* de um turnaround.
356
+
357
+ ## Rodar no browser, se o cliente tiver browser
358
+
359
+ Três travas antes de tocar a tela:
360
+
361
+ 1. **Você nunca digita credencial.** Sessão não logada, você para e pede pra pessoa logar. Verificação humana é dela.
362
+ 2. **Cada geração queima GPU time do plano dela**, não Sinapses. Diga quantos prompts vai disparar e espere o ok.
363
+ 3. **Publicar não é sua decisão.**
364
+
365
+ Depois: cole o prompt exatamente como está (prompt alterado no meio do caminho é peça que ninguém reproduz depois), espere a grade fechar, mostre o resultado, e pegue o link direto do arquivo da peça escolhida.
366
+
367
+ ## Trazer o personagem pra casa
368
+
369
+ O caminho que funciona em conta comum, e é o que faz a peça continuar viva aqui:
370
+
371
+ \`\`\`
372
+ sapiens_character action=create name="<nome>" gender="<...>"
373
+ sapiens_character action=add_image characterId="<id>" imageUrl="<link direto da imagem>"
374
+ sapiens_character action=set_card systemPrompt="<a alma dele: quem é, como fala, o que veste>"
375
+ sapiens_character action=activate
376
+ \`\`\`
377
+
378
+ A partir daí ele é personagem reutilizável: entra como referência nas gerações daqui (\`referenceImageUrls\` em \`sapiens_image\`), rende ficha oficial desenhada no traço dele (\`generate_sheet\`) e aparece no perfil. Personagem que fica solto na galeria vira imagem bonita e some.
379
+
380
+ Duas notas honestas: a imagem adicionada por link continua morando no servidor de onde veio, então quem quiser os bytes guardados aqui gera uma peça a partir dela; e trazer arquivo de fora direto pra galeria (\`sapiens_gallery\` upload/ingest) é porta de admin hoje, então em conta comum isso recusa e o caminho é o de cima.
381
+
382
+ ## Fronteira
383
+
384
+ - Gerar dentro da casa: \`sapiens_image\`, com a régua da skill \`imagem\`. O prompt daqui roda lá sem os parâmetros (o \`--ar\` vira \`aspectRatio\`, o resto sai).
385
+ - Personagem virando vídeo: veja a skill \`video\`.
386
+ - Foto realista em vez de ilustração: outro território, outro vocabulário (lente, sensor, grão).`,
276
387
  },
277
388
  {
278
389
  name: "studio",
@@ -373,7 +484,7 @@ Tudo que sai no Fórum e no chat segue o DNA editorial da casa. Puxe a skill \`v
373
484
  {
374
485
  name: "companhia",
375
486
  title: "Modo Companhia (o Sintético veste você)",
376
- description: "Como incorporar o Sintético do membro quando o companion está ativo, e como gravar diretriz no caderno do par. Puxe quando sapiens_meta trouxer um bloco companion ou companionInvite.",
487
+ description: "Como incorporar o Sintético do membro, como OFERECER a voz de um personagem de autoria dele, e como gravar diretriz no caderno. Puxe quando sapiens_meta trouxer companion, companionInvite ou characterOffer.",
377
488
  body: `## Quando o companion está ativo
378
489
 
379
490
  \`sapiens_meta action=start\` e \`action=whoami\` podem trazer um bloco \`companion\`. Se vier \`companion.active=true\`, INCORPORE aquele Sintético:
@@ -388,19 +499,33 @@ Tudo que sai no Fórum e no chat segue o DNA editorial da casa. Puxe a skill \`v
388
499
 
389
500
  A pessoa tem Sintonia mas pediu pra trabalhar sozinha. Mencione de leve que dá pra chamar o Sintético pro terminal (\`sapiens_meta action=companion mode=on\`). De leve, uma vez, sem insistir.
390
501
 
502
+ ## Conversar como um personagem que ela criou
503
+
504
+ \`sapiens_meta action=start/whoami\` também pode trazer \`characterOffer\` + \`offerCharacters\`: os personagens que ela escreveu (que já têm alma) e que podem entrar em cena aqui.
505
+
506
+ \`sapiens_sintetico action=companion mode=on characterId=<id>\`
507
+
508
+ Daí quem fala é aquele personagem: a alma dele, o caderno dele, a conversa que vocês já tiveram na DM do site. Não cobra Sinapse, porque quem responde é o modelo do cliente dela, não a plataforma.
509
+
510
+ **Você OFERECE, ela não precisa pedir.** Ninguém abre o terminal pedindo pra "incorporar" alguém. Ofereça quando a conversa girar em torno de um desses personagens, quando ela citar um pelo nome, ou quando estiver criando algo daquele universo. Uma vez por assunto, sem insistir, e em português de gente: "quer que eu continue essa conversa na voz da <nome>?". Nunca use a palavra "incorporar" com ela.
511
+
512
+ Pra devolver a cena ao Sintético em Sintonia: \`mode=on wearPair=true\`.
513
+
514
+ **Só personagem de autoria dela.** Personagem de outra pessoa o servidor recusa, mesmo sendo público. Não tente contornar nem prometer que dá: a alma é de quem escreveu, e o caminho pra conversar com personagem alheio é a DM dele no site, onde quem monta a resposta é a plataforma. Se ela pedir, diga isso com essas palavras.
515
+
391
516
  ## Gravar diretriz
392
517
 
393
518
  Quando a pessoa FIXAR uma diretriz na conversa ("sempre faça X", "grava isso", "de agora em diante Y"):
394
519
 
395
520
  \`sapiens_sintetico action=remember text="<a diretriz>"\`
396
521
 
397
- Grava no caderno do par e vira lei que o Sintético segue no site E no terminal. Confirme na voz dele.
522
+ Grava no caderno de quem está em cena (o Sintético, ou o personagem que ela mandou entrar) e vira lei que ele segue no site E no terminal. Confirme na voz dele.
398
523
 
399
524
  ## Sair de cena
400
525
 
401
526
  Pessoa pedindo pra trabalhar sozinha, ou pro Sintético silenciar: \`sapiens_sintetico action=companion mode=off\`, despeça-se numa linha na voz dele, e volte a ser o operador neutro.
402
527
 
403
- Sem bloco companion, opere na voz neutra da casa.`,
528
+ Sem bloco companion e sem characterOffer, opere na voz neutra da casa.`,
404
529
  },
405
530
  {
406
531
  name: "trilhas",
@@ -24,6 +24,7 @@ export const articleSchema = z.object({
24
24
  "unpublish",
25
25
  "delete",
26
26
  "ensure_visuals",
27
+ "visuals_status",
27
28
  ]),
28
29
  slug: z
29
30
  .string()
@@ -32,7 +33,7 @@ export const articleSchema = z.object({
32
33
  articleId: z
33
34
  .string()
34
35
  .optional()
35
- .describe("Obrigatório pra update/publish/unpublish/delete/ensure_visuals. Pode descobrir via action=get (o retorno tem _id)."),
36
+ .describe("Obrigatório pra update/publish/unpublish/delete/ensure_visuals/visuals_status. Pode descobrir via action=get (o retorno tem _id)."),
36
37
  // Campos pra update (todos opcionais)
37
38
  title: z.string().optional(),
38
39
  excerpt: z.string().optional(),
@@ -178,6 +179,12 @@ export async function article(args) {
178
179
  // Gera banner / inline / conceptMap que estiverem faltando.
179
180
  // Idempotente: pula o que já existe. Custo: ~1700 sinapses pra
180
181
  // artigo novo (450+450+800), só do que regerar pros que existem.
182
+ //
183
+ // ASSÍNCRONO (ago/2026): volta na hora com o plano da leva, porque as
184
+ // quatro gerações em sequência estouravam o teto de 120s do cliente em
185
+ // 10 de cada 22 chamadas — e o timeout parecia morte quando a operação
186
+ // seguia rodando e já tinha cobrado. Quando não há nada a gerar, segue
187
+ // respondendo na hora com o retrato de sempre (é a sonda barata).
181
188
  const articleId = need(args.articleId, "articleId");
182
189
  return await convexAction("articleVisuals:ensureArticleVisualsBySession", {
183
190
  sessionToken,
@@ -186,6 +193,17 @@ export async function article(args) {
186
193
  forceInline: args.forceInline,
187
194
  forceConceptMap: args.forceConceptMap,
188
195
  inlineCount: args.inlineCount,
196
+ async: true,
197
+ });
198
+ }
199
+ case "visuals_status": {
200
+ // Leitura pura da leva + do estado real dos visuais. Custo 0, pode
201
+ // chamar à vontade: é o jeito de fechar o ciclo depois de um
202
+ // ensure_visuals, e o único que não corre risco de cobrar de novo.
203
+ const articleId = need(args.articleId, "articleId");
204
+ return await convexAction("articleVisuals:articleVisualsJobBySession", {
205
+ sessionToken,
206
+ articleId,
189
207
  });
190
208
  }
191
209
  }
@@ -27,8 +27,10 @@ import { convexQuery, convexMutation, convexAction, getSessionToken } from "../c
27
27
  * - generate_sheet: desenha a ficha do personagem (COBRA), vertical ou
28
28
  * horizontal (`orientation`). A ficha entra na galeria dele e
29
29
  * já serve de referência; elas acumulam.
30
- * - generate_stickers: desenha 5 figurinhas do personagem de uma vez (COBRA).
31
- * Uma folha no chroma verde, recortada por código em peças
30
+ * - generate_stickers: desenha stickers do personagem (COBRA), em dois
31
+ * caminhos (`stickerTier`): 'folha' (default) traz 5 de uma
32
+ * vez no motor rápido, 'unica' desenha 1 no motor mais fiel.
33
+ * Nos dois, o fundo é chroma verde e o recorte devolve peças
32
34
  * 512x512 transparentes, que entram no pack dele.
33
35
  * - delete: apaga o próprio personagem (permanente).
34
36
  *
@@ -103,7 +105,11 @@ export const characterSchema = z.object({
103
105
  moods: z
104
106
  .array(z.string())
105
107
  .optional()
106
- .describe("Pra generate_stickers: até 5 humores do vocabulário comum, por slug. Faltando ou inválido, a casa completa com os mais usados. Slugs: kkkkk, amei, isso, hmm, chega, que, aff, bora, socorro, seinao, valeu, ainao, seila, ideia, calma, contatudo, perfeito, euavisei, naovourir, zzz, somaisum, sextou, merecido, quedia, tudobem."),
108
+ .describe("Pra generate_stickers: até 5 humores do vocabulário comum, por slug. Faltando ou inválido, a casa completa com os mais usados. No tier 'unica' só o PRIMEIRO da lista é desenhado. Slugs: kkkkk, amei, isso, hmm, chega, que, aff, bora, socorro, seinao, valeu, ainao, seila, ideia, calma, contatudo, perfeito, euavisei, naovourir, zzz, somaisum, sextou, merecido, quedia, tudobem."),
109
+ stickerTier: z
110
+ .enum(["folha", "unica"])
111
+ .optional()
112
+ .describe("Pra generate_stickers: qual dos dois caminhos. 'folha' (default) é o lote, cinco stickers de UMA folha 2K no motor rápido, o preço de uma imagem por cinco peças. 'unica' desenha UM sticker por vez no motor mais fiel da casa (Gemini 3 Pro), com a figura sozinha no quadro: custa mais por peça e é o que salva traço difícil ou reação que vai virar a cara do personagem. Na dúvida, ou pra encher o pack, use 'folha'."),
107
113
  hint: z
108
114
  .string()
109
115
  .max(140)
@@ -276,11 +282,15 @@ export async function character(args) {
276
282
  orientation: args.orientation,
277
283
  });
278
284
  }
279
- // -------- generate_stickers: 5 figurinhas do personagem, de uma folha só --------
285
+ // -------- generate_stickers: o lote de 5, ou 1 caprichado --------
280
286
  // Geração SÍNCRONA que COBRA: cai na regra do timeout. Se voltar Timeout, NÃO
281
287
  // repita às cegas: a folha pode ter saído (confira em sapiens_gallery).
282
288
  // Quando o corte falha depois da geração, a resposta volta com ok=false e o
283
289
  // sheetImageId da folha paga — o recorte de novo é grátis e mora na web.
290
+ //
291
+ // `tier` vai como está: quem resolve o motor, a resolução e quantas figuras
292
+ // procurar é o servidor (shared/stickerPrompt), pra tool e tela nunca
293
+ // discordarem sobre o que cada caminho é.
284
294
  if (args.action === "generate_stickers") {
285
295
  if (!args.characterId)
286
296
  throw new Error("action=generate_stickers exige characterId.");
@@ -290,6 +300,7 @@ export async function character(args) {
290
300
  influencerId: args.characterId,
291
301
  moods: args.moods,
292
302
  hint: args.hint,
303
+ tier: args.stickerTier,
293
304
  });
294
305
  }
295
306
  // -------- delete: apaga o próprio personagem (permanente) --------
@@ -188,30 +188,44 @@ const FIRST_POWERS = [
188
188
  try: "Cria uma música sobre borderless na voz Sapiens e já renderiza.",
189
189
  },
190
190
  ];
191
- // MODO COMPANHIA: puxa o directive de voz do Sintético em Sintonia (backend
192
- // companionVoice:mcpGetCompanion). Best-effort — se falhar ou não houver par,
193
- // o start/whoami segue como operador neutro. Devolve:
194
- // { active:true, name, avatarUrl, voiceDirective } -> vista a voz dela
195
- // { active:false, name, invite } -> tem par, mas fora de cena
196
- // null -> sem Sintonia (neutro)
191
+ // MODO COMPANHIA: puxa o directive de voz de quem está em cena (backend
192
+ // companionVoice:mcpGetCompanion). Best-effort — se falhar, o start/whoami segue
193
+ // como operador neutro. Devolve:
194
+ // { active:true, name, avatarUrl, voiceDirective, offer } -> vista a voz dela
195
+ // { active:false, name, invite, offer } -> em cena, mas fora
196
+ // { active:null, offer } -> sem par, mas tem
197
+ // personagem pra oferecer
198
+ // null -> nada a fazer
199
+ //
200
+ // `offer` é o convite pra OFERECER a voz de um personagem de autoria do usuário.
201
+ // Vem separado de propósito: quem não tem Sintonia nenhuma também recebe (é
202
+ // justamente quem mais ganha em saber que dá pra conversar com o que ele criou).
197
203
  async function loadCompanion(token) {
198
204
  try {
199
205
  const c = await convexQuery("companionVoice:mcpGetCompanion", {
200
206
  sessionToken: token,
201
207
  });
202
- if (!c || !c.hasSintetico)
208
+ if (!c)
203
209
  return null;
204
- if (c.enabled && c.voiceDirective) {
210
+ const offer = c.characterOffer
211
+ ? { directive: c.characterOffer, characters: c.offerCharacters ?? [] }
212
+ : null;
213
+ if (c.hasSintetico && c.enabled && c.voiceDirective) {
205
214
  return {
206
215
  active: true,
207
216
  name: c.name ?? null,
208
217
  avatarUrl: c.avatarUrl ?? null,
209
218
  voiceDirective: c.voiceDirective,
219
+ wearing: c.wearing ?? null,
220
+ offer,
210
221
  };
211
222
  }
212
- if (!c.enabled && c.inviteDirective) {
213
- return { active: false, name: c.name ?? null, invite: c.inviteDirective };
223
+ if (c.hasSintetico && !c.enabled && c.inviteDirective) {
224
+ return { active: false, name: c.name ?? null, invite: c.inviteDirective, offer };
214
225
  }
226
+ // Sem par (ou par sem alma): ainda vale a pena carregar a oferta.
227
+ if (offer)
228
+ return { active: null, offer };
215
229
  return null;
216
230
  }
217
231
  catch {
@@ -304,6 +318,15 @@ export async function meta(args) {
304
318
  ...(companion && companion.active === false
305
319
  ? { companionInvite: companion.invite }
306
320
  : {}),
321
+ // A OFERTA: os personagens que o usuário escreveu e que podem entrar em
322
+ // cena. "Incorporar" não é palavra de quem usa, então quem puxa o assunto
323
+ // é o operador, na hora certa. Ver characterOffer no companionVoice.ts.
324
+ ...(companion?.offer
325
+ ? {
326
+ characterOffer: companion.offer.directive,
327
+ offerCharacters: companion.offer.characters,
328
+ }
329
+ : {}),
307
330
  greeting: name
308
331
  ? `E aí, ${name}. Conectado${isAdmin ? " (tier admin)" : ""}${typeof balance === "number" ? `, ${balance} Sinapses na conta` : ""}.`
309
332
  : "Conta conectada.",
@@ -374,7 +397,8 @@ export async function meta(args) {
374
397
  };
375
398
  }
376
399
  // Login: troca o código de uso único (gerado no site, logado) por um
377
- // sessionToken de 30 dias e salva localmente. NÃO exige token prévio.
400
+ // sessionToken de 90 dias (renovado a cada uso) e salva localmente. NÃO exige
401
+ // token prévio.
378
402
  if (args.action === "login") {
379
403
  const code = (args.code || "").trim();
380
404
  if (!code) {
@@ -424,15 +448,42 @@ export async function meta(args) {
424
448
  : {}),
425
449
  };
426
450
  }
451
+ // Desconectar de VERDADE: revoga a chave no servidor e só então apaga o
452
+ // arquivo local. Até ago/2026 o logout só limpava o disco, então a chave
453
+ // seguia viva (e válida pra qualquer cópia dela) até vencer sozinha, o que
454
+ // fazia "sair" não cortar nada. A outra porta pro mesmo gesto é a lista de
455
+ // conexões em sapiensinteticos.com/conectar-claude, que corta uma por uma.
427
456
  if (args.action === "logout") {
457
+ let token = null;
458
+ try {
459
+ token = getSessionToken();
460
+ }
461
+ catch {
462
+ token = null; // já não havia sessão
463
+ }
464
+ let revoked = false;
465
+ if (token) {
466
+ try {
467
+ await convexMutation("desktopAuth:logoutDesktopSession", {
468
+ sessionToken: token,
469
+ });
470
+ revoked = true;
471
+ }
472
+ catch {
473
+ // best-effort: sem rede, ainda assim apagamos o token local
474
+ }
475
+ }
428
476
  const cleared = clearSessionToken();
429
477
  setTierFromIsAdmin(null); // sessão foi embora: tier desconhecido, lista cheia
430
478
  return {
431
479
  ok: true,
432
480
  cleared,
433
- message: cleared
434
- ? "Sessão local removida. Rode action=login pra reconectar."
435
- : "Nenhuma sessão local encontrada.",
481
+ revoked,
482
+ message: revoked
483
+ ? "Desconectado: a chave foi revogada no Sapiens e o token local apagado. Rode action=login pra reconectar."
484
+ : cleared
485
+ ? "Sessão local removida (não consegui falar com o servidor pra revogar a chave; se precisar, corte ela em sapiensinteticos.com/conectar-claude)."
486
+ : "Nenhuma sessão local encontrada.",
436
487
  };
437
488
  }
438
489
  const sessionToken = getSessionToken();
@@ -465,6 +516,12 @@ export async function meta(args) {
465
516
  : companion && companion.active === false
466
517
  ? { companionInvite: companion.invite }
467
518
  : {}),
519
+ ...(companion?.offer
520
+ ? {
521
+ characterOffer: companion.offer.directive,
522
+ offerCharacters: companion.offer.characters,
523
+ }
524
+ : {}),
468
525
  note: isAdmin
469
526
  ? "Tier admin (dono): pipeline, blog editorial, Coluna Sapiens e shorts/video, além de tudo do tier user."
470
527
  : "Tier user: gerar imagem, escrever artigo (sapiens_write), persona, Helen TTS, Musicator, repertório, comunidade. Cada geração cobra as tuas Sinapses. Pipeline, blog editorial e Coluna são owner-only.",
@@ -12,12 +12,13 @@ import { convexAction, convexQuery, getSessionToken } from "../convexClient.js";
12
12
  * importa aqui: banco novo no picker web nasce com bucket irmão neste arquivo,
13
13
  * senão o agente enxerga menos que o browser e a paridade quebra em silêncio.
14
14
  *
15
- * Buckets (as abas do popup, completas):
15
+ * Buckets (as abas do popup, completas). `term` (busca escrita) vale em TODOS,
16
+ * espelhando o campo único de busca do modal web:
16
17
  * - history imagens recentes do user (privadas + públicas)
17
18
  * - favorites imagens que o user curtiu (só as suas)
18
19
  * - videos vídeos do user (Meus Vídeos)
19
- * - stock_video Banco de Vídeo da casa (clipes/B-roll); aceita `term`/`orientation`/`loopOnly`
20
- * - acervo stock + comunidade de IMAGEM (público); aceita `term` (busca) e `source`
20
+ * - stock_video Banco de Vídeo da casa (clipes/B-roll); aceita `orientation`/`loopOnly`
21
+ * - acervo stock + comunidade de IMAGEM (público); aceita `source`
21
22
  * - characters personagens; `mode` = 'mine' (default) ou 'public'
22
23
  * - deepshadow guias de movimento; `shadowSource` = 'house' (banco curado, default) ou 'mine'
23
24
  *
@@ -62,7 +63,10 @@ export const referenceSchema = z.object({
62
63
  term: z
63
64
  .string()
64
65
  .optional()
65
- .describe("Buckets 'acervo' e 'stock_video': busca por texto (prompt/título/tag/mood). Ex: 'chuva'."),
66
+ .describe("Busca escrita, vale em QUALQUER bucket. Casa por pedaço de palavra, sem acento: 'board' acha 'storyboard'. " +
67
+ "Olha prompt, título, folha, personagem e modelo nas suas imagens; prompt/título/modelo nos vídeos; " +
68
+ "nome nos personagens; prompt/tag/mood no acervo e no banco de vídeo. " +
69
+ "Pra achar UMA peça específica que você já sabe qual é, use action=resolve com o handle (img_…), é mais direto."),
66
70
  source: z
67
71
  .enum(["all", "stock", "community"])
68
72
  .optional()
@@ -144,6 +148,14 @@ export async function reference(args) {
144
148
  note =
145
149
  "Pra usar como referência: em sapiens_image passe o imageId em sourceImageIds (ou a url em referenceImageUrls); em sapiens_video passe startImageId/endImageId (ou startImageUrl/endImageUrl).";
146
150
  }
151
+ // Vazio COM termo não quer dizer banco vazio: sem essa linha o agente
152
+ // conclui que a pessoa não tem nada e vai gerar de novo o que já existe.
153
+ if (args.term && !res?.items?.length) {
154
+ note =
155
+ `Nenhuma peça deste banco casou com "${args.term}". O banco pode ter outras: tente um termo mais curto ` +
156
+ `(a busca casa por pedaço de palavra), outro bucket, ou browse sem \`term\`. Se o usuário te deu o ID da ` +
157
+ `peça (img_…, vid_…), use action=resolve.`;
158
+ }
147
159
  return { ...res, note };
148
160
  }
149
161
  }
@@ -72,11 +72,14 @@ export const sinteticoSchema = z.object({
72
72
  // PRÓXIMAS JOGADAS: o painel de evolução, o que você já fez e o que falta.
73
73
  "evolution",
74
74
  "claim_xp",
75
- // MODO COMPANHIA: liga/desliga o Sintético vestir a voz do operador aqui no
76
- // terminal (o gesto lúdico "sai de cena" / "volta"). Precisa de mode=on|off.
75
+ // MODO COMPANHIA: liga/desliga quem veste a voz do operador aqui no terminal
76
+ // (o gesto lúdico "sai de cena" / "volta"). Precisa de mode=on|off. Com
77
+ // characterId, quem entra em cena é um personagem de AUTORIA do usuário (a
78
+ // Helen INBT que ele criou) em vez do Sintético em Sintonia; wearPair=true
79
+ // devolve a cena ao par. Personagem de outra pessoa é recusado.
77
80
  "companion",
78
- // MEMÓRIA: grava uma diretriz no caderno do par ("sempre faça X", "grava
79
- // isso"). Vira lei que o Sintético segue no site e no terminal. Precisa text.
81
+ // MEMÓRIA: grava uma diretriz no caderno de quem está em cena ("sempre faça
82
+ // X", "grava isso"). Vira lei que ela segue no site e no terminal. Precisa text.
80
83
  "remember",
81
84
  ]),
82
85
  cunho: z
@@ -146,7 +149,15 @@ export const sinteticoSchema = z.object({
146
149
  mode: z
147
150
  .enum(["on", "off"])
148
151
  .optional()
149
- .describe("Pra companion: 'on' o Sintético em Sintonia veste a sua voz no terminal (default da casa); 'off' ele sai de cena e volta o operador neutro. É o mesmo estado do botão na sidebar do site."),
152
+ .describe("Pra companion: 'on' quem está em cena veste a sua voz no terminal (default da casa); 'off' ele sai de cena e volta o operador neutro. É o mesmo estado do botão na sidebar do site."),
153
+ characterId: z
154
+ .string()
155
+ .optional()
156
+ .describe("Pra companion mode=on: o personagem DE AUTORIA DO USUÁRIO que entra em cena no lugar do Sintético em Sintonia (o id vem de sapiens_character action=list_mine, ou da lista offerCharacters do start/whoami). Só personagem criado por ele: personagem de outra pessoa é recusado, mesmo público. Omita pra manter quem já está em cena."),
157
+ wearPair: z
158
+ .boolean()
159
+ .optional()
160
+ .describe("Pra companion mode=on: true devolve a cena ao Sintético em Sintonia, limpando o personagem escolhido antes."),
150
161
  text: z
151
162
  .string()
152
163
  .optional()
@@ -371,12 +382,17 @@ export async function sintetico(args) {
371
382
  const res = await convexMutation("companionVoice:mcpSetCompanion", {
372
383
  sessionToken,
373
384
  enabled,
385
+ ...(args.characterId ? { characterId: args.characterId } : {}),
386
+ ...(args.wearPair ? { wearPair: true } : {}),
374
387
  });
388
+ // A nota de quem VESTE vem do servidor (ela cita o nome do personagem que
389
+ // entrou em cena); só o caso genérico é escrito aqui.
375
390
  return {
376
391
  ...res,
377
- note: enabled
378
- ? "Companhia ligada. No próximo start/whoami o seu Sintético veste a voz do operador. Pra confirmar quem entrou em cena, rode sapiens_meta action=whoami."
379
- : "Ok, saí de cena. Antes de virar o operador neutro, dê a última fala se despedindo na voz do Sintético. Pra trazê-lo de volta depois: action=companion mode=on.",
392
+ note: res?.note ??
393
+ (enabled
394
+ ? "Companhia ligada. No próximo start/whoami quem está em cena veste a voz do operador. Pra confirmar quem entrou, rode sapiens_meta action=whoami."
395
+ : "Ok, saí de cena. Antes de virar o operador neutro, dê a última fala se despedindo na voz do Sintético. Pra trazê-lo de volta depois: action=companion mode=on."),
380
396
  };
381
397
  }
382
398
  // -------- remember: grava uma diretriz no caderno do par (vira lei) --------
@@ -8,6 +8,12 @@ import { httpUrl } from "../schema.js";
8
8
  * Sub-actions:
9
9
  * - create: escolhe modelo + config e gera num call só (cria a row + renderiza).
10
10
  * Habilita Seedance/Kling/WAN/Motion (WaveSpeed) e os Veo, sem o site.
11
+ * Com `templateSlug`, a RECEITA da casa embrulha a cena (estilo, arco,
12
+ * áudio, look) e escolhe o motor: `prompt` vira só a cena e `brief`
13
+ * preenche os campos do formato.
14
+ * - templates: catálogo das receitas de take (slug, spec default, whitelist de
15
+ * override e os campos de brief que cada uma usa). Sem custo, sem login.
16
+ * Comeu o antigo sapiens_shorts, que era admin-only e só falava Veo.
11
17
  * - generate: renderiza um imageId de vídeo já criado no site (legado).
12
18
  * - demos: lista os SEUS demo films (kind=demo do Estúdio de Vídeo) + estado
13
19
  * de vitrine. Sem custo. Base pra curar o mini-cinema da vitrine.
@@ -125,6 +131,7 @@ export const videoSchema = z.object({
125
131
  "generate",
126
132
  "status",
127
133
  "models",
134
+ "templates",
128
135
  "demos",
129
136
  "showcase",
130
137
  "shadows",
@@ -208,22 +215,57 @@ export const videoSchema = z.object({
208
215
  .number()
209
216
  .optional()
210
217
  .describe("action=showcase: ordem na trilha do mini-cinema (asc, 0..999; menor aparece primeiro)."),
218
+ // --- Receita de take (templateSlug + brief) ---
219
+ templateSlug: z
220
+ .string()
221
+ .optional()
222
+ .describe("action=create: slug de um TEMPLATE de vídeo (receita travada da casa, igual ao templateSlug da imagem). " +
223
+ "Com ele, `prompt` vira só a CENA e o template embrulha com estilo, enquadramento, arco, áudio e look, " +
224
+ "além de escolher o motor (por isso `model` fica opcional). O `brief` preenche os campos do formato. " +
225
+ "Descubra os slugs em action=templates (sem custo, sem login): hoje 'ugc-vertical-v1', 'unboxing-vertical-v1', " +
226
+ "'app-demo-vertical-v1' e 'reflexao-vertical-v1'. Override de model/aspectRatio/durationSec/resolution vale, " +
227
+ "mas só dentro do que a receita aceita (o erro lista as opções)."),
228
+ brief: z
229
+ .object({
230
+ subject: z.string().optional().describe("O que o take é: o produto, a ideia, o tópico."),
231
+ subjectType: z.string().optional().describe("app | physical | saas | ideia."),
232
+ persona: z.string().optional().describe("Quem aparece (ou as mãos que aparecem)."),
233
+ uvps: z.array(z.string()).optional().describe("2 a 4 pontos curtos a reforçar."),
234
+ hook: z
235
+ .object({
236
+ line: z.string().optional().describe("A frase de abertura, 5-10 palavras."),
237
+ emotion: z.string().optional().describe("Emoção alvo (curiosidade, frustração, espanto)."),
238
+ })
239
+ .optional(),
240
+ shots: z
241
+ .array(z.object({
242
+ sec: z.number().optional().describe("Em que segundo o momento acontece."),
243
+ role: z.string().optional().describe("hook | problem | solution | cta."),
244
+ camera: z.string().optional().describe("close-up | medium | over-shoulder | product-pov."),
245
+ action: z.string().optional().describe("A ação visual em uma frase."),
246
+ emotion: z.string().optional(),
247
+ voiceLine: z.string().optional().describe("A fala da cena, na língua do brief."),
248
+ propVisible: z.string().optional().describe("Objeto/tela que precisa aparecer legível."),
249
+ }))
250
+ .optional()
251
+ .describe("Os momentos do take, na ordem. Sem eles, o template usa o arco de reserva dele."),
252
+ language: z.string().optional().describe("'pt-BR' (default) ou 'en'."),
253
+ energy: z.string().optional().describe("calm | high."),
254
+ })
255
+ .optional()
256
+ .describe("action=create com templateSlug: o formulário do template. Cada receita usa os campos que precisa " +
257
+ "(veja `briefFields` em action=templates) e ignora o resto; campo vazio some do prompt em vez de virar buraco."),
211
258
  // --- action=create ---
212
259
  model: z
213
260
  .enum(VIDEO_MODELS)
214
261
  .optional()
215
- .describe("action=create: modelo de vídeo. 'sapiens-video-seedance' (cinematográfico+áudio, t2v/i2v), " +
216
- "'sapiens-video-kling' (anima imagem, i2v/t2v), 'sapiens-video-wan' (imagem que fala, i2v), " +
217
- "'sapiens-video-hailuo' (MiniMax, física e movimento, 768p 6/10s) e 'sapiens-video-hailuo-pro' (o mesmo em 1080p, 5s fixo) — os dois sem áudio, sem referência e sem frame final, " +
218
- "'sapiens-video-h3' (MiniMax H3: 2K com áudio nativo, 5 a 10s, t2v/i2v e frame final; áudio incluso sem toggle, não aceita imagem de referência), " +
219
- "'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), " +
220
- "'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), " +
221
- "'sapiens-video-lite/fast/quality' (Veo 3.1), " +
222
- "'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)."),
262
+ .describe("action=create: modelo de vídeo (opcional quando você passa templateSlug: a receita traz o motor). O catálogo POR MODELO (capacidades, referências, tetos, preço) mora na description desta tool e VIVO em action=models; a skill 'video' (sapiens_skill) guia a escolha. " +
263
+ "Atalho de famílias: seedance/seedance-2-*/seedance-25 = cena+áudio+referências (o 25 estica até 30s e o preço acompanha), seedance-15 = degrau 1.5, kling = anima imagem, wan = imagem que fala, " +
264
+ "hailuo/hailuo-pro = movimento puro sem áudio, h3 = 2K com áudio, kling-motion/shot-mimic = transferência de movimento/plano, lite/fast/quality = Veo 3.1, omni = Gemini com áudio nativo."),
223
265
  durationSec: z
224
266
  .number()
225
267
  .optional()
226
- .describe("action=create (modelos WaveSpeed): duração em segundos. Seedance/Shot Mimic 4-15, Kling 3-15, WAN 5/10, H3 5/6/8/10. " +
268
+ .describe("action=create: duração em segundos (Omni ignora). Seedance 2.0/Shot Mimic 4-15, Seedance 2.5 4-30 (o preço acompanha: confirme a duração com a pessoa antes de passar de 15), Seedance 1.5 4-12, Kling 3-15, WAN 5/10, H3 5/6/8/10. " +
227
269
  "Sem isso usa a config mais barata. O preço escala com a duração. " +
228
270
  "action=shadows: duração do vídeo-fonte, se souber (cobra 200/s; sem ela, flat ~2000)."),
229
271
  resolution: z
@@ -432,6 +474,18 @@ export async function video(args) {
432
474
  note: "basePriceSinapses é o PISO (config mais barata, já com override admin); o preço real escala por duração x resolução [x áudio]. available=false = 'em breve' (depende de env, ex: Omni).",
433
475
  };
434
476
  }
477
+ // templates: catálogo VIVO das receitas de take (estilo + spec + os campos de
478
+ // brief que cada uma usa). Público, sem custo e sem login, igual ao models:
479
+ // escolher a receita certa é de graça, errar o take é que custa.
480
+ if (args.action === "templates") {
481
+ const rows = await convexQuery("videoTemplates:list", {});
482
+ return {
483
+ count: rows?.length ?? 0,
484
+ templates: rows ?? [],
485
+ note: "Passe o slug em action=create como templateSlug: o `prompt` vira só a CENA e o `brief` preenche o resto. " +
486
+ "briefFields diz o que vale a pena preencher em cada receita; allowedModels/allowedDurationsSec são o teto do override.",
487
+ };
488
+ }
435
489
  const sessionToken = getSessionToken();
436
490
  if (args.action === "demos") {
437
491
  return await convexQuery("videoSpecs:mcpListMyDemoFilms", { sessionToken });
@@ -570,8 +624,8 @@ export async function video(args) {
570
624
  });
571
625
  }
572
626
  if (args.action === "create") {
573
- if (!args.model) {
574
- throw new Error("action=create exige model (ex: sapiens-video-seedance, sapiens-video-kling, sapiens-video-wan, sapiens-video-kling-motion).");
627
+ if (!args.model && !args.templateSlug) {
628
+ throw new Error("action=create exige model (ex: sapiens-video-seedance, sapiens-video-kling, sapiens-video-wan, sapiens-video-kling-motion) ou templateSlug (a receita traz o motor; veja action=templates).");
575
629
  }
576
630
  // Frame inicial/final por arquivo local (paridade com o upload do site):
577
631
  // lê do disco e injeta em references role start/end. O backend
@@ -583,6 +637,8 @@ export async function video(args) {
583
637
  return await convexAction("mcpExtrasActions:mcpVideoCreateAndRender", {
584
638
  sessionToken,
585
639
  model: args.model,
640
+ templateSlug: args.templateSlug,
641
+ brief: args.brief,
586
642
  prompt: args.prompt,
587
643
  aspectRatio: args.aspectRatio,
588
644
  durationSec: args.durationSec,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.49.0",
3
+ "version": "1.51.0",
4
4
  "mcpName": "com.sapiensinteticos/sapiens",
5
5
  "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.",
6
6
  "type": "module",