sapiens-mcp 1.69.2 → 1.71.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/dist/clientIdentity.js +41 -0
- package/dist/index.js +4 -1
- package/dist/registry.js +22 -21
- package/dist/remote.js +5 -1
- package/dist/skills.js +70 -0
- package/dist/tools/character.js +3 -0
- package/dist/tools/gallery.js +181 -24
- package/dist/tools/image.js +50 -5
- package/dist/tools/meta.js +106 -2
- package/package.json +1 -1
- package/dist/tools/referencia.js +0 -114
- package/dist/tools/shorts.js +0 -81
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// QUEM É O CLIENT — o motor que a pessoa usa do outro lado do MCP.
|
|
3
|
+
//
|
|
4
|
+
// O backend quer saber por onde um personagem nasceu ou foi ativado ("MCP
|
|
5
|
+
// (Claude)" em vez de "MCP (agente)" no alerta do dono). Dois transportes,
|
|
6
|
+
// duas fontes:
|
|
7
|
+
// - stdio: o handshake `initialize` traz clientInfo {name, version} e o
|
|
8
|
+
// Server do SDK guarda em getClientVersion(). É a fonte boa: "claude-ai",
|
|
9
|
+
// "claude-code", "cursor", "gemini-cli"...
|
|
10
|
+
// - remoto (streamable HTTP, stateless na Vercel): cada request é um Server
|
|
11
|
+
// novo e o initialize não fica. Sobra o User-Agent do HTTP, que viaja com
|
|
12
|
+
// o prefixo "ua:" pro backend saber que é pista, não nome.
|
|
13
|
+
// O valor cru vai pro backend, que rotula (convex/shared/characterOrigin.ts).
|
|
14
|
+
// Nunca é identidade: a identidade é sempre o bearer/login.
|
|
15
|
+
// ============================================
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
const MAX = 120;
|
|
18
|
+
const requestClient = new AsyncLocalStorage();
|
|
19
|
+
/** Limpa o que veio de fora: uma linha, sem caractere de controle, teto de 120. */
|
|
20
|
+
export function sanitizeClientName(raw) {
|
|
21
|
+
if (typeof raw !== "string")
|
|
22
|
+
return undefined;
|
|
23
|
+
const clean = raw
|
|
24
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
if (!clean)
|
|
28
|
+
return undefined;
|
|
29
|
+
return clean.slice(0, MAX);
|
|
30
|
+
}
|
|
31
|
+
/** Embrulha a chamada com a pista do client (call do stdio ou request remota). */
|
|
32
|
+
export function runWithClientHint(hint, fn) {
|
|
33
|
+
const clean = sanitizeClientName(hint);
|
|
34
|
+
if (!clean)
|
|
35
|
+
return fn();
|
|
36
|
+
return requestClient.run(clean, fn);
|
|
37
|
+
}
|
|
38
|
+
/** A pista do client da chamada em curso, se houver. */
|
|
39
|
+
export function getClientName() {
|
|
40
|
+
return requestClient.getStore();
|
|
41
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getCachedTier, onTierVisibilityChange, probeTierInBackground, } from ".
|
|
|
13
13
|
import { getPrompt, listPrompts } from "./prompts.js";
|
|
14
14
|
import { listSkillResources, readSkillResource } from "./skills.js";
|
|
15
15
|
import { convexMutation, getSessionToken } from "./convexClient.js";
|
|
16
|
+
import { runWithClientHint } from "./clientIdentity.js";
|
|
16
17
|
// Telemetria stdio (fire-and-forget, NUNCA derruba a chamada). O stdio sempre
|
|
17
18
|
// foi caixa-preta (cada cliente roda a própria cópia via npx), então o
|
|
18
19
|
// transporte que mais roda — Helen/Cursor/Gemini CLI — era invisível na
|
|
@@ -77,7 +78,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
77
78
|
const name = req.params.name;
|
|
78
79
|
const args = (req.params.arguments ?? {});
|
|
79
80
|
const t0 = Date.now();
|
|
80
|
-
|
|
81
|
+
// O client do handshake (clientInfo.name) viaja com a chamada: é o que
|
|
82
|
+
// deixa o backend dizer "MCP (Claude)" em vez de "MCP (agente)".
|
|
83
|
+
const result = await runWithClientHint(server.getClientVersion()?.name, () => callTool(name, args));
|
|
81
84
|
logStdioUsage({
|
|
82
85
|
tool: name,
|
|
83
86
|
action: typeof args.action === "string" ? args.action : null,
|
package/dist/registry.js
CHANGED
|
@@ -76,7 +76,7 @@ export const TOOLS = {
|
|
|
76
76
|
handler: repertorio,
|
|
77
77
|
},
|
|
78
78
|
sapiens_gallery: {
|
|
79
|
-
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N peças, com prompt/model/url + isPublic + acervoUrl; kind=image por default, kind=video traz os takes com posterUrl, a capa, e kind=all mistura. Depois de ingerir ou gerar vídeo, é o list kind=video que dá a capa pra mostrar miniatura + player em vez de link solto), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen devolve publicPageUrl; publicado NÃO despublica, não existe unpublish), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), ingest (peça que a
|
|
79
|
+
description: "Browse, publicação e upload das imagens do user. Sub-actions: list (últimas N peças, com prompt/model/url + isPublic + acervoUrl; kind=image por default, kind=video traz os takes com posterUrl, a capa, e kind=all mistura. Depois de ingerir ou gerar vídeo, é o list kind=video que dá a capa pra mostrar miniatura + player em vez de link solto), get (1 imagem com metadados, opcionalmente base64), publish (torna a PRÓPRIA imagem pública: entra na galeria pública + feed Pinterest, e ganha página indexável /imagem/<id> se o modelo não for degen devolve publicPageUrl; publicado NÃO despublica, não existe unpublish), upload (traz pra galeria uma peça gerada FORA da casa, por sourceUrl https, filePath local ou base64; ADMIN por ora), ingest (A PONTE, qualquer conta logada: peça que a PESSOA dirigiu com a ficha do personagem daqui e um motor de fora renderizou, no crédito dela (Kie, fal, Magnific, Sogni, a própria placa): entra NATIVA na timeline e nasce PRIVADA (isPublic false), publicável DEPOIS por ela como qualquer geração da casa, nunca sozinha, com custo 0 em Sinapses e o custo real guardado em externalCost; carrega o prompt VERBATIM, as referências da casa (referenceImageIds), o personagem (characterId) e o motor (externalEngine 'provedor-motor'); a peça chega por filePath (local, só no MCP instalado), sourceUrl (o render, baixado pelo MCP na máquina da pessoa, nunca pelo servidor) ou base64; o carimbo +18 nasce na entrada pela CLASSE do provedor (peso aberto como Sogni, Replicate, HF, fal, WaveSpeed, Civitai e a própria placa entram carimbados, só a curadoria tira; serviço com moderação como Kie, Magnific, Freepik, Krea entra sem) ou por unfiltered=true dito pela pessoa (carimbo dela, ela tira depois); idempotente por sourceUrl ou pelo hash do arquivo, então re-ingerir corrige a ficha em vez de duplicar; tetos 12 MB imagem, 60 MB vídeo, 40 por dia. A chave do provedor NUNCA passa por aqui: quem gerou foi o harness da pessoa. Passo a passo na skill 'pontes'), refs (acopla peças da casa como REFERÊNCIA numa peça que já existe, de upload ou de ingest; ADMIN, e só na própria peça), cast (QUEM ESTÁ EM CENA numa peça que já existe, quando é mais de uma criatura: characterIds em ordem, o protagonista primeiro, e a peça passa a contar na ficha de todos eles em vez de sumir da segunda; qualquer membro, na própria peça). Use list/get pra reusar imagem como referência (passe o imageId em sapiens_image mode=edit ou mode=variation) ou pra mostrar pro user o que ele já tem; publish quando o user quer divulgar a imagem dele. IMPORTANTE sobre upload: a peça entra PRIVADA e continua privada: o servidor recusa publicar e recusa compartilhar na comunidade, porque ela é do usuário e a responsabilidade é dele. O caminho pra ela virar coisa pública é usar como referência numa geração daqui e publicar o resultado. O refs escreve só proveniência (de que ficha/folha a peça nasceu): não promove nada, e um upload continua sem publicar depois de ganhar referência. COMO A PEÇA FOI FEITA: list aceita skill=<slug> e devolve só o que saiu daquela skill (o mesmo slug que sapiens_image gravou em skills), com a busca varrendo as últimas 200 peças antes de cortar no limit; cada item volta com o campo skills, e null ali quer dizer que ninguém marcou, não que foi feita sem skill.",
|
|
80
80
|
schema: gallerySchema,
|
|
81
81
|
handler: gallery,
|
|
82
82
|
},
|
|
@@ -236,26 +236,27 @@ export const TOOLS = {
|
|
|
236
236
|
// É a "skill que anda junto com o pacote": escrevo uma vez, vale pra todos os clients,
|
|
237
237
|
// sem instalar nada. Cobre os tropeços reais (fluxo do musicator, model no vídeo, o
|
|
238
238
|
// disjuntor anti-loop). Mantém curto de propósito: viaja em todo handshake.
|
|
239
|
-
export const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
|
|
240
|
-
|
|
241
|
-
AS SKILLS DA CASA (leia ANTES de operar, não improvise o fluxo):
|
|
242
|
-
O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por este mesmo servidor: sem login, sem custo, sem rede. Ler a skill é mais barato que errar uma geração que cobra.
|
|
243
|
-
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
244
|
-
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
245
|
-
Slugs: ${skillMenuLine()}
|
|
246
|
-
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), 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.
|
|
247
|
-
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
248
|
-
|
|
249
|
-
REGRA DE OURO:
|
|
250
|
-
- PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
|
|
251
|
-
- LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
|
|
252
|
-
- Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes. Vídeo é o mais caro da casa: confirme com ele antes de disparar.
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
239
|
+
export const SAPIENS_INSTRUCTIONS = `Você opera o Sapiens Sintéticos (sapiensinteticos.com) NA CONTA de um usuário logado. Cada tool age de verdade na conta dele e muitas COBRAM Sinapses (o crédito da casa). Aja como operador, não no chute.
|
|
240
|
+
|
|
241
|
+
AS SKILLS DA CASA (leia ANTES de operar, não improvise o fluxo):
|
|
242
|
+
O passo a passo travado de cada fluxo mora na tool sapiens_skill, servida por este mesmo servidor: sem login, sem custo, sem rede. Ler a skill é mais barato que errar uma geração que cobra.
|
|
243
|
+
- sapiens_skill action=list -> o índice (slug + quando usar cada uma).
|
|
244
|
+
- sapiens_skill action=get name=<slug> -> a skill inteira.
|
|
245
|
+
Slugs: ${skillMenuLine()}
|
|
246
|
+
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), 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), gerar em motor de fora com crédito próprio e trazer a peça pro acervo (pontes). Perdido no começo: primeiros-passos.
|
|
247
|
+
Cliente que lê resources MCP acha o MESMO conteúdo em skill://sapiens/<slug>/SKILL.md.
|
|
248
|
+
|
|
249
|
+
REGRA DE OURO:
|
|
250
|
+
- PRIMEIRO CONTATO ou "o que você faz?"/"como começo?"/"o que dá pra fazer?": chame sapiens_meta action=start e MOSTRE o resultado na sua voz. Sem login, ele ensina a conectar; logado, traz saldo + primeiros poderes com exemplos. É a porta de entrada: não despeje a lista inteira de tools, deixe o start guiar.
|
|
251
|
+
- LOGO APÓS UM LOGIN BEM-SUCEDIDO (action=login retornou ok): chame action=start na sequência e mostre a porta de entrada. O recém-chegado não sabe o que pedir; não o deixe na tela em branco, guie a primeira jogada sem ele precisar perguntar.
|
|
252
|
+
- Antes de gerar algo caro (imagem/música/vídeo), cheque saldo: sapiens_meta action=credits (ou subscription). Saldo baixo, avise o usuário antes. Vídeo é o mais caro da casa: confirme com ele antes de disparar.
|
|
253
|
+
- SEM SALDO, ou motor que a casa não tem: a casa é ponte, não muro. Se a pessoa tem crédito na Kie, na fal, na Magnific, na Sogni ou na própria placa, ela gera LÁ com a ficha do personagem daqui (sapiens_character action=get) e a peça volta pro acervo por sapiens_gallery action=ingest. A chave dela NUNCA passa pelo Sapiens (nem em arg, nem no chat). sapiens_meta action=pontes diz o que ela já tem configurado; skill 'pontes' tem o passo a passo.
|
|
254
|
+
- REGRA DO TIMEOUT: geração SÍNCRONA pode estourar o teto de ~120s do cliente e voltar 'Timeout' MESMO tendo gerado e COBRADO. Nunca repita às cegas: confira antes onde o resultado cairia (a tabela de onde conferir está na skill 'primeiros-passos').
|
|
255
|
+
- "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
|
|
256
|
+
- 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.
|
|
257
|
+
|
|
258
|
+
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.
|
|
259
|
+
|
|
259
260
|
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.`;
|
|
260
261
|
// Annotations MCP: título humano + dica read-only. São HINTS (não-confiáveis por
|
|
261
262
|
// spec): quem gateia de verdade continua o servidor (saldo, gate de admin,
|
package/dist/remote.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createMcpHandler, withMcpAuth } from "mcp-handler";
|
|
2
|
+
import { runWithClientHint } from "./clientIdentity.js";
|
|
2
3
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import { buildToolList, callTool, SAPIENS_INSTRUCTIONS, } from "./registry.js";
|
|
4
5
|
import { getPrompt, listPrompts } from "./prompts.js";
|
|
@@ -263,7 +264,10 @@ export function createSapiensRemoteHandler(opts) {
|
|
|
263
264
|
if (!tokenLimiter.hit(token)) {
|
|
264
265
|
return tooMany("Muitas requisições dessa sessão num minuto. Espere um pouco e tente de novo.");
|
|
265
266
|
}
|
|
266
|
-
|
|
267
|
+
// Stateless: o initialize não fica entre requests, então a pista do client
|
|
268
|
+
// aqui é o User-Agent (prefixo "ua:" avisa o backend que é pista, não nome).
|
|
269
|
+
const ua = req.headers.get("user-agent") ?? undefined;
|
|
270
|
+
return runWithSessionToken(token, () => runWithClientHint(ua ? `ua:${ua}` : undefined, () => base(req)));
|
|
267
271
|
};
|
|
268
272
|
const authed = withMcpAuth(inner, (_req, bearerToken) => {
|
|
269
273
|
// Shape-check só (sem rede): a validação de VERDADE acontece por request
|
package/dist/skills.js
CHANGED
|
@@ -729,6 +729,76 @@ A foto NUNCA vai pra banco nenhum, nem da casa nem de terceiro:
|
|
|
729
729
|
- Idiomas com \`pct\` honesto (nativo ~100, fluente ~85, intermédio ~55).
|
|
730
730
|
- Visto/disponibilidade/mudança entram em \`highlight\` (o box perto do contato), não no resumo.`,
|
|
731
731
|
},
|
|
732
|
+
{
|
|
733
|
+
name: "pontes",
|
|
734
|
+
title: "Pontes: gerar fora com crédito próprio e trazer pro acervo",
|
|
735
|
+
description: "A pessoa tem crédito na Kie, na fal, na Magnific, na Sogni ou na própria placa e quer usar o personagem dela lá, ou está sem Sinapse pra vídeo. Como levar a ficha, gerar no balcão dela (a chave nunca passa pelo Sapiens) e trazer a peça de volta por sapiens_gallery action=ingest, com a ficha inteira. Puxe quando ouvir 'tem Kie aí?', 'uso a fal', 'gastei minhas Sinapses', 'gero na Magnific', 'tenho ComfyUI'.",
|
|
736
|
+
body: `## O que é uma ponte
|
|
737
|
+
|
|
738
|
+
O Sapiens é uma das pontes, não um muro. A pessoa monta o personagem aqui (ficha, passaporte, referências) e gera a peça ONDE TEM CRÉDITO: Kie, fal, Magnific, Sogni, Krea, Replicate, a própria placa. A peça volta pro acervo dela como NATIVA, com motor, prompt, custo real e personagem na ficha, custo 0 em Sinapses. O portfólio dela cresce aqui; o dinheiro dela sai de onde ela já pôs.
|
|
739
|
+
|
|
740
|
+
**A regra da chave, sem exceção:** a chave do provedor NUNCA passa pelo Sapiens. Não vai em arg de tool, não vai em prompt, não vai colada no chat. Ela mora no ambiente da pessoa (variável \`FAL_KEY\`, \`KIE_API_KEY\`, um conector MCP do provedor ligado na conversa, o SDK dela) e quem chama o motor é o harness dela, na máquina dela. O Sapiens entra ANTES (a ficha) e DEPOIS (o ingest). Se a pessoa colar a chave no chat por engano, diga pra ela rotacionar a chave no provedor, e siga sem usar o valor.
|
|
741
|
+
|
|
742
|
+
## Passo 1: descobrir onde a pessoa tem crédito
|
|
743
|
+
|
|
744
|
+
\`sapiens_meta action=pontes\` (sem login, sem custo) devolve o catálogo de balcões e, no MCP instalado, o que já está configurado no ambiente dela, lido pelo NOME da variável (o valor nunca é lido nem devolvido). No remoto (claude.ai, ChatGPT) ele não olha nada: pergunte.
|
|
745
|
+
|
|
746
|
+
Ordem de preferência quando há mais de uma opção, pelo que custa DE VERDADE pra pessoa:
|
|
747
|
+
1. O que ela já paga fixo (assinatura Sogni, Magnific com crédito parado, a própria placa): custo marginal zero.
|
|
748
|
+
2. O balcão onde ela tem crédito pré-pago (Kie, fal, WaveSpeed, Krea, Replicate): custo por peça, em dólar.
|
|
749
|
+
3. \`sapiens_video\` / \`sapiens_image\` em Sinapse: o único que serve motor exclusivo da casa e o único com a régua de qualidade da casa por trás. Não é o último por ser pior: é o último por ser o mais caro por peça.
|
|
750
|
+
|
|
751
|
+
Confirme com ela ANTES de disparar em qualquer balcão pago. Geração de fora também custa dinheiro real, só que dela.
|
|
752
|
+
|
|
753
|
+
## Passo 2: a ficha portátil
|
|
754
|
+
|
|
755
|
+
\`sapiens_character action=get characterId=<id>\` (ou \`list_mine\` pra achar o id). O que viaja:
|
|
756
|
+
|
|
757
|
+
- \`passportPrompt\`: o bloco já montado pra colar no prompt do motor (descritor em inglês + negative travado + locks). Cole VERBATIM no começo do prompt; a cena vem depois.
|
|
758
|
+
- \`imageUrls\` e \`mainImageUrl\`: as referências. São URLs públicas do CDN da casa, aceitas direto pelos motores que recebem referência por URL (Kie, fal, Krea). Motor que quer arquivo (ComfyUI, alguns SDKs): baixe a imagem antes.
|
|
759
|
+
- \`passport.recipes\`: as receitas medidas por tipo de peça (\`kind\`, \`engine\`, \`prompt\`, \`note\`). Se existe receita pro motor que ela vai usar, é ELA que vai, verbatim.
|
|
760
|
+
- \`passport.refs\` por papel (rosto, proporção, tatuagem): ref sem papel ninguém sabe usar. Use a de rosto como referência de identidade, a de proporção como frame inicial quando o motor é image-to-video.
|
|
761
|
+
|
|
762
|
+
Regras que valem fora igual dentro: prompt SEM idade em número; personagem nunca menor (a casa trabalha com 23+ no foco e 18 é piso absoluto); motor citado pelo nome que ele tem no provedor. Prompt novo que funcionar lá fora: grave de volta com \`sapiens_character action=set_passport\` (campo \`recipes\`, prompt verbatim + note com o que provou). É isso que faz a próxima rodada não redescobrir.
|
|
763
|
+
|
|
764
|
+
## Passo 3: gerar lá fora
|
|
765
|
+
|
|
766
|
+
O que a casa MEDIU em cada balcão (o resto está na doc do provedor; não invente parâmetro):
|
|
767
|
+
|
|
768
|
+
| Balcão | Onde a chave mora | O que roda bem | Armadilha medida |
|
|
769
|
+
|---|---|---|---|
|
|
770
|
+
| **Kie** | \`KIE_API_KEY\` | Kling 3.0, Kling Motion, MiniMax/Hailuo, WAN; imagem também | Um endpoint pra tudo: \`POST createTask\` com \`model\` + \`input\`, depois \`recordInfo\` até estado final. HTTP 200 NÃO é sucesso: o \`code\` vem no corpo (422 modelo inexistente, 500 campo faltando, 401 chave). O \`resultJson\` é STRING com JSON dentro (\`resultUrls\` está lá). A URL do resultado expira em 24h e a mídia some em 14 dias: ingira no mesmo turno. Rate limit de 20 criações por 10s por conta. |
|
|
771
|
+
| **fal.ai** | \`FAL_KEY\` | Krea 2 (inclusive sem freio), Kling, WAN, Seedance, Flux | \`POST https://fal.run/<endpoint>\` síncrono com \`Authorization: Key <FAL_KEY>\`, ou a fila em \`queue.fal.run\` pra vídeo. O modelo vai no SLUG do endpoint, não no corpo. Entrega em \`fal.media\`. Slug \`fal-\` na casa entra como classe +18 (sai da vitrine anônima, fica no perfil). |
|
|
772
|
+
| **WaveSpeed** | \`WAVESPEED_API_KEY\` | Flux.2 Klein, Krea 2 Livre, WAN 2.2, Kling, Shot Mimic | É o balcão que a casa mais usa por trás do \`sapiens_video\`; o que a casa serve em Sinapse a pessoa pode rodar direto com a chave dela. Slug \`wavespeed-\` é classe +18 na casa. |
|
|
773
|
+
| **Sogni** | \`SOGNI_API_KEY\` (SDK \`@sogni-ai/sogni-client\`) | MiniMax H3 inteira, WAN 2.2 com animate, LTX 2.3 e 2.5, Krea 2 Turbo, Identity Edit | Assinatura Unlimited cobre o catálogo aberto (\`billingMode: "subscription"\`, custo marginal zero); Seedance e GPT Image saem em Spark comprado. UM socket por conta: um job por vez. Vídeo de 10s+ leva 20 min de parede; rode em background e espere. Matar o processo local NÃO cancela o job (cancele pela API). |
|
|
774
|
+
| **Magnific / Freepik** | conector MCP na conversa (sem chave) | Seedance 2.0 e 2.5 (até 30s e 1080p, mais que a casa), MiniMax H3, upscale, relight | A URL assinada do render expira no mesmo dia: ingira na hora. \`multi_prompt\` junto com \`references\` quebra no Seedance 2.0: plano único com prompt longo. A frase "preserve their identity" é recusada pelo H3 de lá: escreva "keep the same face, body proportions, outfit". |
|
|
775
|
+
| **Krea API** | \`KREA_API_KEY\` | Krea 2 Turbo/Medium/Large, MiniMax H3 Max Turbo, Seedream, Veo, Seedance | Preço fixo por geração, saldo pré-pago separado do app. Filtra NSFW e job que falha (inclusive por moderação) não cobra. Sem img2img no Krea 2 e sem LoRA de fora. |
|
|
776
|
+
| **A própria máquina** | nada (ComfyUI, SD local, runner) | o que a placa aguenta: LTX, Klein, Krea 2 com LoRA, H3 destilado | Custo zero em dinheiro, pago em tempo. A peça entra por \`filePath\`, slug \`bancada-<motor>\` (ex: \`bancada-ltx-2.5\`). Guarde o \`.json\` da geração ao lado do arquivo: é de lá que sai o prompt verbatim e a seed pra ficha. |
|
|
777
|
+
|
|
778
|
+
Não repita geração paga às cegas: se a chamada deu timeout, consulte o job no provedor antes de criar outro. Peça grande (master de 30s em 1080p) reencoda em peso de web ANTES de trazer: \`ffmpeg -i in.mp4 -c:v libx264 -crf 23 -preset slow -movflags +faststart out.mp4\`. Guarde o master local.
|
|
779
|
+
|
|
780
|
+
## Passo 4: trazer pra casa
|
|
781
|
+
|
|
782
|
+
\`sapiens_gallery action=ingest\`, no MESMO turno do render (URL de render expira):
|
|
783
|
+
|
|
784
|
+
- A peça: \`filePath\` (arquivo local; só no MCP instalado, é a porta de quem gerou na máquina ou baixou antes), ou \`sourceUrl\` (o link do render; quem baixa é o MCP na máquina da pessoa, não o servidor), ou \`base64\` (imagem).
|
|
785
|
+
- \`externalEngine\`: \`provedor-motor\`, sempre. \`kie-kling-3.0\`, \`fal-krea-2\`, \`sogni-minimax-h3\`, \`magnific-seedance-2.5\`, \`bancada-ltx-2.5\`. Marca sozinha é recusada: é este campo que a ficha mostra em "Motor".
|
|
786
|
+
- \`prompt\`: o texto EXATO mandado ao motor. Não é resumo, não é título. É o que permite regerar.
|
|
787
|
+
- \`externalCost\`: na moeda de lá ("420 créditos Kie", "US$ 0,35 na fal", "0, assinatura Sogni", "0, 17 min na RTX 5060 Ti").
|
|
788
|
+
- \`characterId\` (ou \`characterIds\` em ordem de cena quando há mais de uma criatura) e \`referenceImageIds\` (as peças da casa que serviram de ref). Sem os dois a peça entra órfã: publica, mas não conta no personagem.
|
|
789
|
+
- \`aspectRatio\` e \`size\` como saíram do motor.
|
|
790
|
+
- O carimbo +18 nasce na entrada pela CLASSE do provedor, porque a peça já chega renderizada e nenhuma guarda de geração da casa alcança os bytes. Peso aberto (Sogni, Replicate, Hugging Face, fal, WaveSpeed, Civitai, a própria placa) ou provedor que a casa não conhece: a peça entra COM o carimbo, que só a curadoria tira (igual ao motor sem freio da casa). Serviço com moderação (Kie, Magnific, Freepik, faceless, Krea, Midjourney, Runway, Higgsfield): entra SEM carimbo. Nome de motor com \`-spicy\`, \`-nsfw\`, \`-uncensored\` carimba por classe também. \`unfiltered: true\` é a declaração da pessoa pra motor de nome neutro em modo livre num serviço que modera: carimba em nome dela, e ela tira depois na ficha. Carimbo é alcance, não existência: publica, fica no perfil e no link, sai da vitrine anônima. Avise a pessoa quando a peça entrar carimbada por classe: ela precisa saber que a vitrine anônima não vai mostrar.
|
|
791
|
+
|
|
792
|
+
Tetos: 12 MB imagem, 60 MB vídeo por peça; 40 peças por dia e 300 no estoque por conta. Formatos: mp4, mov, png, jpg, webp.
|
|
793
|
+
|
|
794
|
+
O que a peça vira: NATIVA, PRIVADA, publicável depois por \`action=publish\` (ato da pessoa, nunca seu), custo 0 em Sinapses, sem assinatura C2PA (o motor rodou fora). Idempotente: repetir a mesma \`sourceUrl\` (ou o mesmo arquivo) corrige a ficha em vez de duplicar. Depois de ingerir, \`action=list kind=video\` traz a capa e a ficha: mostre a peça, não o link solto. Não apague o arquivo local antes de ver a peça no acervo.
|
|
795
|
+
|
|
796
|
+
## Quando a ponte NÃO é o caminho
|
|
797
|
+
|
|
798
|
+
- Motor exclusivo da casa: Helen TTS, Musicator, os templates de take (\`templateSlug\`), Sombras, o carimbo assinado. Isso só existe em Sinapse.
|
|
799
|
+
- A pessoa tem Sinapse e o take é curto: \`sapiens_video\` é um comando só, com a régua de qualidade da casa. Menos atrito vence quando o custo cabe.
|
|
800
|
+
- Peça achada pronta na internet, sem direção dela: é \`action=upload\` (privada pra sempre), não ingest.`,
|
|
801
|
+
},
|
|
732
802
|
];
|
|
733
803
|
export const SKILLS = Object.fromEntries(SKILL_LIST.map((s) => [s.name, s]));
|
|
734
804
|
/** Monta o SKILL.md completo (frontmatter do formato aberto + corpo). */
|
package/dist/tools/character.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { httpUrl } from "../schema.js";
|
|
3
3
|
import { convexQuery, convexMutation, convexAction, getSessionToken } from "../convexClient.js";
|
|
4
|
+
import { getClientName } from "../clientIdentity.js";
|
|
4
5
|
/**
|
|
5
6
|
* sapiens_character — personagens (character sheets) do Sapiens.
|
|
6
7
|
*
|
|
@@ -333,6 +334,7 @@ export async function character(args) {
|
|
|
333
334
|
const sessionToken = getSessionToken();
|
|
334
335
|
return await convexMutation("influencers:mcpCreateCharacter", {
|
|
335
336
|
sessionToken,
|
|
337
|
+
client: getClientName(),
|
|
336
338
|
name: args.name,
|
|
337
339
|
fullName: args.fullName,
|
|
338
340
|
gender: args.gender,
|
|
@@ -441,6 +443,7 @@ export async function character(args) {
|
|
|
441
443
|
const sessionToken = getSessionToken();
|
|
442
444
|
return await convexMutation("influencers:mcpActivateCharacter", {
|
|
443
445
|
sessionToken,
|
|
446
|
+
client: getClientName(),
|
|
444
447
|
characterId: args.characterId,
|
|
445
448
|
});
|
|
446
449
|
}
|
package/dist/tools/gallery.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { convexAction, convexMutation, describeConvexError, getSessionToken, isRemoteContext, } from "../convexClient.js";
|
|
4
5
|
/**
|
|
5
6
|
* Galeria do usuário: browse, publicação e UPLOAD de peça de fora.
|
|
6
7
|
* Wrapper sobre `desktopMcp.galleryList` / `galleryGet` / `gallerySetPublic` e
|
|
@@ -13,18 +14,21 @@ import { convexAction, convexMutation, getSessionToken, isRemoteContext, } from
|
|
|
13
14
|
* - Publicar/despublicar a própria imagem (galeria pública + feed Pinterest)
|
|
14
15
|
* - Subir peça gerada FORA da casa (action=upload), que entra privada e serve
|
|
15
16
|
* de referência pras gerações daqui
|
|
16
|
-
* - Ingerir peça gerada em motor EXTERNO com receita da
|
|
17
|
-
*
|
|
17
|
+
* - Ingerir peça gerada em motor EXTERNO com a receita da pessoa
|
|
18
|
+
* (action=ingest, qualquer conta desde set/2026: a PONTE), que entra como
|
|
19
|
+
* peça nativa: timeline, publicação, comunidade
|
|
18
20
|
*
|
|
19
21
|
* O upload resolve URL/arquivo pra base64 AQUI, no cliente, e manda os bytes
|
|
20
22
|
* prontos pro Convex. O servidor não busca URL nenhuma (sem SSRF novo lá).
|
|
21
23
|
*
|
|
22
|
-
* O INGEST
|
|
23
|
-
* fechada de hosts
|
|
24
|
-
* a
|
|
25
|
-
*
|
|
26
|
-
* upload
|
|
27
|
-
*
|
|
24
|
+
* O INGEST tem duas portas no servidor com o mesmo contrato de peça: a do dono
|
|
25
|
+
* (o servidor baixa de allowlist fechada de hosts, convex/externalIngest.ts) e
|
|
26
|
+
* a ponte (os bytes são resolvidos AQUI, na máquina da pessoa, sobem pro
|
|
27
|
+
* storage do Convex e o servidor só move pro Bunny, convex/externalIngestNode.ts).
|
|
28
|
+
* A diferença entre upload e ingest não é de onde vieram os bytes, é de quem
|
|
29
|
+
* foi a DIREÇÃO: peça achada pronta é upload; peça que a pessoa dirigiu (a
|
|
30
|
+
* ficha do personagem dela, o prompt dela) e um motor terceiro só renderizou é
|
|
31
|
+
* ingest. A chave do provedor nunca passa por aqui nem pelo servidor.
|
|
28
32
|
*/
|
|
29
33
|
export const gallerySchema = z.object({
|
|
30
34
|
action: z.enum([
|
|
@@ -47,6 +51,10 @@ export const gallerySchema = z.object({
|
|
|
47
51
|
.string()
|
|
48
52
|
.optional()
|
|
49
53
|
.describe("Filtro de model (action=list). Ex: 'nano-banana-max' pra ver só Pro."),
|
|
54
|
+
skill: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Filtro por SKILL que dirigiu a peça (action=list): devolve só o que saiu daquela skill, pelo slug exato que a geração gravou (ex: 'camarim', 'escala-gigantismo'). É o que responde 'me mostra tudo que saiu do camarim'. Com filtro, a busca varre as últimas 200 peças antes de cortar no limit, então acha coisa antiga. Peça sem marcação nenhuma volta com skills null e nunca casa com filtro."),
|
|
50
58
|
kind: z
|
|
51
59
|
.enum(["image", "video", "all"])
|
|
52
60
|
.optional()
|
|
@@ -59,23 +67,23 @@ export const gallerySchema = z.object({
|
|
|
59
67
|
.boolean()
|
|
60
68
|
.optional()
|
|
61
69
|
.describe("Default false. Quando true, action=get devolve os bytes em base64 (pesado, evite em listagens)."),
|
|
62
|
-
// --- action=upload
|
|
70
|
+
// --- action=upload e action=ingest: de onde vêm os bytes ---
|
|
63
71
|
sourceUrl: z
|
|
64
72
|
.string()
|
|
65
73
|
.optional()
|
|
66
|
-
.describe("action=upload: URL https direta da
|
|
74
|
+
.describe("action=upload/ingest: URL https direta da peça pronta no motor de fora (o link do render da Kie, da fal, da Magnific). No ingest ela também é o RASTRO e a chave de idempotência: repetir a mesma sourceUrl corrige a ficha em vez de duplicar. Quem baixa é o cliente MCP (na sua máquina, no stdio; no remoto, o servidor do site): URL de render expira (Kie em 24h, Magnific no mesmo dia), então ingira no mesmo turno do render. Alternativa a filePath/base64."),
|
|
67
75
|
filePath: z
|
|
68
76
|
.string()
|
|
69
77
|
.optional()
|
|
70
|
-
.describe("action=upload: caminho ABSOLUTO de um arquivo
|
|
78
|
+
.describe("action=upload/ingest: caminho ABSOLUTO de um arquivo local (png, jpg, webp, mp4, mov). SÓ no MCP instalado (stdio): é a porta de quem gerou na própria máquina ou baixou o render antes; no remoto use sourceUrl. No ingest, o sha256 do arquivo vira a chave de idempotência."),
|
|
71
79
|
base64: z
|
|
72
80
|
.string()
|
|
73
81
|
.optional()
|
|
74
|
-
.describe("action=upload: os bytes já em base64, se você mesmo os tem em mãos."),
|
|
82
|
+
.describe("action=upload/ingest: os bytes já em base64, se você mesmo os tem em mãos (imagem; vídeo vai por filePath ou sourceUrl)."),
|
|
75
83
|
mimeType: z
|
|
76
84
|
.string()
|
|
77
85
|
.optional()
|
|
78
|
-
.describe("action=upload: tipo do arquivo (image/png,
|
|
86
|
+
.describe("action=upload/ingest: tipo do arquivo (image/png, video/mp4...). Detectado sozinho a partir da URL/extensão/resposta HTTP quando omitido."),
|
|
79
87
|
note: z
|
|
80
88
|
.string()
|
|
81
89
|
.optional()
|
|
@@ -84,7 +92,7 @@ export const gallerySchema = z.object({
|
|
|
84
92
|
externalEngine: z
|
|
85
93
|
.string()
|
|
86
94
|
.optional()
|
|
87
|
-
.describe("action=ingest
|
|
95
|
+
.describe("action=ingest: qual motor renderizou, sempre no formato provedor-motor ('kie-kling-3.0', 'fal-krea-2', 'magnific-minimax-h3', 'sogni-minimax-h3', 'bancada-ltx-2.5'). É ESTE campo que a ficha da peça mostra em 'Motor', então a marca sozinha ('kie', 'magnific') é recusada pelo servidor: provedor é a loja, não o motor. O provedor decide o carimbo +18 na entrada: peso aberto (sogni-, replicate-, hf-, fal-, wavespeed-, civitai-, bancada-, comfy-, local-) ou provedor que a casa não conhece entra com o carimbo por classe (publica e fica no perfil, só não vai pra vitrine anônima); serviço com moderação (kie-, magnific-, freepik-, faceless-, krea-, midjourney-, runway-, higgsfield-, modelark-) entra sem."),
|
|
88
96
|
externalCost: z
|
|
89
97
|
.string()
|
|
90
98
|
.optional()
|
|
@@ -117,6 +125,10 @@ export const gallerySchema = z.object({
|
|
|
117
125
|
.union([z.array(z.string()), z.string()])
|
|
118
126
|
.optional()
|
|
119
127
|
.describe("QUEM ESTÁ EM CENA, quando é mais de uma criatura (action=ingest e action=cast). Em ordem de cena: o PRIMEIRO é o protagonista (é ele que a ficha e o cartão do vídeo mostram) e o resto vira elenco, então a peça conta na ficha de TODOS eles em vez de sumir da segunda. Teto de 6. Use quando a fita foi dirigida com duas folhas de personagem: sem isto, ela aparece só na ficha de um. Aceita lista ou os ids separados por vírgula, personagem seu ou público. Pra peça de uma criatura só continue usando characterId."),
|
|
128
|
+
unfiltered: z
|
|
129
|
+
.boolean()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("action=ingest: true quando o motor rodou SEM filtro de conteúdo (modo livre, spicy, uncensored) num serviço que normalmente modera. A peça nasce com o carimbo +18 da casa DECLARADO por você (você pode tirar depois na ficha): publica, fica no seu perfil e no link direto, e sai da vitrine anônima e dos feeds. Não precisa dizer pra provedor de peso aberto (Sogni, Replicate, Hugging Face, fal, WaveSpeed, Civitai, a própria placa): esses já entram com o carimbo por CLASSE, que só a curadoria tira, igual ao motor sem freio da casa. Serviço com moderação (Kie, Magnific, Freepik, faceless, Krea, Midjourney, Runway, Higgsfield) entra sem carimbo, a menos que você diga isto ou o nome do motor tenha '-spicy', '-nsfw', '-uncensored'."),
|
|
120
132
|
replace: z
|
|
121
133
|
.boolean()
|
|
122
134
|
.optional()
|
|
@@ -130,6 +142,13 @@ export const gallerySchema = z.object({
|
|
|
130
142
|
// que falhar depois de trafegar o base64 inteiro.
|
|
131
143
|
const MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
|
|
132
144
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
145
|
+
// Tetos da PONTE (action=ingest pela porta do membro), espelho de
|
|
146
|
+
// convex/shared/ingestRules.ts. O servidor devolve os dele na uploadUrl e é o
|
|
147
|
+
// que manda; estes só recusam antes de trafegar o arquivo.
|
|
148
|
+
const MAX_INGEST_VIDEO_BYTES = 60 * 1024 * 1024;
|
|
149
|
+
const MAX_INGEST_IMAGE_BYTES = 12 * 1024 * 1024;
|
|
150
|
+
// Render de vídeo demora mais pra baixar que uma imagem.
|
|
151
|
+
const INGEST_FETCH_TIMEOUT_MS = 120_000;
|
|
133
152
|
const EXT_MIME = {
|
|
134
153
|
png: "image/png",
|
|
135
154
|
jpg: "image/jpeg",
|
|
@@ -137,6 +156,9 @@ const EXT_MIME = {
|
|
|
137
156
|
webp: "image/webp",
|
|
138
157
|
gif: "image/gif",
|
|
139
158
|
avif: "image/avif",
|
|
159
|
+
// Vídeo entra pela ponte (action=ingest); o upload continua só imagem.
|
|
160
|
+
mp4: "video/mp4",
|
|
161
|
+
mov: "video/quicktime",
|
|
140
162
|
};
|
|
141
163
|
function mimeFromPath(p) {
|
|
142
164
|
const ext = p.split("?")[0].split("#")[0].split(".").pop()?.toLowerCase();
|
|
@@ -217,6 +239,101 @@ async function resolveUploadBytes(args) {
|
|
|
217
239
|
}
|
|
218
240
|
throw new Error("action=upload exige a peça: sourceUrl (https), filePath (local, stdio) ou base64.");
|
|
219
241
|
}
|
|
242
|
+
const AVISO_INGEST = "Peça NATIVA: entra na timeline PRIVADA (isPublic false), igual a qualquer geração da casa. Ela NÃO publica sozinha e NÃO vai pra comunidade: publicar é ato separado do dono, por action=publish. Custo em Sinapses é 0 e ela NÃO leva assinatura C2PA, porque o motor rodou fora. Use ingest só quando a receita (ficha do personagem, prompt, folha) foi sua; peça achada pronta é action=upload. Mostre a peça com action=list kind=video (capa + player) em vez de devolver link solto.";
|
|
243
|
+
/**
|
|
244
|
+
* Os bytes da PONTE, resolvidos aqui (na máquina da pessoa no stdio; no
|
|
245
|
+
* servidor do site no remoto, com o mesmo guard de host público do upload).
|
|
246
|
+
* Devolve também a chave de idempotência: a própria URL quando veio de URL, ou
|
|
247
|
+
* `sha256:<hash>` do conteúdo pra arquivo local e base64, pra o retry corrigir
|
|
248
|
+
* a ficha em vez de duplicar a peça.
|
|
249
|
+
*/
|
|
250
|
+
async function resolveIngestBytes(args) {
|
|
251
|
+
const tetoDe = (mime) => mime.startsWith("video/") ? MAX_INGEST_VIDEO_BYTES : MAX_INGEST_IMAGE_BYTES;
|
|
252
|
+
const confereTeto = (bytes, mime, oQue) => {
|
|
253
|
+
const teto = tetoDe(mime);
|
|
254
|
+
if (bytes.byteLength > teto) {
|
|
255
|
+
throw new Error(`${oQue} grande demais: ${(bytes.byteLength / 1024 / 1024).toFixed(1)} MB (teto ${Math.round(teto / 1024 / 1024)} MB). ` +
|
|
256
|
+
(mime.startsWith("video/")
|
|
257
|
+
? "Reencode em peso de web antes de trazer: ffmpeg -i in.mp4 -c:v libx264 -crf 23 -preset slow -movflags +faststart out.mp4 (um take de 15s em 1080p fica abaixo de 15 MB)."
|
|
258
|
+
: "Exporte em webp ou jpg com qualidade 85."));
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const hexDe = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
262
|
+
if (args.base64?.trim()) {
|
|
263
|
+
const mimeType = args.mimeType ||
|
|
264
|
+
args.base64.match(/^data:([^;]+);base64,/)?.[1] ||
|
|
265
|
+
"image/png";
|
|
266
|
+
const bytes = Buffer.from(args.base64.trim().replace(/^data:[^;]+;base64,/, ""), "base64");
|
|
267
|
+
confereTeto(bytes, mimeType, "Peça");
|
|
268
|
+
const hex = hexDe(bytes);
|
|
269
|
+
return { bytes, mimeType, sourceKey: `sha256:${hex}`, sha256: hex };
|
|
270
|
+
}
|
|
271
|
+
if (args.filePath) {
|
|
272
|
+
if (isRemoteContext()) {
|
|
273
|
+
throw new Error("filePath só funciona no MCP instalado (stdio). No remoto, mande sourceUrl.");
|
|
274
|
+
}
|
|
275
|
+
const bytes = await readFile(args.filePath);
|
|
276
|
+
const mimeType = args.mimeType || mimeFromPath(args.filePath) || "image/png";
|
|
277
|
+
confereTeto(bytes, mimeType, "Arquivo");
|
|
278
|
+
const hex = hexDe(bytes);
|
|
279
|
+
return { bytes, mimeType, sourceKey: `sha256:${hex}`, sha256: hex };
|
|
280
|
+
}
|
|
281
|
+
if (args.sourceUrl) {
|
|
282
|
+
const u = assertPublicHttpsUrl(args.sourceUrl);
|
|
283
|
+
const res = await fetch(u, {
|
|
284
|
+
redirect: "follow",
|
|
285
|
+
signal: AbortSignal.timeout(INGEST_FETCH_TIMEOUT_MS),
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
throw new Error(`Baixar o render falhou: HTTP ${res.status} ${res.statusText}. URL de render expira (Kie em 24h, Magnific no mesmo dia): se passou do prazo, baixe de novo no provedor ou mande o arquivo por filePath.`);
|
|
289
|
+
}
|
|
290
|
+
// Revalida o destino final: um 3xx pra host privado seria o buraco que o
|
|
291
|
+
// assertPublicHttpsUrl da URL inicial não vê.
|
|
292
|
+
if (res.url)
|
|
293
|
+
assertPublicHttpsUrl(res.url);
|
|
294
|
+
const headerMime = res.headers.get("content-type")?.split(";")[0]?.trim();
|
|
295
|
+
const mimeType = args.mimeType || mimeFromPath(u.pathname) || headerMime || "image/png";
|
|
296
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
297
|
+
confereTeto(bytes, mimeType, "Render");
|
|
298
|
+
return { bytes, mimeType, sourceKey: args.sourceUrl, sha256: hexDe(bytes) };
|
|
299
|
+
}
|
|
300
|
+
throw new Error("action=ingest exige a peça: sourceUrl (https), filePath (local, stdio) ou base64.");
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A ponte, de ponta a ponta: bytes -> URL assinada do storage -> action Node
|
|
304
|
+
* que move pro Bunny e grava a ficha. Três chamadas, e a chave de provedor não
|
|
305
|
+
* aparece em nenhuma delas, porque o motor já rodou antes, no harness da pessoa.
|
|
306
|
+
*/
|
|
307
|
+
async function ingestPelaPonte(sessionToken, args, ficha) {
|
|
308
|
+
const { bytes, mimeType, sourceKey, sha256 } = await resolveIngestBytes(args);
|
|
309
|
+
const porta = await convexMutation("externalIngest:mcpIngestUploadUrl", { sessionToken });
|
|
310
|
+
const teto = mimeType.startsWith("video/")
|
|
311
|
+
? porta.maxVideoBytes
|
|
312
|
+
: porta.maxImageBytes;
|
|
313
|
+
if (bytes.byteLength > teto) {
|
|
314
|
+
throw new Error(`Peça grande demais pro servidor: ${(bytes.byteLength / 1024 / 1024).toFixed(1)} MB (teto ${Math.round(teto / 1024 / 1024)} MB). Reencode em peso de web e tente de novo.`);
|
|
315
|
+
}
|
|
316
|
+
const subida = await fetch(porta.uploadUrl, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": mimeType },
|
|
319
|
+
body: bytes,
|
|
320
|
+
});
|
|
321
|
+
if (!subida.ok) {
|
|
322
|
+
throw new Error(`Subir a peça pro storage falhou: HTTP ${subida.status}. Tente de novo; se repetir, peça outra uploadUrl (ela vale 1h).`);
|
|
323
|
+
}
|
|
324
|
+
const { storageId } = (await subida.json());
|
|
325
|
+
return await convexAction("externalIngestNode:mcpIngestFromStorage", {
|
|
326
|
+
sessionToken,
|
|
327
|
+
storageId,
|
|
328
|
+
mimeType,
|
|
329
|
+
sourceUrl: sourceKey,
|
|
330
|
+
// Prova de posse: o servidor confere contra o sha256 do storage antes de
|
|
331
|
+
// mover ou apagar qualquer byte.
|
|
332
|
+
contentSha256: sha256,
|
|
333
|
+
unfiltered: args.unfiltered,
|
|
334
|
+
...ficha,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
220
337
|
/**
|
|
221
338
|
* Normaliza `referenceImageIds` pro formato que o Convex espera (lista).
|
|
222
339
|
*
|
|
@@ -255,6 +372,7 @@ export async function gallery(args) {
|
|
|
255
372
|
sessionToken,
|
|
256
373
|
limit: args.limit ?? 20,
|
|
257
374
|
model: args.model,
|
|
375
|
+
skill: args.skill,
|
|
258
376
|
kind: args.kind,
|
|
259
377
|
});
|
|
260
378
|
return {
|
|
@@ -303,16 +421,32 @@ export async function gallery(args) {
|
|
|
303
421
|
aviso: "Peça enviada de fora: fica privada, é sua e a responsabilidade é sua. Não publica nem vai pra comunidade. Use como referência (sourceImageIds em sapiens_image) e publique o que gerar a partir dela.",
|
|
304
422
|
};
|
|
305
423
|
}
|
|
424
|
+
// ingest: a peça que a pessoa DIRIGIU (ficha do personagem, prompt dela) e um
|
|
425
|
+
// motor de fora renderizou entra como NATIVA. Duas portas no servidor, o mesmo
|
|
426
|
+
// contrato de peça:
|
|
427
|
+
//
|
|
428
|
+
// 1. A do dono (`externalIngest:mcpIngestExternal`): o SERVIDOR baixa a URL,
|
|
429
|
+
// só de host liberado, e fita grande que já mora no CDN da casa entra sem
|
|
430
|
+
// trafegar byte. Continua como sempre foi.
|
|
431
|
+
// 2. A ponte (`externalIngestNode:mcpIngestFromStorage`): qualquer conta.
|
|
432
|
+
// Os bytes são resolvidos AQUI, na máquina da pessoa (URL do render,
|
|
433
|
+
// arquivo local ou base64), sobem pro storage do Convex pela URL assinada
|
|
434
|
+
// e o servidor só move pro Bunny. Sem fetch de URL do membro no servidor,
|
|
435
|
+
// sem chave de provedor em lugar nenhum: quem gerou lá fora foi o harness
|
|
436
|
+
// da pessoa, com o crédito dela.
|
|
437
|
+
//
|
|
438
|
+
// Com só a URL em mãos, tenta a porta do dono primeiro (é a que aceita a fita
|
|
439
|
+
// grande do CDN); se o servidor recusar por cargo ou por host, cai pra ponte
|
|
440
|
+
// sem a pessoa precisar saber que existem duas.
|
|
306
441
|
if (args.action === "ingest") {
|
|
307
|
-
if (!args.sourceUrl) {
|
|
308
|
-
throw new Error("action=ingest exige sourceUrl: o link https da peça pronta no motor externo. Diferente do upload, aqui é o SERVIDOR que baixa, e só de host liberado.");
|
|
309
|
-
}
|
|
310
442
|
if (!args.externalEngine) {
|
|
311
|
-
throw new Error("action=ingest exige externalEngine: qual motor renderizou, no formato provedor-motor ('
|
|
443
|
+
throw new Error("action=ingest exige externalEngine: qual motor renderizou, no formato provedor-motor ('kie-kling-3.0', 'fal-krea-2', 'magnific-minimax-h3', 'sogni-minimax-h3'). É o que a ficha mostra em 'Motor' e o que explica depois por que a peça não tem custo em Sinapses. Marca sozinha ('kie', 'magnific') não vale.");
|
|
312
444
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
445
|
+
const temBytesLocais = Boolean(args.filePath || args.base64?.trim());
|
|
446
|
+
if (!args.sourceUrl && !temBytesLocais) {
|
|
447
|
+
throw new Error("action=ingest exige a peça: sourceUrl (o link https do render no motor de fora), filePath (arquivo local, só no MCP instalado) ou base64.");
|
|
448
|
+
}
|
|
449
|
+
const ficha = {
|
|
316
450
|
externalEngine: args.externalEngine,
|
|
317
451
|
prompt: args.prompt ?? args.note,
|
|
318
452
|
externalCost: args.externalCost,
|
|
@@ -323,10 +457,33 @@ export async function gallery(args) {
|
|
|
323
457
|
characterIds: normalizeCastIds(args.characterIds).length
|
|
324
458
|
? normalizeCastIds(args.characterIds)
|
|
325
459
|
: undefined,
|
|
326
|
-
}
|
|
460
|
+
};
|
|
461
|
+
if (args.sourceUrl && !temBytesLocais) {
|
|
462
|
+
try {
|
|
463
|
+
const result = await convexAction("externalIngest:mcpIngestExternal", {
|
|
464
|
+
sessionToken,
|
|
465
|
+
sourceUrl: args.sourceUrl,
|
|
466
|
+
...ficha,
|
|
467
|
+
});
|
|
468
|
+
return { ...result, aviso: AVISO_INGEST };
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
// Só as duas recusas que a ponte resolve caem pra ela. O resto (motor
|
|
472
|
+
// sem nome, personagem errado, tipo recusado) é erro de verdade e sobe.
|
|
473
|
+
const msg = describeConvexError(e);
|
|
474
|
+
if (!/restrita ao dono|Host não liberado/i.test(msg))
|
|
475
|
+
throw e;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const result = await ingestPelaPonte(sessionToken, args, ficha);
|
|
327
479
|
return {
|
|
328
480
|
...result,
|
|
329
|
-
aviso:
|
|
481
|
+
aviso: AVISO_INGEST +
|
|
482
|
+
(result.adulta
|
|
483
|
+
? result.adultaPor === "declaracao"
|
|
484
|
+
? " Esta nasceu com o carimbo +18 que você declarou (unfiltered): publica e fica no seu perfil e no link, só não vai pra vitrine anônima. Você tira o carimbo na ficha se mudar de ideia."
|
|
485
|
+
: " Esta nasceu com o carimbo +18 por CLASSE (provedor de peso aberto ou motor sem freio): publica e fica no seu perfil e no link, só não vai pra vitrine anônima. Esse carimbo só a curadoria tira, igual ao motor sem freio da casa."
|
|
486
|
+
: ""),
|
|
330
487
|
};
|
|
331
488
|
}
|
|
332
489
|
// cast: quem está EM CENA numa peça que já existe.
|
package/dist/tools/image.js
CHANGED
|
@@ -73,6 +73,39 @@ const MODELS = [
|
|
|
73
73
|
// assim, listados em action=models e recusados no generate). Hoje vazio: todo
|
|
74
74
|
// motor ativo de imagem entra.
|
|
75
75
|
export const IMAGE_FORA_DE_PROPOSITO = {};
|
|
76
|
+
/**
|
|
77
|
+
* Motores que NASCEM em 2K quando quem chama não diz o tamanho. Padrão do MCP
|
|
78
|
+
* desde 20/09/2026, decisão do dono depois do take de aferição.
|
|
79
|
+
*
|
|
80
|
+
* Por que só estes dois grupos: na família Krea 2 o 2K é nativo do endpoint
|
|
81
|
+
* (2720x1536 em 16:9, o dobro do lado do 1K) e a peça de teste segurou fio de
|
|
82
|
+
* cabelo, poro e trama de tricô sem virar cera. No Gemini o 2K não custa NADA a
|
|
83
|
+
* mais pro provider (mesma conta de tokens de saída do 1K; o salto de custo só
|
|
84
|
+
* aparece no 4K), então ali é detalhe de graça.
|
|
85
|
+
*
|
|
86
|
+
* Quem fica de fora e por quê, pra ninguém tentar de novo: no gpt-image-2 e nos
|
|
87
|
+
* 2.5 o size é vestigial, quem decide o tamanho é o aspect ratio
|
|
88
|
+
* (mapAspectRatioToImageSize no backend, teto de 1536x1024), então não existe 2K
|
|
89
|
+
* pra pedir ali. O Seedream já entrega 2K nativo sem escada. Grok e MAI aceitam,
|
|
90
|
+
* mas ficaram de fora por decisão: neles o 2K custa mais pro provider.
|
|
91
|
+
*
|
|
92
|
+
* É padrão do MCP, não da casa: o gerador web continua abrindo em 1K, porque lá
|
|
93
|
+
* quem escolhe vê o preço mudar na tela. O size explícito manda em tudo,
|
|
94
|
+
* inclusive pra baixo (size="1K" volta ao barato).
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_2K_MODELS = new Set([
|
|
97
|
+
"wavespeed-krea2",
|
|
98
|
+
"wavespeed-krea2-realism",
|
|
99
|
+
"wavespeed-krea2-base",
|
|
100
|
+
"wavespeed-krea2-transparencia",
|
|
101
|
+
"wavespeed-krea2-broken-tears",
|
|
102
|
+
"nano-banana-max",
|
|
103
|
+
"nano-banana-2",
|
|
104
|
+
]);
|
|
105
|
+
/** Tamanho padrão do motor, quando quem chama não passou o size. */
|
|
106
|
+
function defaultSizeFor(model) {
|
|
107
|
+
return model && DEFAULT_2K_MODELS.has(model) ? "2K" : undefined;
|
|
108
|
+
}
|
|
76
109
|
export const imageSchema = z.object({
|
|
77
110
|
action: z.enum(["generate", "request_generation", "compose", "models"]),
|
|
78
111
|
prompt: z
|
|
@@ -90,7 +123,7 @@ export const imageSchema = z.object({
|
|
|
90
123
|
size: z
|
|
91
124
|
.enum(["1K", "2K", "4K"])
|
|
92
125
|
.optional()
|
|
93
|
-
.describe("
|
|
126
|
+
.describe("PADRÃO DO MCP: quem NÃO passa size nasce em 2K no Gemini (nano-banana-max, nano-banana-2) e na família Krea 2 da WaveSpeed, e em 1K no resto. Nesses dois grupos o 2K soma o adder ao preço (+100 hoje), e é decisão da casa pagar por ele: no Gemini o 2K não custa nada a mais pro provider, e na Krea 2 ele dobra o lado da peça (2720x1536 em 16:9) sem perder pele. Passe size='1K' explícito quando o barato importar mais que o detalhe. Resolução acima de 1K só existe nos modelos com hasResolutionAdder (o Gemini vai até 4K; grok-2-image, grok-2-image-quality e a família Krea 2 param em 2K, e pedir 4K neles entrega 2K e cobra o adder de 2K). Nos demais modelos o size é ignorado (fica em 1K): no gpt-image-2 e nos 2.5, por exemplo, quem decide o tamanho é o aspect ratio. O valor VIVO do adder por modelo sai em action=models (campo resolutionAdders, já clampado ao teto do motor): não escreva o número de cabeça, ele muda quando o provider reajusta."),
|
|
94
127
|
styleId: z
|
|
95
128
|
.string()
|
|
96
129
|
.optional()
|
|
@@ -112,6 +145,10 @@ export const imageSchema = z.object({
|
|
|
112
145
|
.enum(["persona", "logo", "none"])
|
|
113
146
|
.optional()
|
|
114
147
|
.describe("Marca na imagem do brand: 'persona' (personagem do brand via character sheets, ex: Helen), 'logo' (carimba a logo no canto), 'none' (só o estilo). Default 'none'. Só aplica se brandSlug setado e o brand oferecer a marca."),
|
|
148
|
+
skills: z
|
|
149
|
+
.array(z.string())
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("COMO A PEÇA FOI FEITA: os slugs das skills que dirigiram esta geração (ex: ['camarim'], ['dop','escala-gigantismo']). Metadado puro: não muda preço, motor nem resultado, e serve pra ACHAR depois tudo que saiu de uma skill. Passe sempre que a geração estiver saindo de uma skill da casa, na ordem em que elas entraram (quem escreveu o prompt primeiro). Fica no registro da peça e volta em sapiens_gallery."),
|
|
115
152
|
templateSlug: z
|
|
116
153
|
.string()
|
|
117
154
|
.optional()
|
|
@@ -259,16 +296,18 @@ export async function image(args) {
|
|
|
259
296
|
// exigem imageId pré-existente). Pra modelos de imagem (nano-banana-*,
|
|
260
297
|
// gpt-image-2-*), prefira action="generate" que já faz tudo num call.
|
|
261
298
|
if (args.action === "request_generation") {
|
|
299
|
+
const reqModel = args.model ?? "nano-banana-2";
|
|
262
300
|
return await import("../convexClient.js").then(({ convexMutation }) => convexMutation("mcpExtras:mcpRequestGeneration", {
|
|
263
301
|
sessionToken,
|
|
264
|
-
model:
|
|
302
|
+
model: reqModel,
|
|
265
303
|
styleId: args.styleId,
|
|
266
304
|
prompt: args.prompt,
|
|
267
305
|
negativePrompt: args.negativePrompt,
|
|
268
306
|
aspectRatio: args.aspectRatio ?? "16:9",
|
|
269
|
-
size: args.size ?? "1K",
|
|
307
|
+
size: args.size ?? defaultSizeFor(reqModel) ?? "1K",
|
|
270
308
|
brandSlug: args.brandSlug,
|
|
271
309
|
brandMark: args.brandMark,
|
|
310
|
+
skills: args.skills,
|
|
272
311
|
}));
|
|
273
312
|
}
|
|
274
313
|
// Edit/variation usam desktopMcp.generateImageOnline (suporta sourceImageId
|
|
@@ -289,14 +328,19 @@ export async function image(args) {
|
|
|
289
328
|
// (estilo + persona via refs server-side + logo) aplica dentro do
|
|
290
329
|
// generateImageAction, igual ao gerador web. Substitui o antigo branch
|
|
291
330
|
// referenceImageUrls→pipelineMcpImage (que era admin-only).
|
|
331
|
+
// Com templateSlug e sem model, fica undefined DE PROPÓSITO: quem manda no
|
|
332
|
+
// motor (e no tamanho) é o default do template.
|
|
333
|
+
const genModel = args.model ?? (args.templateSlug ? undefined : "nano-banana-2");
|
|
292
334
|
const result = await convexAction("desktopMcp:generateImageOnline", {
|
|
293
335
|
sessionToken,
|
|
294
336
|
prompt: args.prompt,
|
|
295
337
|
aspectRatio: args.aspectRatio,
|
|
296
|
-
size
|
|
338
|
+
// Sem size explícito, o motor decide (ver DEFAULT_2K_MODELS). Quando nem ele
|
|
339
|
+
// manda, o campo vai undefined e o backend fica no 1K de sempre.
|
|
340
|
+
size: args.size ?? defaultSizeFor(genModel),
|
|
297
341
|
// Com templateSlug, deixa o model em branco pro default do template valer
|
|
298
342
|
// (sem template, mantém o default nano-banana-2 da tool).
|
|
299
|
-
model:
|
|
343
|
+
model: genModel,
|
|
300
344
|
negativePrompt: args.negativePrompt,
|
|
301
345
|
mode: args.mode ?? "create",
|
|
302
346
|
sourceImageId: args.sourceImageId,
|
|
@@ -304,6 +348,7 @@ export async function image(args) {
|
|
|
304
348
|
referenceImageUrls: args.referenceImageUrls,
|
|
305
349
|
brandSlug: args.brandSlug,
|
|
306
350
|
brandMark: args.brandMark,
|
|
351
|
+
skills: args.skills,
|
|
307
352
|
templateSlug: args.templateSlug,
|
|
308
353
|
registro: args.registro,
|
|
309
354
|
influencerId: args.influencerId,
|
package/dist/tools/meta.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, } from "../convexClient.js";
|
|
2
|
+
import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, isRemoteContext, } from "../convexClient.js";
|
|
3
3
|
import { getMcpVersion } from "../version.js";
|
|
4
4
|
import { setTierFromIsAdmin } from "../tier.js";
|
|
5
5
|
/**
|
|
@@ -50,6 +50,7 @@ export const metaSchema = z.object({
|
|
|
50
50
|
"app_url",
|
|
51
51
|
"subscription",
|
|
52
52
|
"version",
|
|
53
|
+
"pontes",
|
|
53
54
|
]),
|
|
54
55
|
code: z
|
|
55
56
|
.string()
|
|
@@ -135,6 +136,77 @@ const FORMAT_GUIDE = {
|
|
|
135
136
|
// porque o MCP é pacote separado e não importa o examples.ts do app Next).
|
|
136
137
|
// NÃO menciona slash command (/sapiens:login etc.): isso só existe no Claude
|
|
137
138
|
// Code; Helen/Gemini/Cursor falam só MCP em linguagem natural.
|
|
139
|
+
/**
|
|
140
|
+
* AS PONTES: onde a pessoa pode gerar com crédito PRÓPRIO, com a ficha do
|
|
141
|
+
* personagem daqui, e trazer pro acervo por sapiens_gallery action=ingest.
|
|
142
|
+
*
|
|
143
|
+
* A chave do provedor NUNCA passa pelo Sapiens. O que este catálogo faz é dizer
|
|
144
|
+
* ao harness da pessoa o NOME da variável onde a chave costuma morar, pra ele
|
|
145
|
+
* descobrir sozinho o que ela já tem (`action=pontes` detecta por nome, no
|
|
146
|
+
* stdio, sem ler o valor) e o que cada balcão roda. O passo a passo vive na
|
|
147
|
+
* skill 'pontes'; aqui é só o índice.
|
|
148
|
+
*
|
|
149
|
+
* Só entra balcão que a casa mediu (motor que rodou de verdade por lá). Preço
|
|
150
|
+
* não entra: muda toda semana e a skill diz onde conferir.
|
|
151
|
+
*/
|
|
152
|
+
const PONTES = [
|
|
153
|
+
{
|
|
154
|
+
key: "kie",
|
|
155
|
+
nome: "Kie",
|
|
156
|
+
envVars: ["KIE_API_KEY"],
|
|
157
|
+
roda: "Kling 3.0, Kling Motion, MiniMax/Hailuo, WAN, Seedance; imagem também",
|
|
158
|
+
nota: "Um endpoint pra tudo (createTask + recordInfo). A URL do render expira em 24h: ingira no mesmo turno.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
key: "fal",
|
|
162
|
+
nome: "fal.ai",
|
|
163
|
+
envVars: ["FAL_KEY"],
|
|
164
|
+
roda: "Krea 2, Kling, WAN, Seedance, Flux; Krea 2 sem freio existe lá",
|
|
165
|
+
nota: "fal.run síncrono ou queue.fal.run. Slug 'fal-' na casa é classe +18 (sai da vitrine anônima, fica no perfil).",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: "wavespeed",
|
|
169
|
+
nome: "WaveSpeed",
|
|
170
|
+
envVars: ["WAVESPEED_API_KEY"],
|
|
171
|
+
roda: "Flux.2 Klein, Krea 2 Livre, WAN 2.2, Kling, Shot Mimic",
|
|
172
|
+
nota: "É o balcão que a casa mais usa por trás do sapiens_video. Slug 'wavespeed-' é classe +18.",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "sogni",
|
|
176
|
+
nome: "Sogni",
|
|
177
|
+
envVars: ["SOGNI_API_KEY"],
|
|
178
|
+
roda: "MiniMax H3 inteira, WAN 2.2 (com animate), LTX 2.3 e 2.5, Krea 2 Turbo, Identity Edit",
|
|
179
|
+
nota: "Assinatura Unlimited cobre o catálogo aberto (custo marginal zero). Um socket por conta: um job por vez.",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: "magnific",
|
|
183
|
+
nome: "Magnific / Freepik",
|
|
184
|
+
envVars: [],
|
|
185
|
+
roda: "Seedance 2.0 e 2.5 (até 30s e 1080p), MiniMax H3, upscale, relight",
|
|
186
|
+
nota: "Sem chave: é o conector MCP da Magnific na sessão da pessoa. A URL assinada do render expira no mesmo dia.",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: "krea",
|
|
190
|
+
nome: "Krea API",
|
|
191
|
+
envVars: ["KREA_API_KEY"],
|
|
192
|
+
roda: "Krea 2 Turbo/Medium/Large, MiniMax H3 Max Turbo, Seedream, Veo, Seedance",
|
|
193
|
+
nota: "Filtra NSFW; job que falha não cobra. Saldo pré-pago separado do app.",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "replicate",
|
|
197
|
+
nome: "Replicate",
|
|
198
|
+
envVars: ["REPLICATE_API_TOKEN"],
|
|
199
|
+
roda: "catálogo aberto (Flux, WAN, LTX e o que a comunidade sobe)",
|
|
200
|
+
nota: "A casa não mediu motor por lá; o contrato de ingest é o mesmo.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
key: "bancada",
|
|
204
|
+
nome: "A própria máquina (ComfyUI, SD local)",
|
|
205
|
+
envVars: [],
|
|
206
|
+
roda: "o que a placa aguenta: LTX, Klein, Krea 2 com LoRA, H3 destilado",
|
|
207
|
+
nota: "Custo zero em dinheiro, pago em tempo. Entra por filePath, slug 'bancada-<motor>'.",
|
|
208
|
+
},
|
|
209
|
+
];
|
|
138
210
|
const FIRST_POWERS = [
|
|
139
211
|
{
|
|
140
212
|
icon: "📚",
|
|
@@ -319,7 +391,7 @@ export async function meta(args) {
|
|
|
319
391
|
balance,
|
|
320
392
|
...(lowBalance
|
|
321
393
|
? {
|
|
322
|
-
balanceWarning: "Saldo abaixo de 500 Sinapses: o que é grátis roda tranquilo, mas pra imagem/música/vídeo talvez precise recarregar.",
|
|
394
|
+
balanceWarning: "Saldo abaixo de 500 Sinapses: o que é grátis roda tranquilo, mas pra imagem/música/vídeo talvez precise recarregar. Outra saída é a ponte: se a pessoa tem crédito na Kie, na fal, na Magnific ou na Sogni, gera lá com a ficha do personagem daqui e traz a peça pro acervo por sapiens_gallery action=ingest (skill 'pontes'; sapiens_meta action=pontes diz o que ela já tem configurado).",
|
|
323
395
|
}
|
|
324
396
|
: {}),
|
|
325
397
|
firstPowers: FIRST_POWERS,
|
|
@@ -471,6 +543,38 @@ export async function meta(args) {
|
|
|
471
543
|
: "Nenhuma sessão local encontrada.",
|
|
472
544
|
};
|
|
473
545
|
}
|
|
546
|
+
// pontes: o que a pessoa já tem de crédito FORA da casa, e como cada balcão
|
|
547
|
+
// entra. Não exige login e não fala com o backend: é leitura do catálogo
|
|
548
|
+
// acima mais uma olhada nos NOMES das variáveis do ambiente do MCP instalado
|
|
549
|
+
// (o valor nunca é lido nem devolvido; no remoto o processo é o servidor do
|
|
550
|
+
// site, então lá não se olha nada e a resposta manda perguntar).
|
|
551
|
+
if (args.action === "pontes") {
|
|
552
|
+
const remoto = isRemoteContext();
|
|
553
|
+
const detectadas = remoto
|
|
554
|
+
? []
|
|
555
|
+
: PONTES.filter((p) => p.envVars.some((nome) => {
|
|
556
|
+
const valor = process.env[nome];
|
|
557
|
+
return typeof valor === "string" && valor.trim().length >= 8;
|
|
558
|
+
})).map((p) => p.key);
|
|
559
|
+
return {
|
|
560
|
+
regra: "A chave de provedor NUNCA passa pelo Sapiens: quem chama o motor é o seu harness, na sua máquina, com o seu crédito. A casa entra com a ficha do personagem (sapiens_character action=get: passportPrompt + imageUrls) e recebe a peça de volta por sapiens_gallery action=ingest, que grava motor, prompt verbatim, custo real e personagem. Passo a passo: sapiens_skill action=get name=pontes.",
|
|
561
|
+
detectadas,
|
|
562
|
+
detectadasNota: remoto
|
|
563
|
+
? "No MCP remoto não dá pra olhar o ambiente da pessoa: pergunte a ela onde tem crédito (Kie, fal, Magnific, Sogni, Krea...) ou olhe os conectores que já estão nesta conversa."
|
|
564
|
+
: detectadas.length
|
|
565
|
+
? `Achei chave configurada (pelo NOME da variável, sem ler o valor) pra: ${detectadas.join(", ")}. Proponha gerar por aí antes de gastar Sinapse.`
|
|
566
|
+
: "Nenhuma variável de provedor no ambiente deste MCP. Pergunte onde a pessoa tem crédito, ou olhe os conectores já ligados nesta conversa (Magnific, Krea, Sogni).",
|
|
567
|
+
pontes: PONTES.map((p) => ({
|
|
568
|
+
key: p.key,
|
|
569
|
+
nome: p.nome,
|
|
570
|
+
roda: p.roda,
|
|
571
|
+
envVars: p.envVars,
|
|
572
|
+
nota: p.nota,
|
|
573
|
+
ingestSlug: `${p.key}-<motor>`,
|
|
574
|
+
})),
|
|
575
|
+
voltaPraCasa: "sapiens_gallery action=ingest com externalEngine='<provedor>-<motor>', prompt VERBATIM, externalCost na moeda de lá, characterId e referenceImageIds. filePath (arquivo local, só no MCP instalado) ou sourceUrl (o render, que expira: ingira no mesmo turno). unfiltered=true quando o motor rodou sem filtro. Tetos: 12 MB imagem, 60 MB vídeo, 40 peças por dia.",
|
|
576
|
+
};
|
|
577
|
+
}
|
|
474
578
|
const sessionToken = getSessionToken();
|
|
475
579
|
if (args.action === "whoami") {
|
|
476
580
|
// mcpGetMySubscription usa requireMcpUser (qualquer logado), então whoami
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapiens-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.71.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",
|
package/dist/tools/referencia.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { convexQuery, convexMutation, getSessionToken } from "../convexClient.js";
|
|
3
|
-
/**
|
|
4
|
-
* sapiens_referencia — o acervo de publicidade da casa, pela porta do agente.
|
|
5
|
-
*
|
|
6
|
-
* Existe pra resolver um gargalo de curadoria, não pra expor mais um catálogo:
|
|
7
|
-
* achar um comercial bom e precisar de terminal pra guardar significa que o
|
|
8
|
-
* acervo só cresce quando o dono está na máquina. Aqui ele guarda de onde
|
|
9
|
-
* estiver, com o link e uma frase.
|
|
10
|
-
*
|
|
11
|
-
* O acervo tem duas trilhas e as duas ensinam coisas opostas: `referencia` é
|
|
12
|
-
* campanha premiada (o teto do ofício) e `replicavel` é anúncio veiculando
|
|
13
|
-
* agora (o degrau). O membro vê tudo em /dashboard/acervo?tab=referencias.
|
|
14
|
-
*
|
|
15
|
-
* ADMIN-ONLY de ponta a ponta: o Convex recusa quem não é dono em toda action,
|
|
16
|
-
* e a tool some do tools/list de membro. A curadoria é o valor deste acervo, e
|
|
17
|
-
* acervo que qualquer agente alimenta vira lista.
|
|
18
|
-
*
|
|
19
|
-
* O que esta tool NÃO faz: guardar peça de biblioteca de anúncio (Meta,
|
|
20
|
-
* TikTok). Aquelas servem por URL assinada que expira, então o vídeo precisa
|
|
21
|
-
* ser espelhado no CDN da casa antes de virar ficha, e isso é trabalho do
|
|
22
|
-
* script `apps/sapiens/scripts/ingest-ad-references.mjs`, com ffmpeg e
|
|
23
|
-
* browser. Link do YouTube e do Vimeo, que têm embed oficial, entram aqui.
|
|
24
|
-
*/
|
|
25
|
-
export const referenciaSchema = z.object({
|
|
26
|
-
action: z.enum(["list", "save", "note"]),
|
|
27
|
-
url: z
|
|
28
|
-
.string()
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("Pra save: link do YouTube ou do Vimeo. Só essas duas, porque têm embed " +
|
|
31
|
-
"oficial que não expira. Peça de Meta Ad Library ou TikTok entra pelo " +
|
|
32
|
-
"script de ingestão da casa, não por aqui."),
|
|
33
|
-
titulo: z.string().optional().describe("Pra save: o nome da campanha."),
|
|
34
|
-
marca: z.string().optional().describe("Pra save: quem anuncia. Resolve o setor sozinho quando o setor não vem."),
|
|
35
|
-
trilha: z
|
|
36
|
-
.enum(["referencia", "replicavel"])
|
|
37
|
-
.optional()
|
|
38
|
-
.describe("Pra save: 'referencia' (campanha premiada, o teto do ofício) ou " +
|
|
39
|
-
"'replicavel' (anúncio veiculando agora). Default: referencia."),
|
|
40
|
-
setor: z
|
|
41
|
-
.string()
|
|
42
|
-
.optional()
|
|
43
|
-
.describe("Pra save/list: o setor do catálogo (Alimentação e bebida, IA e software, " +
|
|
44
|
-
"Moda e beleza...). Em save, sem isto o servidor deduz pela marca."),
|
|
45
|
-
ano: z.number().optional().describe("Pra save: ano da campanha."),
|
|
46
|
-
leitura: z
|
|
47
|
-
.string()
|
|
48
|
-
.optional()
|
|
49
|
-
.describe("Pra save/note: a leitura editorial. O que a peça faz que dá pra roubar, " +
|
|
50
|
-
"onde está o gancho, por que ela segura. É o que o acervo tem e uma " +
|
|
51
|
-
"busca no YouTube não tem."),
|
|
52
|
-
fonte: z
|
|
53
|
-
.enum(["youtube", "vimeo", "tiktok-library", "tiktok-creative-center", "meta-ad-library"])
|
|
54
|
-
.optional()
|
|
55
|
-
.describe("Pra note: a fonte da peça já guardada (vem do list)."),
|
|
56
|
-
externalId: z.string().optional().describe("Pra note: o id da peça na fonte (vem do list)."),
|
|
57
|
-
limite: z.number().optional().describe("Pra list: teto de fichas (default 60, máx 200)."),
|
|
58
|
-
});
|
|
59
|
-
export async function referencia(args) {
|
|
60
|
-
const sessionToken = getSessionToken();
|
|
61
|
-
if (args.action === "list") {
|
|
62
|
-
const fichas = await convexQuery("adReferences:mcpListar", {
|
|
63
|
-
sessionToken,
|
|
64
|
-
...(args.trilha ? { trilha: args.trilha } : {}),
|
|
65
|
-
...(args.setor ? { setor: args.setor } : {}),
|
|
66
|
-
...(args.limite ? { limite: args.limite } : {}),
|
|
67
|
-
});
|
|
68
|
-
const n = Array.isArray(fichas) ? fichas.length : 0;
|
|
69
|
-
const semLeitura = Array.isArray(fichas) ? fichas.filter((f) => !f.temLeitura).length : 0;
|
|
70
|
-
return {
|
|
71
|
-
count: n,
|
|
72
|
-
fichas,
|
|
73
|
-
note: n === 0
|
|
74
|
-
? "Nada nesse recorte ainda."
|
|
75
|
-
: `${semLeitura} sem leitura escrita. A leitura é o que o acervo tem e uma busca no YouTube não tem: action=note fonte=<f> externalId=<id> leitura="...".`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
if (args.action === "save") {
|
|
79
|
-
if (!args.url)
|
|
80
|
-
throw new Error("sapiens_referencia save: exige url.");
|
|
81
|
-
if (!args.titulo)
|
|
82
|
-
throw new Error("sapiens_referencia save: exige titulo.");
|
|
83
|
-
const r = await convexMutation("adReferences:mcpGuardar", {
|
|
84
|
-
sessionToken,
|
|
85
|
-
url: args.url,
|
|
86
|
-
titulo: args.titulo,
|
|
87
|
-
...(args.marca ? { marca: args.marca } : {}),
|
|
88
|
-
...(args.trilha ? { trilha: args.trilha } : {}),
|
|
89
|
-
...(args.setor ? { setor: args.setor } : {}),
|
|
90
|
-
...(args.ano ? { ano: args.ano } : {}),
|
|
91
|
-
...(args.leitura ? { leitura: args.leitura } : {}),
|
|
92
|
-
});
|
|
93
|
-
return {
|
|
94
|
-
...r,
|
|
95
|
-
note: r?.novo
|
|
96
|
-
? "Guardada. Aparece pro membro em /dashboard/acervo?tab=referencias."
|
|
97
|
-
: "Já estava no acervo; a ficha foi atualizada em vez de duplicar.",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (args.action === "note") {
|
|
101
|
-
if (!args.fonte || !args.externalId) {
|
|
102
|
-
throw new Error("sapiens_referencia note: exige fonte e externalId (vêm do list).");
|
|
103
|
-
}
|
|
104
|
-
if (!args.leitura)
|
|
105
|
-
throw new Error("sapiens_referencia note: exige leitura.");
|
|
106
|
-
return await convexMutation("adReferences:mcpAnotar", {
|
|
107
|
-
sessionToken,
|
|
108
|
-
fonte: args.fonte,
|
|
109
|
-
externalId: args.externalId,
|
|
110
|
-
leitura: args.leitura,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
throw new Error(`sapiens_referencia: action desconhecida "${args.action}".`);
|
|
114
|
-
}
|
package/dist/tools/shorts.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
-
/**
|
|
4
|
-
* Sapiens Shorts — vídeo vertical 9:16 via VEO (v1.5).
|
|
5
|
-
*
|
|
6
|
-
* Sub-action:
|
|
7
|
-
* - render: dispara render com brief structured + imageId persona pré-existente.
|
|
8
|
-
*
|
|
9
|
-
* Pré-requisitos pro caller:
|
|
10
|
-
* 1. Gerar imagem persona via sapiens_image (ou escolher do gallery via sapiens_gallery)
|
|
11
|
-
* 2. Ter créditos suficientes (custo varia por modelo)
|
|
12
|
-
* 3. Brief montado segundo schema (product/hook/shots/vibe)
|
|
13
|
-
*
|
|
14
|
-
* Estilos:
|
|
15
|
-
* - ugc: real-life talking-head (mais comum)
|
|
16
|
-
* - unboxing: close em produto físico
|
|
17
|
-
* - app-demo: persona + tela
|
|
18
|
-
* - reflexao: atmosfera mais lenta, contemplativa
|
|
19
|
-
*
|
|
20
|
-
* Retorna `{ success:true, imageId, status:'rendering', url:null }` (ASSÍNCRONO):
|
|
21
|
-
* o render VEO roda fora da chamada. Acompanhe com sapiens_video action=status
|
|
22
|
-
* imageId=<id> até status='completed' (traz a url VEO, expiração curta, baixe
|
|
23
|
-
* logo) ou 'error'/'blocked'.
|
|
24
|
-
*/
|
|
25
|
-
export const shortsSchema = z.object({
|
|
26
|
-
action: z.enum(["render"]),
|
|
27
|
-
imageId: z.string().describe("generatedImages:_id da persona base. Use sapiens_gallery action=list pra descobrir."),
|
|
28
|
-
styleId: z.enum(["ugc", "unboxing", "app-demo", "reflexao"]),
|
|
29
|
-
brief: z.object({
|
|
30
|
-
product: z.object({
|
|
31
|
-
name: z.string(),
|
|
32
|
-
type: z.enum(["app", "physical", "saas"]),
|
|
33
|
-
uvps: z.array(z.string()).describe("Unique Value Propositions, 2-4 bullets curtos"),
|
|
34
|
-
persona: z.string().describe("Descrição da persona-protagonista (idade/contexto/estilo)"),
|
|
35
|
-
}),
|
|
36
|
-
hook: z.object({
|
|
37
|
-
line: z.string().describe("Frase de abertura punchy, 5-10 palavras"),
|
|
38
|
-
emotion: z.string().describe("Emoção alvo (ex: 'curiosity', 'frustration', 'awe')"),
|
|
39
|
-
}),
|
|
40
|
-
shots: z
|
|
41
|
-
.array(z.object({
|
|
42
|
-
sec: z.number().describe("Duração em segundos (cap 8 por shot)"),
|
|
43
|
-
role: z.enum(["hook", "problem", "solution", "cta"]),
|
|
44
|
-
camera: z.enum(["close-up", "medium", "over-shoulder", "product-pov"]),
|
|
45
|
-
action: z.string().describe("Ação visual descrita em 1 frase"),
|
|
46
|
-
emotion: z.string(),
|
|
47
|
-
voiceLine: z.string().optional().describe("Linha falada na cena (PT-BR)"),
|
|
48
|
-
propVisible: z.string().optional(),
|
|
49
|
-
}))
|
|
50
|
-
.min(2)
|
|
51
|
-
.max(6)
|
|
52
|
-
.describe("2-6 shots. Total 15-60s. Geralmente: hook → problem → solution → cta."),
|
|
53
|
-
vibe: z.object({
|
|
54
|
-
energy: z.enum(["calm", "high"]).describe("Energia geral do edit"),
|
|
55
|
-
language: z.enum(["pt-BR", "en"]).optional().describe("Default 'pt-BR'"),
|
|
56
|
-
}),
|
|
57
|
-
}),
|
|
58
|
-
references: z
|
|
59
|
-
.array(z.object({
|
|
60
|
-
mimeType: z.string(),
|
|
61
|
-
data: z.string(),
|
|
62
|
-
role: z.string().optional(),
|
|
63
|
-
}))
|
|
64
|
-
.optional()
|
|
65
|
-
.describe("References opcionais (start/end frames) em base64. Se omitido, persona é o start frame default."),
|
|
66
|
-
});
|
|
67
|
-
export async function shorts(args) {
|
|
68
|
-
if (args.action === "render") {
|
|
69
|
-
const sessionToken = getSessionToken();
|
|
70
|
-
if (args.brief.shots.length === 0) {
|
|
71
|
-
throw new Error("brief.shots vazio — passe 2-6 shots descrevendo a sequência.");
|
|
72
|
-
}
|
|
73
|
-
return await convexAction("mcpExtrasActions:mcpShortsRender", {
|
|
74
|
-
sessionToken,
|
|
75
|
-
imageId: args.imageId,
|
|
76
|
-
styleId: args.styleId,
|
|
77
|
-
brief: args.brief,
|
|
78
|
-
references: args.references,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|