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