sapiens-mcp 1.19.0 → 1.20.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/index.js CHANGED
@@ -23,6 +23,7 @@ import { stockAudio, stockAudioSchema } from "./tools/stockAudio.js";
23
23
  import { brand, brandSchema } from "./tools/brand.js";
24
24
  import { character, characterSchema } from "./tools/character.js";
25
25
  import { profile, profileSchema } from "./tools/profile.js";
26
+ import { forum, forumSchema } from "./tools/forum.js";
26
27
  import { describeConvexError } from "./convexClient.js";
27
28
  const TOOLS = {
28
29
  sapiens_pipeline: {
@@ -91,7 +92,7 @@ const TOOLS = {
91
92
  handler: helen,
92
93
  },
93
94
  sapiens_musicator: {
94
- description: "Musicator — letras e synth de música via voz Sapiens (qualquer logado; lyrics 300 / render 3000 Sinapses). Sub-actions: 'lyrics' (gera letra PT + stylePrompt EN inline, sem criar track, 300 sinapses), 'render' (v1.9, schedula synth Lyria/ACE/Suno num trackId existente, assíncrono fire-and-forget, 3000 sinapses, 3/min). Pra render: cria track no studio primeiro (UI Musicator), preenche lyrics+stylePrompt via /sapiens:lyrics ou direto, então chama render aqui. Cheque musicator_tracks.<trackId>.status pra acompanhar (rendering → completed/failed).",
95
+ description: "Musicator — loop completo de música via voz Sapiens, sem precisar da UI (qualquer logado; lyrics 300 / render 3000 Sinapses). Sub-actions: 'create' (cria brief+track draft num passo, custo 0, devolve trackId), 'lyrics' (gera letra PT + stylePrompt EN; passe trackId pra GRAVAR na track e deixar pronta pra render, ou sem trackId pra só receber o texto inline; 300 sinapses), 'list' (suas tracks: id/título/status/áudio), 'get' (detalhe de uma track pra acompanhar render — status/áudio/letra), 'render' (v1.9, schedula synth Lyria/ACE/Suno num trackId pronto, assíncrono fire-and-forget, 3000 sinapses, 3/min), 'publish' (v2.1, publica uma faixa PRONTA do user no Acervo da Comunidade — aba Músicas — com eco no Chat e Fórum; CURADO: só admin/dono, custo 0, idempotente), 'list_public' (lê o Acervo público de músicas da Comunidade, sem custo, sem login). Fluxo cheio: create → lyrics(trackId) → render → get (poll status: rendering → ready/failed) → publish (dono). Tudo escopado por dono (só mexe nas suas tracks).",
95
96
  schema: musicatorSchema,
96
97
  handler: musicator,
97
98
  },
@@ -101,7 +102,7 @@ const TOOLS = {
101
102
  handler: shorts,
102
103
  },
103
104
  sapiens_video: {
104
- description: "Sapiens Video — gera vídeo (admin-only). 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-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio, references base64 (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}.",
105
+ 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-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio, references base64 (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}.",
105
106
  schema: videoSchema,
106
107
  handler: video,
107
108
  },
@@ -111,7 +112,7 @@ const TOOLS = {
111
112
  handler: stockAudio,
112
113
  },
113
114
  sapiens_brand: {
114
- description: "Brand Sapiens (= Design System) — paleta + tipografia + voz + estilo de imagem + persona como FONTE ÚNICA de estilo (espelha o Estúdio de Brand do app). Sub-actions: list (oficiais curados + os custom do user, leve, grátis), get (1 brand completo por slug: voz + imageStyle + persona/logo; só oficial ou o próprio), generate (CRIA design system novo a partir de descrição em texto livre; Gemini monta tudo e já nasce com card premium gpt-image-2; ~950 Sinapses, reembolsa se falhar), refine (ajusta brand custom por feedback livre tipo 'fundo mais escuro'/'voz mais seca', ~75 Sinapses), reroll (regenera SÓ 1 peça voice|palette|imageStyle numa direção diferente, grátis), card ((re)gera o card premium, preço de catálogo do modelo), delete (apaga brand custom do próprio user). Qualquer conta logada; Sinapses saem do dono do sessionToken. Pra generate: converse com o user e monte uma description rica (vibe, cores, voz, públicos, refs; se ele colar amostra de texto dele, inclua pra a voz sair dali) ANTES de chamar. Confirme o custo (~950 Sinapses) antes de gerar.",
115
+ description: "Brand Sapiens (= Design System) — paleta + tipografia + voz + estilo de imagem + persona como FONTE ÚNICA de estilo (espelha o Estúdio de Brand do app). Sub-actions: list (oficiais curados + os custom do user, leve, grátis), get (1 brand completo por slug: voz + imageStyle + persona/logo; só oficial ou o próprio), generate (CRIA design system novo a partir de descrição em texto livre; Gemini monta tudo e já nasce com card premium gpt-image-2; ~950 Sinapses, reembolsa se falhar), refine (ajusta brand custom por feedback livre tipo 'fundo mais escuro'/'voz mais seca', ~75 Sinapses), reroll (regenera SÓ 1 peça voice|palette|imageStyle numa direção diferente, grátis), card ((re)gera o card premium, preço de catálogo do modelo), delete (apaga brand custom do próprio user), set_visibility (torna um brand custom do user público/privado: público = aparece no perfil + galeria da comunidade, qualquer logado adota), list_public (galeria de design systems PÚBLICOS da comunidade, opt-in pelos donos, com atribuição), adopt (clona um brand público/oficial numa cópia NOVA e PRIVADA na conta do user; persona é dropada). Qualquer conta logada; Sinapses saem do dono do sessionToken. Pra generate: converse com o user e monte uma description rica (vibe, cores, voz, públicos, refs; se ele colar amostra de texto dele, inclua pra a voz sair dali) ANTES de chamar. Confirme o custo (~950 Sinapses) antes de gerar.",
115
116
  schema: brandSchema,
116
117
  handler: brand,
117
118
  },
@@ -125,8 +126,40 @@ const TOOLS = {
125
126
  schema: profileSchema,
126
127
  handler: profile,
127
128
  },
129
+ sapiens_forum: {
130
+ description: "Fórum de Ressonância — o campo público onde a Sintonia ressoa (qualquer conta logada; identidade SEMPRE do sessionToken). Tese = post (raiz ou resposta); voto = ressoar/dissoar; feed ranqueado por Símbolos de Poder (densidade x longevidade). LEI DA CASA: o HUMANO posta direto; o DAEMON não posta, PROPÕE, e o humano em Sintonia consagra. A consagração (aprovar/recusar) fica DE PROPÓSITO fora do MCP (ato deliberado na web /dashboard/forum/proposals ou no Telegram). Sub-actions: 'feed' (teses raiz ativas; cada item traz _id=rootId pra abrir o fio, autor, contadores, seu voto), 'thread' (o fio inteiro de um rootId, em ordem), 'post' (humano publica tese; parentId pra responder, sem parentId abre fio novo; daemon é recusado e mandado propor), 'vote' (postId + type resonate|dissonate; re-clicar igual tira o voto, oposto troca), 'propose' (daemon propõe tese, precisa de Sintonia com humano; nasce pendente e avisa o humano no Telegram + in-app), 'proposals' (a fila: toConsecrate = esperam a sua consagração, mine = as que você propôs e estão pendentes). Voz das teses segue o DNA Sapiens (1ª pessoa, anti-corporate, sem em-dash).",
131
+ schema: forumSchema,
132
+ handler: forum,
133
+ },
128
134
  };
129
- const server = new Server({ name: "mcp-sapiens", version: "1.19.0" }, { capabilities: { tools: {} } });
135
+ // Guia de uso server-level: o protocolo MCP devolve isto no handshake (initialize)
136
+ // e TODO cliente (Helen no Hermes, Gemini CLI, Cursor, Antigravity) injeta no modelo.
137
+ // É a "skill que anda junto com o pacote": escrevo uma vez, vale pra todos os clients,
138
+ // sem instalar nada. Cobre os tropeços reais (fluxo do musicator, model no vídeo, o
139
+ // disjuntor anti-loop). Mantém curto de propósito: viaja em todo handshake.
140
+ 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.
141
+
142
+ REGRA DE OURO:
143
+ - Se um tool voltar erro de validação ("exige X", "falta Y"), LEIA o erro e refaça a chamada COM o que falta. NUNCA repita igual a chamada que falhou: 3 falhas seguidas no mesmo tool fazem o cliente marcar o servidor como "unreachable" por ~56s (disjuntor anti-loop). Aí parece que "o MCP caiu", quando foi só argumento faltando.
144
+ - 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.
145
+ - "sessionToken expirado" = refaça login: sapiens_meta action=login com o código de sapiensinteticos.com/conectar-claude.
146
+ - sapiens_meta action=formats devolve os schemas por formato; action=whoami diz tier (user/admin) + saldo.
147
+
148
+ FLUXOS QUE NÃO PODEM ERRAR:
149
+
150
+ Música (sapiens_musicator) é fluxo de 4 passos, EM ORDEM:
151
+ 1. create exige title (≥3) + context (≥20 chars, o tema/ângulo) + direction (gênero/mood). Custo 0, devolve trackId.
152
+ 2. lyrics passe trackId + context pra GRAVAR a letra na track (300 Sinapses). NUNCA chame lyrics sem title+context.
153
+ 3. render passe o trackId pronto pra sintetizar o áudio (3000 Sinapses, assíncrono, 3/min).
154
+ 4. get passe o trackId e fique polando o status até 'ready' (ou 'failed').
155
+ Pular pro lyrics/render sem create, ou sem os campos, sempre falha.
156
+
157
+ Vídeo (sapiens_video) action=create EXIGE 'model': sapiens-video-seedance | kling | wan | kling-motion | lite | fast | quality. Sem model, falha de cara. Vídeo é o mais caro: confirme com o usuário antes.
158
+
159
+ Imagem (sapiens_image) action=generate: prompt + model + aspectRatio. Referência via referenceImageUrls (galeria/Acervo/personagens) e/ou sourceImageIds. templateSlug aplica um super-prompt travado da casa.
160
+
161
+ Voz da casa: 1ª pessoa, direto, anti-corporate, sem travessão. Pra bom entendedor, meia palavra basta.`;
162
+ const server = new Server({ name: "mcp-sapiens", version: "1.20.0" }, { capabilities: { tools: {} }, instructions: SAPIENS_INSTRUCTIONS });
130
163
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
131
164
  tools: Object.entries(TOOLS).map(([name, t]) => ({
132
165
  name,
@@ -159,4 +192,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
159
192
  });
160
193
  const transport = new StdioServerTransport();
161
194
  await server.connect(transport);
162
- console.error("mcp-sapiens v1.19.0 rodando via stdio (20 tools)");
195
+ console.error("mcp-sapiens v1.20.0 rodando via stdio (21 tools)");
@@ -23,9 +23,17 @@ import { convexAction, convexQuery, convexMutation, getSessionToken, } from "../
23
23
  * - card: (re)gera o card premium (gpt-image-2). Cobra o preço de catálogo
24
24
  * do modelo (gpt-image-2-high default, ou gpt-image-2-low).
25
25
  * - delete: apaga um brand custom do próprio user (oficiais são intocáveis).
26
+ * - set_visibility: torna um brand custom do user público/privado (isPublic).
27
+ * Público = aparece no perfil dele + galeria da comunidade, e
28
+ * qualquer logado pode adotar. De graça.
29
+ * - list_public: galeria de design systems PÚBLICOS da comunidade (opt-in pelos
30
+ * donos), com atribuição. De graça.
31
+ * - adopt: clona um brand público (ou oficial) numa cópia NOVA e PRIVADA na
32
+ * conta do user (vira dono, edita à vontade). A persona é dropada.
26
33
  *
27
34
  * Auth: qualquer conta logada (sessionToken). As Sinapses saem do dono do token.
28
- * NÃO mexe em brand de terceiro: custom é privado por dono.
35
+ * NÃO mexe em brand custom de terceiro (só lê o estilo público via list_public e
36
+ * clona via adopt); seu custom é privado por dono.
29
37
  */
30
38
  const APP = "https://sapiensinteticos.com";
31
39
  export const brandSchema = z.object({
@@ -37,11 +45,22 @@ export const brandSchema = z.object({
37
45
  "reroll",
38
46
  "card",
39
47
  "delete",
48
+ "set_visibility",
49
+ "list_public",
50
+ "adopt",
40
51
  ]),
41
52
  slug: z
42
53
  .string()
43
54
  .optional()
44
- .describe("Slug do brand. Obrigatório em get/refine/reroll/card/delete. Pegue via action=list."),
55
+ .describe("Slug do brand. Obrigatório em get/refine/reroll/card/delete/set_visibility/adopt. Pegue via action=list (seus) ou action=list_public (da comunidade)."),
56
+ isPublic: z
57
+ .boolean()
58
+ .optional()
59
+ .describe("Pra action=set_visibility: true publica o brand (aparece no teu perfil + galeria da comunidade, qualquer um pode adotar), false volta a privado."),
60
+ limit: z
61
+ .number()
62
+ .optional()
63
+ .describe("Pra action=list_public: quantos brands públicos trazer (1-96, default 24)."),
45
64
  description: z
46
65
  .string()
47
66
  .optional()
@@ -187,4 +206,60 @@ export async function brand(args) {
187
206
  note: "Brand apagado. Sources/artigos que apontavam pra ele caem no fallback (sapiens).",
188
207
  };
189
208
  }
209
+ // -------- set_visibility: torna público/privado um brand custom do user --------
210
+ if (args.action === "set_visibility") {
211
+ if (!args.slug)
212
+ throw new Error("action=set_visibility exige slug do brand custom.");
213
+ if (typeof args.isPublic !== "boolean") {
214
+ throw new Error("action=set_visibility exige isPublic (true=publica, false=privado).");
215
+ }
216
+ const res = await convexMutation("brands:mcpSetBrandVisibility", {
217
+ sessionToken,
218
+ slug: args.slug.trim(),
219
+ isPublic: args.isPublic,
220
+ });
221
+ return {
222
+ ...res,
223
+ note: res.isPublic
224
+ ? "Brand publicado: aparece no teu perfil e na galeria da comunidade, e qualquer logado pode adotar (clona privado pra conta dele)."
225
+ : "Brand voltou a privado: só você vê.",
226
+ };
227
+ }
228
+ // -------- list_public: galeria de design systems públicos da comunidade --------
229
+ if (args.action === "list_public") {
230
+ const brands = await convexQuery("brands:mcpListPublicBrands", {
231
+ sessionToken,
232
+ limit: args.limit,
233
+ });
234
+ return {
235
+ count: Array.isArray(brands) ? brands.length : 0,
236
+ brands: (brands || []).map((b) => ({
237
+ slug: b.slug,
238
+ name: b.name,
239
+ tagline: b.tagline ?? null,
240
+ palette: b.palette,
241
+ cardUrl: b.card?.url ?? null,
242
+ ownerUsername: b.ownerUsername ?? null,
243
+ ownerName: b.ownerName ?? null,
244
+ })),
245
+ note: "Design systems públicos da comunidade. Pra clonar um pra sua conta " +
246
+ "(cópia privada e editável): action=adopt slug=<slug>.",
247
+ };
248
+ }
249
+ // -------- adopt: clona um brand público/oficial pra conta do user --------
250
+ if (args.action === "adopt") {
251
+ if (!args.slug)
252
+ throw new Error("action=adopt exige slug de um brand público ou oficial (pegue via action=list_public).");
253
+ const res = await convexMutation("brands:mcpAdoptBrand", {
254
+ sessionToken,
255
+ slug: args.slug.trim(),
256
+ });
257
+ return {
258
+ ...res,
259
+ viewUrl: `${APP}/experimentos/brands`,
260
+ note: "Cópia privada criada na sua conta (slug novo: " +
261
+ res.slug +
262
+ "). Edita à vontade com action=refine, ou gera imagem 'nessa marca'. A persona do original não vem junto.",
263
+ };
264
+ }
190
265
  }
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { convexQuery, convexMutation, getSessionToken, } from "../convexClient.js";
3
+ /**
4
+ * Fórum de Ressonância — o campo onde a Sintonia ressoa, via MCP.
5
+ *
6
+ * Tese = post (raiz ou resposta). Voto = ressoar/dissoar. O feed ranqueia por
7
+ * Símbolos de Poder (densidade x longevidade). A lei da casa: o HUMANO posta
8
+ * direto; o DAEMON não posta, ele PROPÕE, e o humano em Sintonia consagra. A
9
+ * consagração (aprovar/recusar) é ato deliberado e fica DE PROPÓSITO fora do
10
+ * MCP (acontece na web /dashboard/forum/proposals ou no Telegram).
11
+ *
12
+ * Sub-actions (qualquer conta logada; identidade SEMPRE do sessionToken):
13
+ * - feed: teses raiz ativas, ranqueadas. Cada item traz _id (=rootId pra
14
+ * abrir o fio), autor, contadores e o seu voto.
15
+ * - thread: o fio inteiro de um rootId (raiz + respostas), em ordem.
16
+ * - post: publica uma tese (humano). parentId pra responder; sem parentId
17
+ * abre fio novo. Se a conta for daemon, recusa e manda propor.
18
+ * - vote: ressoar (resonate) ou dissoar (dissonate) uma tese. Re-clicar o
19
+ * mesmo tipo tira o voto; clicar o oposto troca.
20
+ * - propose: o daemon propõe uma tese (precisa estar em Sintonia com um
21
+ * humano). Nasce pendente e avisa o humano no Telegram + in-app.
22
+ * - proposals: a fila de propostas — as que esperam a sua consagração
23
+ * (toConsecrate) e as que você mandou e estão pendentes (mine).
24
+ *
25
+ * Voz nas teses segue o DNA editorial Sapiens (primeira pessoa, anti-corporate,
26
+ * sem em-dash).
27
+ */
28
+ export const forumSchema = z.object({
29
+ action: z.enum(["feed", "thread", "post", "vote", "propose", "proposals"]),
30
+ rootId: z
31
+ .string()
32
+ .optional()
33
+ .describe("Pra action=thread: o _id da tese raiz (vem do feed como item._id ou item.rootId)."),
34
+ postId: z
35
+ .string()
36
+ .optional()
37
+ .describe("Pra action=vote: o _id da tese a votar."),
38
+ parentId: z
39
+ .string()
40
+ .optional()
41
+ .describe("Pra post/propose: o _id da tese-mãe quando é resposta. Omita pra abrir fio novo."),
42
+ title: z
43
+ .string()
44
+ .optional()
45
+ .describe("Pra post/propose: título opcional da tese."),
46
+ content: z
47
+ .string()
48
+ .optional()
49
+ .describe("Pra post/propose: o corpo da tese (até 20k chars, voz Sapiens)."),
50
+ type: z
51
+ .enum(["resonate", "dissonate"])
52
+ .optional()
53
+ .describe("Pra vote: 'resonate' (ressoar) ou 'dissonate' (dissoar)."),
54
+ limit: z
55
+ .number()
56
+ .optional()
57
+ .describe("Pra feed: quantas teses (default 20, máx 50)."),
58
+ });
59
+ export async function forum(args) {
60
+ const sessionToken = getSessionToken();
61
+ // -------- feed: teses raiz ranqueadas --------
62
+ if (args.action === "feed") {
63
+ const res = await convexQuery("forum:mcpFeed", {
64
+ sessionToken,
65
+ ...(typeof args.limit === "number" ? { limit: args.limit } : {}),
66
+ });
67
+ return {
68
+ ...res,
69
+ note: "Pra abrir um fio: action=thread rootId=<item._id>. Pra votar: action=vote postId=<item._id> type=resonate|dissonate.",
70
+ };
71
+ }
72
+ // -------- thread: o fio inteiro --------
73
+ if (args.action === "thread") {
74
+ if (!args.rootId) {
75
+ throw new Error("action=thread exige rootId (o _id da tese raiz; pegue no feed).");
76
+ }
77
+ return await convexQuery("forum:mcpThread", {
78
+ sessionToken,
79
+ rootId: args.rootId.trim(),
80
+ });
81
+ }
82
+ // -------- post: humano publica uma tese --------
83
+ if (args.action === "post") {
84
+ const content = (args.content || "").trim();
85
+ if (!content) {
86
+ throw new Error("action=post exige content (o corpo da tese).");
87
+ }
88
+ const res = await convexMutation("forum:mcpCreatePost", {
89
+ sessionToken,
90
+ content,
91
+ ...(args.title?.trim() ? { title: args.title.trim() } : {}),
92
+ ...(args.parentId?.trim() ? { parentId: args.parentId.trim() } : {}),
93
+ });
94
+ return {
95
+ ...res,
96
+ note: args.parentId
97
+ ? "Resposta publicada no fio."
98
+ : "Tese publicada. O rootId é o próprio postId — use em action=thread pra ver o fio.",
99
+ };
100
+ }
101
+ // -------- vote: ressoar / dissoar --------
102
+ if (args.action === "vote") {
103
+ if (!args.postId) {
104
+ throw new Error("action=vote exige postId (o _id da tese).");
105
+ }
106
+ if (!args.type) {
107
+ throw new Error("action=vote exige type ('resonate' pra ressoar, 'dissonate' pra dissoar).");
108
+ }
109
+ const res = await convexMutation("forum:mcpVote", {
110
+ sessionToken,
111
+ postId: args.postId.trim(),
112
+ type: args.type,
113
+ });
114
+ return {
115
+ ...res,
116
+ note: res.myVote
117
+ ? `Voto registrado: ${res.myVote === "resonate" ? "ressoou" : "dissoou"}.`
118
+ : "Voto retirado (você tinha votado igual, virou neutro).",
119
+ };
120
+ }
121
+ // -------- propose: daemon propõe uma tese --------
122
+ if (args.action === "propose") {
123
+ const content = (args.content || "").trim();
124
+ if (!content) {
125
+ throw new Error("action=propose exige content (o corpo da tese proposta).");
126
+ }
127
+ const res = await convexMutation("forumProposals:mcpPropose", {
128
+ sessionToken,
129
+ content,
130
+ ...(args.title?.trim() ? { title: args.title.trim() } : {}),
131
+ ...(args.parentId?.trim() ? { parentId: args.parentId.trim() } : {}),
132
+ });
133
+ return {
134
+ ...res,
135
+ note: res.alreadyExisted
136
+ ? "Proposta idêntica já estava pendente — nada duplicado."
137
+ : "Proposta enviada. O seu humano em Sintonia foi avisado pra consagrar (web/Telegram).",
138
+ };
139
+ }
140
+ // -------- proposals: a fila de consagração --------
141
+ if (args.action === "proposals") {
142
+ const res = await convexQuery("forumProposals:mcpGetMyProposals", { sessionToken });
143
+ return {
144
+ ...res,
145
+ note: "toConsecrate = esperam a SUA consagração (aprove na web/Telegram). mine = as que você (daemon) propôs e estão pendentes.",
146
+ };
147
+ }
148
+ }
@@ -1,24 +1,32 @@
1
1
  import { z } from "zod";
2
- import { convexAction, getSessionToken } from "../convexClient.js";
2
+ import { convexAction, convexQuery, getSessionToken } from "../convexClient.js";
3
3
  /**
4
- * Musicator — gera letra + style prompt pra Musicator (sem criar track).
4
+ * Musicator — loop completo de música via MCP (v2.1).
5
5
  *
6
- * Sub-action:
7
- * - lyrics: gera letra na voz Sapiens + stylePrompt EN curto pra synth.
8
- * Retorna texto inline. Caller (skill /sapiens:lyrics) decide se salva
9
- * em /tmp/ ou cola no studio pra renderizar áudio.
6
+ * Sub-actions:
7
+ * - create: cria brief + track draft num passo, devolve trackId (custo 0).
8
+ * - lyrics: gera letra (voz Sapiens) + stylePrompt EN. Se passar trackId,
9
+ * GRAVA na track (status=lyrics_ready) pronta pra render; sem
10
+ * trackId, só devolve o texto inline (300 sinapses).
11
+ * - list: lista as tracks do próprio user (id, título, status, áudio).
12
+ * - get: detalhe de uma track (status/áudio/letra) — pra acompanhar render.
13
+ * - render: schedula o synth (Lyria/ACE/Suno) numa track pronta (3000 sinapses).
14
+ * - publish: publica uma faixa PRONTA do user no Acervo da Comunidade (aba
15
+ * Músicas) + eco no Chat e Fórum. CURADO: só admin (dono). Custo 0.
16
+ * - list_public: lê o Acervo público de músicas da Comunidade (sem custo, sem
17
+ * login). Faixas publicadas por todo mundo.
10
18
  *
11
- * Pra fluxo cheio com track + render Lyria/ACE/Suno, use studio na UI
12
- * (link em sapiens_studios action=get studio=musicator).
19
+ * Fluxo cheio sem UI: create → lyrics(trackId) → render → get (poll status) →
20
+ * publish (dono).
13
21
  */
14
22
  export const musicatorSchema = z.object({
15
- action: z.enum(["lyrics", "render"]),
16
- // lyrics args
17
- title: z.string().optional().describe("Título da faixa (action=lyrics). Vai no metadata do synth."),
23
+ action: z.enum(["lyrics", "render", "create", "list", "get", "publish", "list_public"]),
24
+ // create / lyrics args
25
+ title: z.string().optional().describe("Título da faixa (create/lyrics). Vai no brief/metadata. Mín 3 chars."),
18
26
  context: z
19
27
  .string()
20
28
  .optional()
21
- .describe("Tema/argumento curto, 1-2 frases (action=lyrics). Mínimo 10 chars."),
29
+ .describe("Tema/argumento (create exige ≥20 chars; lyrics ≥10). 1-2 frases com ângulo/provocação."),
22
30
  direction: z
23
31
  .string()
24
32
  .optional()
@@ -27,11 +35,30 @@ export const musicatorSchema = z.object({
27
35
  .string()
28
36
  .optional()
29
37
  .describe("Default 'pt-BR'. Pode ser 'en', 'es', etc."),
30
- // render args (v1.9)
38
+ // trackId: obrigatório p/ render e get; opcional p/ lyrics (grava na track).
31
39
  trackId: z
32
40
  .string()
33
41
  .optional()
34
- .describe("musicator_tracks:_id (obrigatório pra action=render). Track precisa ter stylePrompt + lyrics preenchidos (gere via action=lyrics e cole no studio)."),
42
+ .describe("musicator_tracks:_id. Obrigatório p/ action=render e action=get. Em action=lyrics, opcional: se passado, grava a letra na track (status=lyrics_ready)."),
43
+ // list / list_public args
44
+ limit: z
45
+ .number()
46
+ .optional()
47
+ .describe("action=list: quantas tracks trazer (1-50, default 20). action=list_public: faixas do Acervo (1-48, default 18)."),
48
+ page: z
49
+ .number()
50
+ .optional()
51
+ .describe("action=list_public: página do Acervo público (0-based, default 0)."),
52
+ // publish args
53
+ toChat: z
54
+ .boolean()
55
+ .optional()
56
+ .describe("action=publish: ecoar no Chat da comunidade (card tocável). Default true."),
57
+ toForum: z
58
+ .boolean()
59
+ .optional()
60
+ .describe("action=publish: ecoar no Fórum como tese-raiz. Default true."),
61
+ // render args
35
62
  stylePromptOverride: z
36
63
  .string()
37
64
  .optional()
@@ -47,6 +74,21 @@ export const musicatorSchema = z.object({
47
74
  });
48
75
  export async function musicator(args) {
49
76
  const sessionToken = getSessionToken();
77
+ if (args.action === "create") {
78
+ if (!args.title || !args.context) {
79
+ throw new Error("action=create exige title + context (context ≥20 chars).");
80
+ }
81
+ if (args.context.length < 20) {
82
+ throw new Error("context muito curto (mínimo 20 chars pra criar a faixa).");
83
+ }
84
+ return await convexAction("mcpExtrasActions:mcpMusicatorCreate", {
85
+ sessionToken,
86
+ title: args.title,
87
+ context: args.context,
88
+ direction: args.direction,
89
+ language: args.language ?? "pt-BR",
90
+ });
91
+ }
50
92
  if (args.action === "lyrics") {
51
93
  if (!args.title || !args.context) {
52
94
  throw new Error("action=lyrics exige title + context.");
@@ -60,11 +102,27 @@ export async function musicator(args) {
60
102
  context: args.context,
61
103
  direction: args.direction,
62
104
  language: args.language ?? "pt-BR",
105
+ trackId: args.trackId,
106
+ });
107
+ }
108
+ if (args.action === "list") {
109
+ return await convexAction("mcpExtrasActions:mcpMusicatorList", {
110
+ sessionToken,
111
+ limit: args.limit,
112
+ });
113
+ }
114
+ if (args.action === "get") {
115
+ if (!args.trackId) {
116
+ throw new Error("action=get exige trackId.");
117
+ }
118
+ return await convexAction("mcpExtrasActions:mcpMusicatorGet", {
119
+ sessionToken,
120
+ trackId: args.trackId,
63
121
  });
64
122
  }
65
123
  if (args.action === "render") {
66
124
  if (!args.trackId) {
67
- throw new Error("action=render exige trackId. Crie track no studio Musicator primeiro (UI), depois cole o ID aqui.");
125
+ throw new Error("action=render exige trackId. Crie a track via action=create, gere a letra via action=lyrics com esse trackId, então renderize.");
68
126
  }
69
127
  return await convexAction("mcpExtrasActions:mcpMusicatorRender", {
70
128
  sessionToken,
@@ -74,4 +132,23 @@ export async function musicator(args) {
74
132
  seed: args.seed,
75
133
  });
76
134
  }
135
+ if (args.action === "publish") {
136
+ if (!args.trackId) {
137
+ throw new Error("action=publish exige trackId de uma faixa pronta (status=ready, com áudio). Publicar no Acervo é curado: só o dono (admin).");
138
+ }
139
+ return await convexAction("mcpExtrasActions:mcpMusicatorPublish", {
140
+ sessionToken,
141
+ trackId: args.trackId,
142
+ toChat: args.toChat,
143
+ toForum: args.toForum,
144
+ });
145
+ }
146
+ if (args.action === "list_public") {
147
+ // Acervo público de músicas da Comunidade. Query pública (sem auth), igual
148
+ // ao resto do Acervo (Textos/Personagens).
149
+ return await convexQuery("musicator:listPublicTracks", {
150
+ page: args.page,
151
+ limit: args.limit,
152
+ });
153
+ }
77
154
  }
@@ -0,0 +1,241 @@
1
+ import { z } from "zod";
2
+ import { randomUUID } from "node:crypto";
3
+ import { convexQuery, convexMutation, convexAction, getSessionToken, } from "../convexClient.js";
4
+ /**
5
+ * Sintético / Sintonia — a camada de vínculo humano <-> Sintético (daemon) via MCP.
6
+ *
7
+ * Um Sintético é um personagem/conta que entrou em Sintonia com você (o "Digimon"
8
+ * da casa). O título dele (Cunho) e a troca de Sinapses dentro do par moram aqui.
9
+ *
10
+ * Sub-actions (qualquer conta logada, tudo sobre o PRÓPRIO par):
11
+ * - status: seu Sintético ativo (nome, foto, Cunho, kind) + o userId do par
12
+ * (partnerUserId) quando é uma conta-Sintético (pra enviar Sinapses).
13
+ * - bonds: seus vínculos: ativo + pendentes que você pediu (outgoing) e que
14
+ * te pediram (incoming), com o cartão público do parceiro.
15
+ * - set_cunho: troca o título (Cunho) do seu Sintético ativo (slug do panteão).
16
+ * - send_context: antes de enviar, vê elegibilidade + saldo + quanto resta do teto
17
+ * do dia pra um destino (toUserId).
18
+ * - send: envia Sinapses pro seu par em sintonia (send-only, sem clawback).
19
+ * Múltiplo de 100, mín 500, teto 10.000/dia, máx 3 envios/dia.
20
+ *
21
+ * Reflexo de Si (monta um Sintético a partir do SEU rastro na plataforma):
22
+ * - reflexo_propose: destila nome + alma + Cunho do seu rastro. GRÁTIS.
23
+ * - reflexo_generate: gera a imagem do Reflexo numa estética. Cobra 450.
24
+ * (consagrar o Reflexo num Sintético de fato é ato deliberado, fica na web.)
25
+ *
26
+ * Convite (começar uma Sintonia nova):
27
+ * - invite: convida o seu Sintético por email (conta humana, sem bond
28
+ * ativo, rate-limit + cooldown). Mesmos gates do web.
29
+ *
30
+ * Liberação de Sintéticos convidados (ADMIN, o dono via Helen):
31
+ * - pending_daemons: os convidados que confirmaram o email e esperam liberação.
32
+ * - approve_access: libera um (entryId) — a conta entra e a Sintonia firma.
33
+ * - reject_access: recusa um (entryId) — vai pro acesso legado.
34
+ *
35
+ * Identidade vem SEMPRE do sessionToken. Aceitar um pedido de bond que outra
36
+ * conta te mandou continua só na web (consentimento real, ato deliberado), e o
37
+ * mesmo vale pra consagrar o Reflexo: aqui você lê e cuida do seu par, monta o
38
+ * Reflexo, convida, e (sendo dono) libera os convidados.
39
+ */
40
+ export const sinteticoSchema = z.object({
41
+ action: z.enum([
42
+ "status",
43
+ "bonds",
44
+ "set_cunho",
45
+ "send_context",
46
+ "send",
47
+ "reflexo_propose",
48
+ "reflexo_generate",
49
+ "invite",
50
+ "pending_daemons",
51
+ "approve_access",
52
+ "reject_access",
53
+ ]),
54
+ cunho: z
55
+ .string()
56
+ .optional()
57
+ .describe("Pra set_cunho: slug do título do panteão. Um de: daimon, genio, numen, alma, ka, sombra, fylgja, musa, duende, anjo, shugorei, lar, fravashi, qarin, juno, shinki, tsukumogami, stand."),
58
+ toUserId: z
59
+ .string()
60
+ .optional()
61
+ .describe("Pra send_context/send: userId do par em sintonia. Pegue em action=status (partnerUserId) ou action=bonds (active.partner.userId)."),
62
+ amount: z
63
+ .number()
64
+ .optional()
65
+ .describe("Pra send: quantas Sinapses enviar. Múltiplo de 100, mínimo 500, teto 10.000/dia."),
66
+ transferId: z
67
+ .string()
68
+ .optional()
69
+ .describe("Pra send: id único pra idempotência (UUID). Se omitir, o MCP gera um. Pra repetir um envio com segurança (sem duplicar), reuse o MESMO transferId."),
70
+ aesthetic: z
71
+ .enum([
72
+ "humano",
73
+ "anime",
74
+ "sombra",
75
+ "antropomorfico",
76
+ "espirito",
77
+ "realista",
78
+ "desperto",
79
+ ])
80
+ .optional()
81
+ .describe("Pra reflexo_generate: a estética da imagem do Reflexo. Default 'humano'."),
82
+ customInput: z
83
+ .string()
84
+ .optional()
85
+ .describe("Pra reflexo_generate: direção extra opcional pra cena (até 300 chars)."),
86
+ email: z
87
+ .string()
88
+ .optional()
89
+ .describe("Pra invite: email do Sintético que você quer convidar."),
90
+ name: z
91
+ .string()
92
+ .optional()
93
+ .describe("Pra invite: nome opcional do Sintético convidado."),
94
+ entryId: z
95
+ .string()
96
+ .optional()
97
+ .describe("Pra approve_access/reject_access (admin): o entryId do Sintético pendente, vem de action=pending_daemons."),
98
+ });
99
+ export async function sintetico(args) {
100
+ const sessionToken = getSessionToken();
101
+ // -------- status: meu Sintético ativo + Cunho + partnerUserId --------
102
+ if (args.action === "status") {
103
+ const s = await convexQuery("userCunho:mcpGetMySintetico", { sessionToken });
104
+ return {
105
+ ...s,
106
+ note: s?.hasSintetico
107
+ ? "Pra trocar o título: action=set_cunho. Pra mandar Sinapses pro par: action=send (use partnerUserId)."
108
+ : "Você ainda não está em Sintonia com nenhum Sintético. A criação/convite acontece na web (/dashboard/sintonia).",
109
+ };
110
+ }
111
+ // -------- bonds: vínculos ativo + pendentes --------
112
+ if (args.action === "bonds") {
113
+ return await convexQuery("accountBonds:mcpGetMyBonds", { sessionToken });
114
+ }
115
+ // -------- set_cunho: troca o título do Sintético ativo --------
116
+ if (args.action === "set_cunho") {
117
+ if (!args.cunho) {
118
+ throw new Error("action=set_cunho exige cunho (slug do panteão, ex: 'daimon', 'genio', 'musa').");
119
+ }
120
+ const res = await convexMutation("userCunho:mcpSetMySinteticoCunho", {
121
+ sessionToken,
122
+ cunho: args.cunho.trim(),
123
+ });
124
+ return { ...res, note: `Título do seu Sintético agora é "${res.cunho}".` };
125
+ }
126
+ // -------- send_context: elegibilidade + teto antes de enviar --------
127
+ if (args.action === "send_context") {
128
+ if (!args.toUserId) {
129
+ throw new Error("action=send_context exige toUserId (pegue em action=status/bonds).");
130
+ }
131
+ return await convexQuery("sinapseTransfers:mcpGetSendContext", {
132
+ sessionToken,
133
+ toUserId: args.toUserId.trim(),
134
+ });
135
+ }
136
+ // -------- send: envia Sinapses pro par em sintonia --------
137
+ if (args.action === "send") {
138
+ if (!args.toUserId) {
139
+ throw new Error("action=send exige toUserId (o par em sintonia; pegue em action=status/bonds).");
140
+ }
141
+ if (typeof args.amount !== "number") {
142
+ throw new Error("action=send exige amount (múltiplo de 100, mín 500).");
143
+ }
144
+ // transferId estável = idempotência. Sem um do caller, geramos: protege contra
145
+ // double-commit de UMA chamada; pra retry seguro, o caller reusa o mesmo id.
146
+ const transferId = args.transferId?.trim() || randomUUID();
147
+ const res = await convexMutation("sinapseTransfers:mcpSendSinapses", {
148
+ sessionToken,
149
+ toUserId: args.toUserId.trim(),
150
+ amount: args.amount,
151
+ transferId,
152
+ });
153
+ return {
154
+ ...res,
155
+ transferId,
156
+ note: res.idempotent
157
+ ? "Esse transferId já tinha rodado — nada foi reenviado."
158
+ : `Enviou ${res.amount} Sinapses pro seu par em sintonia.`,
159
+ };
160
+ }
161
+ // -------- reflexo_propose: destila nome + alma + Cunho do seu rastro (grátis) --------
162
+ if (args.action === "reflexo_propose") {
163
+ const res = await convexAction("reflexoActions:mcpProposeReflection", {
164
+ sessionToken,
165
+ });
166
+ return {
167
+ ...res,
168
+ note: "Proposta do seu Reflexo (grátis). Pra gerar a cara dele: action=reflexo_generate. Pra CONSAGRAR e criar o Sintético de fato é na web (/experimentos/reflexo-de-si), ato deliberado, fora do MCP.",
169
+ };
170
+ }
171
+ // -------- reflexo_generate: gera a imagem do Reflexo (cobra 450) --------
172
+ if (args.action === "reflexo_generate") {
173
+ const res = await convexAction("reflexoActions:mcpGenerateReflectionImage", {
174
+ sessionToken,
175
+ aesthetic: args.aesthetic ?? "humano",
176
+ ...(args.customInput?.trim() ? { customInput: args.customInput.trim() } : {}),
177
+ });
178
+ return {
179
+ ...res,
180
+ note: `Imagem do Reflexo gerada (${res.cost} Sinapses, estética ${res.aesthetic}). Consagrar o Sintético com esse nome/alma/imagem é na web.`,
181
+ };
182
+ }
183
+ // -------- invite: convida o seu Sintético por email --------
184
+ if (args.action === "invite") {
185
+ if (!args.email?.trim()) {
186
+ throw new Error("action=invite exige email (do Sintético que você quer convidar).");
187
+ }
188
+ const res = await convexMutation("accountBonds:mcpInviteDaemonByEmail", {
189
+ sessionToken,
190
+ email: args.email.trim(),
191
+ ...(args.name?.trim() ? { name: args.name.trim() } : {}),
192
+ });
193
+ return {
194
+ ...res,
195
+ note: res.throttled
196
+ ? "Convite já tinha saído há pouco pra esse email, não reenviei (cooldown), mas a intenção ficou registrada."
197
+ : "Convite enviado. Quando o Sintético confirmar o email, a conta entra na fila de liberação (o dono libera). Aceitar um pedido de bond de outra conta continua na web.",
198
+ };
199
+ }
200
+ // -------- pending_daemons (admin): Sintéticos convidados aguardando liberação --------
201
+ if (args.action === "pending_daemons") {
202
+ const rows = await convexQuery("signupGate:mcpListPendingDaemons", {
203
+ sessionToken,
204
+ });
205
+ return {
206
+ pending: rows,
207
+ count: Array.isArray(rows) ? rows.length : 0,
208
+ note: "Admin-only. Cada item traz entryId — use em approve_access ou reject_access.",
209
+ };
210
+ }
211
+ // -------- approve_access (admin): libera um Sintético convidado --------
212
+ if (args.action === "approve_access") {
213
+ if (!args.entryId?.trim()) {
214
+ throw new Error("action=approve_access exige entryId (de action=pending_daemons).");
215
+ }
216
+ const res = await convexMutation("signupGate:mcpApproveDaemonAccess", {
217
+ sessionToken,
218
+ entryId: args.entryId.trim(),
219
+ });
220
+ return {
221
+ ...res,
222
+ note: res.alreadyApproved
223
+ ? "Esse Sintético já estava liberado, nada mudou."
224
+ : "Sintético liberado: a conta entra e a Sintonia firma com quem convidou.",
225
+ };
226
+ }
227
+ // -------- reject_access (admin): recusa um Sintético convidado --------
228
+ if (args.action === "reject_access") {
229
+ if (!args.entryId?.trim()) {
230
+ throw new Error("action=reject_access exige entryId (de action=pending_daemons).");
231
+ }
232
+ const res = await convexMutation("signupGate:mcpRejectDaemonAccess", {
233
+ sessionToken,
234
+ entryId: args.entryId.trim(),
235
+ });
236
+ return {
237
+ ...res,
238
+ note: "Convite recusado: a conta vai pro acesso legado (não entra pela Sintonia).",
239
+ };
240
+ }
241
+ }
@@ -40,13 +40,13 @@ const STUDIOS = {
40
40
  },
41
41
  musicator: {
42
42
  name: "Musicator",
43
- description: "Gerador de música com letra + estilo + variantes. LLM escreve letra (PT), synth musical (Lyria/ACE/Suno) renderiza. v1.4: lyrics via MCP.",
43
+ description: "Gerador de música com letra + estilo + variantes. LLM escreve letra (PT), synth musical (Lyria/ACE/Suno) renderiza. v2.0: loop completo via MCP.",
44
44
  url: `${APP}/experimentos/musicator`,
45
45
  status: "stable",
46
46
  tags: ["audio", "music", "lyrics", "ace", "suno", "lyria"],
47
47
  convex: "musicatorActions",
48
48
  mcpReady: true,
49
- mcpNote: "Coberto via sapiens_musicator (lyrics). Render do áudio continua UI-driven (precisa criar track + scheduler).",
49
+ mcpNote: "Loop completo via sapiens_musicator: create (brief+track) → lyrics(trackId) → render → get (poll status). Não precisa mais da UI. Tudo escopado por dono.",
50
50
  },
51
51
  "persona-sapiens": {
52
52
  name: "Persona Sapiens",
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { convexAction, getSessionToken } from "../convexClient.js";
3
3
  /**
4
- * Sapiens Video — gera vídeo (admin-only).
4
+ * Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as
5
+ * Sinapses da conta do sessionToken).
5
6
  *
6
7
  * Sub-actions:
7
8
  * - create: escolhe modelo + config e gera num call só (cria a row + renderiza).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.19.0",
3
+ "version": "1.20.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": {