sapiens-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/convexClient.js +167 -0
- package/dist/index.js +137 -0
- package/dist/tools/article.js +155 -0
- package/dist/tools/community.js +74 -0
- package/dist/tools/gallery.js +57 -0
- package/dist/tools/helen.js +120 -0
- package/dist/tools/image.js +145 -0
- package/dist/tools/meta.js +241 -0
- package/dist/tools/musicator.js +77 -0
- package/dist/tools/persona.js +87 -0
- package/dist/tools/pipeline.js +294 -0
- package/dist/tools/quotePop.js +175 -0
- package/dist/tools/repertorio.js +217 -0
- package/dist/tools/search.js +60 -0
- package/dist/tools/shorts.js +79 -0
- package/dist/tools/studios.js +169 -0
- package/dist/tools/video.js +55 -0
- package/dist/tools/write.js +132 -0
- package/package.json +44 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Helen Voice — Text-to-Speech via ElevenLabs ou Google Gemini.
|
|
5
|
+
*
|
|
6
|
+
* Sub-actions:
|
|
7
|
+
* - speak: sintetiza fala. Retorna audioBase64 + mimeType + sizeBytes.
|
|
8
|
+
* - list_presets: catálogo de voiceIds/voiceNames recomendados pra cada
|
|
9
|
+
* provider, com sugestões de uso.
|
|
10
|
+
*
|
|
11
|
+
* Speak retorna áudio inline base64. Pra arquivos grandes (>10s), salve em
|
|
12
|
+
* disco no caller (skill /sapiens:voice faz isso em apps/sapiens/.tmp/).
|
|
13
|
+
*/
|
|
14
|
+
export const helenSchema = z.object({
|
|
15
|
+
action: z.enum(["speak", "list_presets"]),
|
|
16
|
+
text: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Texto a falar (action=speak). Max 5000 chars."),
|
|
20
|
+
provider: z
|
|
21
|
+
.enum(["elevenlabs", "google"])
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("elevenlabs (melhor natural, mais caro) ou google (Gemini TTS, mais barato). Default elevenlabs."),
|
|
24
|
+
modelId: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("ElevenLabs: 'eleven_v3' (default) ou 'eleven_multilingual_v2'. Google: 'gemini-2.5-flash-preview-tts'."),
|
|
28
|
+
voiceId: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("ElevenLabs voice_id (obrigatório se provider=elevenlabs). Use list_presets pra ver opções."),
|
|
32
|
+
googleVoiceName: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Google prebuilt voice (default Kore). Use list_presets pra ver opções."),
|
|
36
|
+
languageCode: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("ElevenLabs: ex 'pt' pra PT-BR otimizado."),
|
|
40
|
+
stylePreamble: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Google: prefixo de instrução de mood ('Read this in a thoughtful tone:'). Não suportado em ElevenLabs."),
|
|
44
|
+
voiceSettings: z
|
|
45
|
+
.object({
|
|
46
|
+
stability: z.number().min(0).max(1).optional(),
|
|
47
|
+
similarity_boost: z.number().min(0).max(1).optional(),
|
|
48
|
+
style: z.number().min(0).max(1).optional(),
|
|
49
|
+
use_speaker_boost: z.boolean().optional(),
|
|
50
|
+
speed: z.number().optional(),
|
|
51
|
+
})
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("ElevenLabs voice_settings. Default razoável: stability=0.45, similarity_boost=0.75, style=0.30."),
|
|
54
|
+
outputFormat: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("ElevenLabs ex 'mp3_44100_128'. Default da provider."),
|
|
58
|
+
clientApiKey: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("BYOK: chave do user. Se vazia, usa env default do deploy (ELEVENLABS_API_KEY ou GEMINI_API_KEY)."),
|
|
62
|
+
});
|
|
63
|
+
const PRESETS = {
|
|
64
|
+
elevenlabs: {
|
|
65
|
+
note: "ElevenLabs presets. modelId default 'eleven_v3'. Custa ~$0.30 USD pra 500 chars.",
|
|
66
|
+
voices: [
|
|
67
|
+
{ voiceId: "pNInz6obpgDQGcFmaJgB", name: "Adam", use: "narração masculina pro/editorial, médio-grave" },
|
|
68
|
+
{ voiceId: "EXAVITQu4vr4xnSDxMAC", name: "Sarah", use: "narração feminina, calorosa" },
|
|
69
|
+
{ voiceId: "IKne3meq5aSn9XLyUdCD", name: "Charlie", use: "masculina jovem, articulada" },
|
|
70
|
+
{ voiceId: "XB0fDUnXU5powFXDhCwa", name: "Charlotte", use: "feminina contemplativa" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
google: {
|
|
74
|
+
note: "Google Gemini TTS. modelId 'gemini-2.5-flash-preview-tts'. Custa ~$0.01 USD pra 500 chars (mais barato).",
|
|
75
|
+
voices: [
|
|
76
|
+
{ voiceName: "Kore", use: "feminina, neutra (default)" },
|
|
77
|
+
{ voiceName: "Aoede", use: "feminina, melódica" },
|
|
78
|
+
{ voiceName: "Charon", use: "masculina, profunda" },
|
|
79
|
+
{ voiceName: "Fenrir", use: "masculina, mais áspera" },
|
|
80
|
+
],
|
|
81
|
+
stylePreambles: [
|
|
82
|
+
"Read this in a thoughtful, almost-whispered tone:",
|
|
83
|
+
"Read this with skeptical curiosity:",
|
|
84
|
+
"Read this fast, like reciting a manifesto:",
|
|
85
|
+
"Read this slow and contemplative, with pauses:",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export async function helen(args) {
|
|
90
|
+
if (args.action === "list_presets") {
|
|
91
|
+
return PRESETS;
|
|
92
|
+
}
|
|
93
|
+
if (args.action === "speak") {
|
|
94
|
+
if (!args.text || !args.text.trim()) {
|
|
95
|
+
throw new Error("action=speak exige text (não vazio).");
|
|
96
|
+
}
|
|
97
|
+
const provider = args.provider ?? "elevenlabs";
|
|
98
|
+
const modelId = args.modelId ??
|
|
99
|
+
(provider === "elevenlabs"
|
|
100
|
+
? "eleven_v3"
|
|
101
|
+
: "gemini-2.5-flash-preview-tts");
|
|
102
|
+
if (provider === "elevenlabs" && !args.voiceId) {
|
|
103
|
+
throw new Error("provider=elevenlabs exige voiceId. Use action=list_presets pra ver opções.");
|
|
104
|
+
}
|
|
105
|
+
const sessionToken = getSessionToken();
|
|
106
|
+
return await convexAction("mcpExtrasActions:mcpHelenSpeak", {
|
|
107
|
+
sessionToken,
|
|
108
|
+
text: args.text,
|
|
109
|
+
provider,
|
|
110
|
+
modelId,
|
|
111
|
+
voiceId: args.voiceId,
|
|
112
|
+
googleVoiceName: args.googleVoiceName,
|
|
113
|
+
languageCode: args.languageCode,
|
|
114
|
+
stylePreamble: args.stylePreamble,
|
|
115
|
+
voiceSettings: args.voiceSettings,
|
|
116
|
+
outputFormat: args.outputFormat,
|
|
117
|
+
clientApiKey: args.clientApiKey,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
+
// IDs canônicos do catálogo (apps/sapiens/convex/shared/imageModels.ts).
|
|
4
|
+
// IDs fora dessa lista caem no fallback e o pricing vira 999. Sempre usar os
|
|
5
|
+
// IDs canônicos.
|
|
6
|
+
const MODELS = [
|
|
7
|
+
"nano-banana-max", // gemini-3-pro-image-preview · 450 + adder
|
|
8
|
+
"nano-banana-2", // gemini-3.1-flash-image-preview (V2, Flash 3.1) · 450 + adder · COM refs · DEFAULT
|
|
9
|
+
"gpt-image-2-low", // Azure gpt-image-2 quality=low · 250
|
|
10
|
+
"gpt-image-2-high", // Azure gpt-image-2 quality=high · 800
|
|
11
|
+
];
|
|
12
|
+
export const imageSchema = z.object({
|
|
13
|
+
action: z.enum(["generate", "request_generation", "compose"]),
|
|
14
|
+
prompt: z.string().describe("Prompt da imagem. Full-bleed, sujeito oversized."),
|
|
15
|
+
model: z
|
|
16
|
+
.enum(MODELS)
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Default 'nano-banana-2' (Flash 3.1 com referenceImageUrls). Use 'nano-banana-max' (Pro 3) pra qualidade alta. 'gpt-image-2-low/high' = Azure OpenAI."),
|
|
19
|
+
aspectRatio: z
|
|
20
|
+
.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"])
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Default '1:1'. 9:16 = vertical pra story/short, 16:9 = landscape."),
|
|
23
|
+
size: z
|
|
24
|
+
.enum(["1K", "2K", "4K"])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Default '1K'. 2K/4K só funcionam em modelos com hasResolutionAdder (nano-banana-max e nano-banana-2), adicionam 100/300 sinapses."),
|
|
27
|
+
styleId: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Default 'none'. IDs de estilo no convex/shared/imageStyles.ts."),
|
|
31
|
+
negativePrompt: z.string().optional(),
|
|
32
|
+
referenceImageUrls: z
|
|
33
|
+
.array(z.string())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("URLs públicas (Convex storage / Bunny CDN / Wikimedia) usadas como reference images. Trava character/style entre múltiplas gerações. Requer model com supportsReferences=true (nano-banana-2 ou gpt-image-2-*)."),
|
|
36
|
+
mode: z
|
|
37
|
+
.enum(["create", "edit", "variation"])
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Default 'create' (gera do zero). 'edit' aplica prompt como mudança sobre sourceImageId. 'variation' gera similar mantendo estilo. Edit/variation exigem sourceImageId e enviam a imagem como referência inline pro modelo."),
|
|
40
|
+
sourceImageId: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("ID de imagem do gallery do próprio user (`generatedImages:_id`). Obrigatório pra mode=edit ou mode=variation. Use sapiens_gallery action=list pra descobrir IDs."),
|
|
44
|
+
// v1.8 — compose frame (persona + screen via Gemini)
|
|
45
|
+
personaBase64: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Pra action=compose: base64 da imagem persona (start ref). 1 dos {personaBase64, screenImage*} obrigatório."),
|
|
49
|
+
personaMimeType: z.string().optional(),
|
|
50
|
+
screenImageBase64: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Pra action=compose: base64 da tela. Alt: screenImageUrl."),
|
|
54
|
+
screenImageMimeType: z.string().optional(),
|
|
55
|
+
screenImageUrl: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Pra action=compose: URL da tela (Bunny CDN). Convex baixa server-side."),
|
|
59
|
+
instruction: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Pra action=compose: instrução em EN pro Gemini. Ex: 'Compose a vertical 9:16 photo: persona holding a smartphone facing the camera, the phone screen displaying the provided second image (clearly visible, sharp). Mobile photography aesthetic.'"),
|
|
63
|
+
});
|
|
64
|
+
export async function image(args) {
|
|
65
|
+
const sessionToken = getSessionToken();
|
|
66
|
+
// v1.8: compose tem args próprios, validação separada
|
|
67
|
+
if (args.action === "compose") {
|
|
68
|
+
if (!args.instruction || args.instruction.trim().length < 5) {
|
|
69
|
+
throw new Error("action=compose exige 'instruction' (mínimo 5 chars).");
|
|
70
|
+
}
|
|
71
|
+
if (!args.personaBase64 && !args.screenImageBase64 && !args.screenImageUrl) {
|
|
72
|
+
throw new Error("compose exige pelo menos um de: personaBase64, screenImageBase64, screenImageUrl.");
|
|
73
|
+
}
|
|
74
|
+
return await convexAction("mcpExtrasActions:mcpComposeFrame", {
|
|
75
|
+
sessionToken,
|
|
76
|
+
personaBase64: args.personaBase64,
|
|
77
|
+
personaMimeType: args.personaMimeType,
|
|
78
|
+
screenImageBase64: args.screenImageBase64,
|
|
79
|
+
screenImageMimeType: args.screenImageMimeType,
|
|
80
|
+
screenImageUrl: args.screenImageUrl,
|
|
81
|
+
instruction: args.instruction,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (!args.prompt || args.prompt.trim().length < 5) {
|
|
85
|
+
throw new Error("Faltando 'prompt' (mínimo 5 chars).");
|
|
86
|
+
}
|
|
87
|
+
// v1.7: request_generation cria row pendente em generatedImages e debita
|
|
88
|
+
// créditos. Necessário antes de chamar sapiens_shorts/sapiens_video (que
|
|
89
|
+
// exigem imageId pré-existente). Pra modelos de imagem (nano-banana-*,
|
|
90
|
+
// gpt-image-2-*), prefira action="generate" que já faz tudo num call.
|
|
91
|
+
if (args.action === "request_generation") {
|
|
92
|
+
return await import("../convexClient.js").then(({ convexMutation }) => convexMutation("mcpExtras:mcpRequestGeneration", {
|
|
93
|
+
sessionToken,
|
|
94
|
+
model: args.model ?? "nano-banana-2",
|
|
95
|
+
styleId: args.styleId,
|
|
96
|
+
prompt: args.prompt,
|
|
97
|
+
negativePrompt: args.negativePrompt,
|
|
98
|
+
aspectRatio: args.aspectRatio ?? "16:9",
|
|
99
|
+
size: args.size ?? "1K",
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
// Edit/variation usam desktopMcp.generateImageOnline (suporta sourceImageId
|
|
103
|
+
// inline como reference). Create sem sourceImageId pode usar a rota mais
|
|
104
|
+
// direta do pipelineMcpImage (catálogo legacy). Pra simplificar, usamos
|
|
105
|
+
// sempre desktopMcp.generateImageOnline a partir da v1 do plugin, porque
|
|
106
|
+
// ele cobre os 3 modos uniformemente e tem a coreografia completa (auth,
|
|
107
|
+
// créditos, upload Bunny, patch row).
|
|
108
|
+
if ((args.mode === "edit" || args.mode === "variation") && !args.sourceImageId) {
|
|
109
|
+
throw new Error(`mode='${args.mode}' exige sourceImageId. Use sapiens_gallery action=list pra descobrir.`);
|
|
110
|
+
}
|
|
111
|
+
// Quando há referenceImageUrls (URLs externas) e mode=create, ainda usamos
|
|
112
|
+
// o pipelineMcpImage (que aceita URLs). Pra modos edit/variation usamos
|
|
113
|
+
// desktopMcp (que aceita sourceImageId interno).
|
|
114
|
+
if (args.sourceImageId) {
|
|
115
|
+
const result = await convexAction("desktopMcp:generateImageOnline", {
|
|
116
|
+
sessionToken,
|
|
117
|
+
prompt: args.prompt,
|
|
118
|
+
aspectRatio: args.aspectRatio,
|
|
119
|
+
size: args.size,
|
|
120
|
+
model: args.model ?? "nano-banana-2",
|
|
121
|
+
negativePrompt: args.negativePrompt,
|
|
122
|
+
mode: args.mode ?? "edit",
|
|
123
|
+
sourceImageId: args.sourceImageId,
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
imageId: result?.imageId,
|
|
127
|
+
url: result?.url,
|
|
128
|
+
mimeType: result?.mimeType,
|
|
129
|
+
// Não incluir base64 no retorno (polui contexto). Quem quiser bytes
|
|
130
|
+
// chama sapiens_gallery action=get com includeBase64=true.
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Fluxo legacy: pipelineMcpImage com referenceImageUrls (externas) ou puro.
|
|
134
|
+
const result = await convexAction("pipelineMcpImage:mcpGenerateImage", {
|
|
135
|
+
sessionToken,
|
|
136
|
+
prompt: args.prompt,
|
|
137
|
+
model: args.model ?? "nano-banana-2",
|
|
138
|
+
aspectRatio: args.aspectRatio,
|
|
139
|
+
size: args.size,
|
|
140
|
+
styleId: args.styleId,
|
|
141
|
+
negativePrompt: args.negativePrompt,
|
|
142
|
+
referenceImageUrls: args.referenceImageUrls,
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, } from "../convexClient.js";
|
|
3
|
+
export const metaSchema = z.object({
|
|
4
|
+
action: z.enum([
|
|
5
|
+
"login",
|
|
6
|
+
"logout",
|
|
7
|
+
"whoami",
|
|
8
|
+
"formats",
|
|
9
|
+
"health",
|
|
10
|
+
"credits",
|
|
11
|
+
"app_url",
|
|
12
|
+
"subscription",
|
|
13
|
+
]),
|
|
14
|
+
code: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Código XXXX-XXXX gerado (logado) em sapiensinteticos.com/conectar-claude. Só pra action=login."),
|
|
18
|
+
});
|
|
19
|
+
const APP_URL = "https://sapiensinteticos.com";
|
|
20
|
+
const FORMAT_GUIDE = {
|
|
21
|
+
post_social: {
|
|
22
|
+
description: "Post de texto curto/médio pra rede social (genérico).",
|
|
23
|
+
payloadHint: {
|
|
24
|
+
text: "string PT principal",
|
|
25
|
+
textEn: "string EN (opcional)",
|
|
26
|
+
hashtags: ["string"],
|
|
27
|
+
cta: "string opcional",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
carrossel_ig: {
|
|
31
|
+
description: "Carrossel Instagram com múltiplos slides texto + imagem.",
|
|
32
|
+
payloadHint: {
|
|
33
|
+
slides: [
|
|
34
|
+
{
|
|
35
|
+
title: "string opcional",
|
|
36
|
+
body: "string",
|
|
37
|
+
imagePrompt: "string (pra gerador de imagem)",
|
|
38
|
+
imageUrl: "string (preenche depois de gerar)",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
caption: "string",
|
|
42
|
+
hashtags: ["string"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
tirinha: {
|
|
46
|
+
description: "Tirinha em painéis (3-6) com diálogo curto.",
|
|
47
|
+
payloadHint: {
|
|
48
|
+
title: "string",
|
|
49
|
+
panels: [
|
|
50
|
+
{
|
|
51
|
+
description: "string narrativo",
|
|
52
|
+
dialogue: "string opcional",
|
|
53
|
+
imagePrompt: "string",
|
|
54
|
+
imageUrl: "string opcional",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
musica: {
|
|
60
|
+
description: "Faixa curta com letra + estilo + mood.",
|
|
61
|
+
payloadHint: {
|
|
62
|
+
style: "string ex 'lo-fi hip-hop'",
|
|
63
|
+
mood: "string",
|
|
64
|
+
lyrics: "string completa",
|
|
65
|
+
audioUrl: "string opcional",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
shorts_yt: {
|
|
69
|
+
description: "Roteiro pra short vertical de 30-60s.",
|
|
70
|
+
payloadHint: {
|
|
71
|
+
title: "string",
|
|
72
|
+
script: "string roteiro completo",
|
|
73
|
+
scenes: [{ visual: "string", voiceover: "string", duration: "number" }],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
cena_visual: {
|
|
77
|
+
description: "Cena visual única pra still ou abertura.",
|
|
78
|
+
payloadHint: {
|
|
79
|
+
prompt: "string",
|
|
80
|
+
composition: "string",
|
|
81
|
+
imageUrl: "string opcional",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
video_yt: {
|
|
85
|
+
description: "Vídeo programático longer-form.",
|
|
86
|
+
payloadHint: {
|
|
87
|
+
title: "string",
|
|
88
|
+
script: "string roteiro completo",
|
|
89
|
+
scenes: [{ visual: "string", voiceover: "string", duration: "number" }],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
post_linkedin: {
|
|
93
|
+
description: "Post LinkedIn (texto + opcional carrossel curto).",
|
|
94
|
+
payloadHint: { text: "string", hashtags: ["string"] },
|
|
95
|
+
},
|
|
96
|
+
post_twitter: {
|
|
97
|
+
description: "Post Twitter/X (single ou thread).",
|
|
98
|
+
payloadHint: {
|
|
99
|
+
tweets: ["string (max 280 chars)"],
|
|
100
|
+
threadStyle: "boolean",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
post_threads: {
|
|
104
|
+
description: "Post Threads (parecido com Twitter mas mais longo).",
|
|
105
|
+
payloadHint: { posts: ["string"] },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
export async function meta(args) {
|
|
109
|
+
if (args.action === "formats") {
|
|
110
|
+
return {
|
|
111
|
+
formats: FORMAT_GUIDE,
|
|
112
|
+
note: "Payload é livre (v.any() no schema). Use o hint como guia, mas pode estender.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (args.action === "app_url") {
|
|
116
|
+
return {
|
|
117
|
+
web: APP_URL,
|
|
118
|
+
conectarClaude: `${APP_URL}/conectar-claude`,
|
|
119
|
+
desktopLogin: `${APP_URL}/desktop-login`,
|
|
120
|
+
dashboard: `${APP_URL}/dashboard`,
|
|
121
|
+
adminCuration: `${APP_URL}/dashboard/admin/blog/sapiens-curation`,
|
|
122
|
+
adminPopArticles: `${APP_URL}/dashboard/admin/repertorio-articles`,
|
|
123
|
+
adminPipeline: `${APP_URL}/dashboard/admin/content`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Login: troca o código de uso único (gerado no site, logado) por um
|
|
127
|
+
// sessionToken de 30 dias e salva localmente. NÃO exige token prévio.
|
|
128
|
+
if (args.action === "login") {
|
|
129
|
+
const code = (args.code || "").trim();
|
|
130
|
+
if (!code) {
|
|
131
|
+
throw new Error("Passe o código: action=login com code=XXXX-XXXX. Gere em sapiensinteticos.com/conectar-claude (logado).");
|
|
132
|
+
}
|
|
133
|
+
const token = await convexMutation("desktopAuth:redeemDesktopCode", {
|
|
134
|
+
code,
|
|
135
|
+
});
|
|
136
|
+
if (typeof token !== "string" || token.length < 8) {
|
|
137
|
+
throw new Error("Não consegui validar o código. Ele expira em 5 min e é de uso único. Gere um novo em sapiensinteticos.com/conectar-claude.");
|
|
138
|
+
}
|
|
139
|
+
const saved = saveSessionToken(token);
|
|
140
|
+
let who = null;
|
|
141
|
+
try {
|
|
142
|
+
who = await convexQuery("mcpExtras:mcpGetMySubscription", {
|
|
143
|
+
sessionToken: token,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// best-effort: o token foi salvo mesmo que o whoami falhe
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
message: "Conta Sapiens conectada. Já dá pra pedir pra gerar imagem, escrever artigo, etc. (gasta as suas Sinapses).",
|
|
152
|
+
email: who?.user?.email ?? null,
|
|
153
|
+
tier: who?.user?.isAdmin ? "admin" : "user",
|
|
154
|
+
balance: who?.balance?.total ?? null,
|
|
155
|
+
savedTo: saved.path,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (args.action === "logout") {
|
|
159
|
+
const cleared = clearSessionToken();
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
cleared,
|
|
163
|
+
message: cleared
|
|
164
|
+
? "Sessão local removida. Rode action=login pra reconectar."
|
|
165
|
+
: "Nenhuma sessão local encontrada.",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const sessionToken = getSessionToken();
|
|
169
|
+
if (args.action === "whoami") {
|
|
170
|
+
// mcpGetMySubscription usa requireMcpUser (qualquer logado), então whoami
|
|
171
|
+
// funciona pra user E admin, e reporta o tier pro Claude saber o que pode.
|
|
172
|
+
const sub = await convexQuery("mcpExtras:mcpGetMySubscription", {
|
|
173
|
+
sessionToken,
|
|
174
|
+
});
|
|
175
|
+
const isAdmin = !!sub?.user?.isAdmin;
|
|
176
|
+
return {
|
|
177
|
+
userId: sub?.user?._id ?? null,
|
|
178
|
+
email: sub?.user?.email ?? null,
|
|
179
|
+
isAdmin,
|
|
180
|
+
tier: isAdmin ? "admin" : "user",
|
|
181
|
+
balance: sub?.balance ?? null,
|
|
182
|
+
warnings: sub?.warnings ?? null,
|
|
183
|
+
note: isAdmin
|
|
184
|
+
? "Tier admin (dono): pipeline, blog editorial, Coluna Sapiens e shorts/video, além de tudo do tier user."
|
|
185
|
+
: "Tier user: gerar imagem, escrever artigo (sapiens_write), persona, Helen TTS, Musicator, repertório, comunidade. Cada geração cobra as tuas Sinapses. Pipeline, blog editorial e Coluna são owner-only.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (args.action === "credits") {
|
|
189
|
+
// desktopMcp.whoami já vem com totalCredits (subscription+grants+legacy+free).
|
|
190
|
+
// Usa essa fonte porque pipelineMcp:mcpWhoami só fala dos campos admin.
|
|
191
|
+
const me = await convexAction("desktopMcp:whoami", { sessionToken });
|
|
192
|
+
return {
|
|
193
|
+
email: me?.email ?? null,
|
|
194
|
+
name: me?.name ?? null,
|
|
195
|
+
credits: me?.credits ?? null,
|
|
196
|
+
role: me?.role ?? null,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (args.action === "subscription") {
|
|
200
|
+
// v1.2: combinação plan + saldo detalhado por bucket (subscription/grants/free)
|
|
201
|
+
return await convexQuery("mcpExtras:mcpGetMySubscription", { sessionToken });
|
|
202
|
+
}
|
|
203
|
+
if (args.action === "health") {
|
|
204
|
+
try {
|
|
205
|
+
// Core: funciona pra qualquer logado (requireMcpUser).
|
|
206
|
+
const sub = await convexQuery("mcpExtras:mcpGetMySubscription", {
|
|
207
|
+
sessionToken,
|
|
208
|
+
});
|
|
209
|
+
const isAdmin = !!sub?.user?.isAdmin;
|
|
210
|
+
// Pipeline só pra admin (owner-only). Wrap pra nunca derrubar o health.
|
|
211
|
+
let pipeline = null;
|
|
212
|
+
if (isAdmin) {
|
|
213
|
+
try {
|
|
214
|
+
const sources = await convexQuery("pipelineMcp:mcpListSources", {
|
|
215
|
+
sessionToken,
|
|
216
|
+
});
|
|
217
|
+
pipeline = {
|
|
218
|
+
sourcesTotal: Array.isArray(sources) ? sources.length : 0,
|
|
219
|
+
sourcesPending: Array.isArray(sources)
|
|
220
|
+
? sources.filter((s) => !s.isDone).length
|
|
221
|
+
: 0,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
pipeline = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
tier: isAdmin ? "admin" : "user",
|
|
231
|
+
email: sub?.user?.email ?? null,
|
|
232
|
+
balance: sub?.balance ?? null,
|
|
233
|
+
pipeline,
|
|
234
|
+
appUrl: APP_URL,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Musicator — gera letra + style prompt pra Musicator (sem criar track).
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Pra fluxo cheio com track + render Lyria/ACE/Suno, use studio na UI
|
|
12
|
+
* (link em sapiens_studios action=get studio=musicator).
|
|
13
|
+
*/
|
|
14
|
+
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."),
|
|
18
|
+
context: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Tema/argumento curto, 1-2 frases (action=lyrics). Mínimo 10 chars."),
|
|
22
|
+
direction: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Gênero/mood (ex: 'synthwave melancólico 1980s', 'lo-fi hip-hop 80 BPM', 'acoustic indie folk'). Default 'livre'."),
|
|
26
|
+
language: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Default 'pt-BR'. Pode ser 'en', 'es', etc."),
|
|
30
|
+
// render args (v1.9)
|
|
31
|
+
trackId: z
|
|
32
|
+
.string()
|
|
33
|
+
.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)."),
|
|
35
|
+
stylePromptOverride: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Pra action=render: sobrescreve stylePrompt antes do render (não muda track persistido)."),
|
|
39
|
+
negativePrompt: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Pra action=render: negative prompt do synth (provider-dependent)."),
|
|
43
|
+
seed: z
|
|
44
|
+
.number()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Pra action=render: seed pra reprodutibilidade do synth."),
|
|
47
|
+
});
|
|
48
|
+
export async function musicator(args) {
|
|
49
|
+
const sessionToken = getSessionToken();
|
|
50
|
+
if (args.action === "lyrics") {
|
|
51
|
+
if (!args.title || !args.context) {
|
|
52
|
+
throw new Error("action=lyrics exige title + context.");
|
|
53
|
+
}
|
|
54
|
+
if (args.context.length < 10) {
|
|
55
|
+
throw new Error("context muito curto (mínimo 10 chars).");
|
|
56
|
+
}
|
|
57
|
+
return await convexAction("mcpExtrasActions:mcpMusicatorLyrics", {
|
|
58
|
+
sessionToken,
|
|
59
|
+
title: args.title,
|
|
60
|
+
context: args.context,
|
|
61
|
+
direction: args.direction,
|
|
62
|
+
language: args.language ?? "pt-BR",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (args.action === "render") {
|
|
66
|
+
if (!args.trackId) {
|
|
67
|
+
throw new Error("action=render exige trackId. Crie track no studio Musicator primeiro (UI), depois cole o ID aqui.");
|
|
68
|
+
}
|
|
69
|
+
return await convexAction("mcpExtrasActions:mcpMusicatorRender", {
|
|
70
|
+
sessionToken,
|
|
71
|
+
trackId: args.trackId,
|
|
72
|
+
stylePromptOverride: args.stylePromptOverride,
|
|
73
|
+
negativePrompt: args.negativePrompt,
|
|
74
|
+
seed: args.seed,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, convexQuery, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Persona Atlas — gera 1 ilustração MBTI ou lista códigos.
|
|
5
|
+
*
|
|
6
|
+
* Sub-actions:
|
|
7
|
+
* - list_codes: retorna os 16 MBTI codes + grupo (NT/NF/SJ/SP). Estático.
|
|
8
|
+
* - generate: gera arte pra 1 code (admin-only via session token). Retorna
|
|
9
|
+
* URL Bunny e tamanho do payload base64.
|
|
10
|
+
*
|
|
11
|
+
* Generate é admin-only e consome quota Gemini (~1 imagem). Não retorna
|
|
12
|
+
* base64 inline pra economizar contexto (use sapiens_gallery action=get pra
|
|
13
|
+
* baixar bytes se precisar).
|
|
14
|
+
*/
|
|
15
|
+
const MBTI_CODES = [
|
|
16
|
+
"ESFJ", "ISFJ", "ESTJ", "ISTJ",
|
|
17
|
+
"ESFP", "ISFP", "ESTP", "ISTP",
|
|
18
|
+
"ENFP", "INFP", "ENFJ", "INFJ",
|
|
19
|
+
"ENTP", "INTP", "ENTJ", "INTJ",
|
|
20
|
+
];
|
|
21
|
+
const MBTI_GROUPS = {
|
|
22
|
+
ESFJ: "SJ", ISFJ: "SJ", ESTJ: "SJ", ISTJ: "SJ",
|
|
23
|
+
ESFP: "SP", ISFP: "SP", ESTP: "SP", ISTP: "SP",
|
|
24
|
+
ENFP: "NF", INFP: "NF", ENFJ: "NF", INFJ: "NF",
|
|
25
|
+
ENTP: "NT", INTP: "NT", ENTJ: "NT", INTJ: "NT",
|
|
26
|
+
};
|
|
27
|
+
const MBTI_NAMES = {
|
|
28
|
+
INTJ: "O Estrategista", INTP: "O Lógico",
|
|
29
|
+
ENTJ: "O Comandante", ENTP: "O Inovador",
|
|
30
|
+
INFJ: "O Defensor", INFP: "O Mediador",
|
|
31
|
+
ENFJ: "O Protagonista", ENFP: "O Ativista",
|
|
32
|
+
ISTJ: "O Logístico", ISFJ: "O Defensor (concreto)",
|
|
33
|
+
ESTJ: "O Executivo", ESFJ: "O Cônsul",
|
|
34
|
+
ISTP: "O Virtuoso", ISFP: "O Aventureiro",
|
|
35
|
+
ESTP: "O Empreendedor", ESFP: "O Animador",
|
|
36
|
+
};
|
|
37
|
+
export const personaSchema = z.object({
|
|
38
|
+
action: z.enum(["list_codes", "list_generated", "generate"]),
|
|
39
|
+
code: z
|
|
40
|
+
.enum(MBTI_CODES)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Pra action=generate: código MBTI a gerar. Case-insensitive (normaliza pra maiúscula)."),
|
|
43
|
+
});
|
|
44
|
+
export async function persona(args) {
|
|
45
|
+
if (args.action === "list_codes") {
|
|
46
|
+
return {
|
|
47
|
+
count: MBTI_CODES.length,
|
|
48
|
+
codes: MBTI_CODES.map((c) => ({
|
|
49
|
+
code: c,
|
|
50
|
+
name: MBTI_NAMES[c],
|
|
51
|
+
group: MBTI_GROUPS[c],
|
|
52
|
+
})),
|
|
53
|
+
groups: {
|
|
54
|
+
NT: "Analistas — pensa em sistemas",
|
|
55
|
+
NF: "Diplomatas — pensa em pessoas",
|
|
56
|
+
SJ: "Sentinelas — pensa em ordem",
|
|
57
|
+
SP: "Exploradores — pensa em sensação",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (args.action === "list_generated") {
|
|
62
|
+
// personaArtData.getAll é query pública sem auth check explícito
|
|
63
|
+
// (read-only, tabela de 16 rows fixos). Sem session token necessário.
|
|
64
|
+
const rows = await convexQuery("personaArtData:getAll", {});
|
|
65
|
+
return {
|
|
66
|
+
count: Array.isArray(rows) ? rows.length : 0,
|
|
67
|
+
generated: (rows || []).map((r) => ({
|
|
68
|
+
code: r.code,
|
|
69
|
+
name: MBTI_NAMES[r.code],
|
|
70
|
+
group: MBTI_GROUPS[r.code],
|
|
71
|
+
bunnyUrl: r.bunnyUrl ?? null,
|
|
72
|
+
updatedAt: r.updatedAt ?? r._creationTime ?? null,
|
|
73
|
+
})),
|
|
74
|
+
note: "Apenas codes que já foram gerados. Pra ver os 16 possíveis (gerados ou não), use action=list_codes.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (args.action === "generate") {
|
|
78
|
+
if (!args.code) {
|
|
79
|
+
throw new Error("action=generate exige code MBTI (ex 'INTJ').");
|
|
80
|
+
}
|
|
81
|
+
const sessionToken = getSessionToken();
|
|
82
|
+
return await convexAction("mcpExtrasActions:mcpGeneratePersonaArt", {
|
|
83
|
+
sessionToken,
|
|
84
|
+
code: args.code,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|