sapiens-mcp 1.8.0 → 1.10.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.
@@ -139,24 +139,67 @@ function readStoredSessionToken() {
139
139
  return null;
140
140
  }
141
141
  export function getSessionToken() {
142
- // Prioridade 1: env var explícita (config do MCP ou .env.local)
143
- const fromEnv = process.env.SAPIENS_DESKTOP_SESSION_TOKEN;
144
- if (fromEnv && fromEnv !== "PASTE_HERE_AFTER_RUNNING_auth.mjs") {
145
- return fromEnv;
146
- }
147
- // Prioridade 2: store local do login do MCP (caminho npm)
142
+ // Ordem de prioridade (mudou em v1.9.1 ver POR QUE abaixo):
143
+ // 1. store local do login do MCP (~/.sapiens-mcp/session.json) — o que
144
+ // `sapiens_meta action=login` acabou de salvar. Fonte canônica.
145
+ // 2. state do plugin Claude Code (monorepo, /sapiens:login).
146
+ // 3. env var SAPIENS_DESKTOP_SESSION_TOKEN (.env.local / config do MCP) —
147
+ // fallback pra setups headless/CI que nunca rodaram login interativo.
148
+ //
149
+ // POR QUE login (disco) vem ANTES do env: até a v1.9.0 o env tinha prioridade
150
+ // 1, então um token VELHO em .env.local sombreava um login fresco. O `/login`
151
+ // dizia "conectado" (ele usa o token recém-redimido direto), mas todas as
152
+ // outras chamadas pegavam o token velho do env e estouravam "sessionToken
153
+ // inválido" — que, sem o fix do describeError, aparecia como "Server Error"
154
+ // opaco. Resultado: login parecia funcionar e nada mais funcionava. Agora um
155
+ // login fresco sempre vence. Pra forçar o env explicitamente (caso raro),
156
+ // rode `sapiens_meta action=logout` (limpa o store em disco) e o fallback de
157
+ // env volta a valer.
148
158
  const fromStore = readStoredSessionToken();
149
159
  if (fromStore) {
150
160
  return fromStore;
151
161
  }
152
- // Prioridade 3: state do plugin Claude Code (monorepo, /sapiens:login)
153
162
  const fromPlugin = readPluginSessionToken();
154
163
  if (fromPlugin) {
155
164
  return fromPlugin;
156
165
  }
166
+ const fromEnv = process.env.SAPIENS_DESKTOP_SESSION_TOKEN;
167
+ if (fromEnv && fromEnv !== "PASTE_HERE_AFTER_RUNNING_auth.mjs") {
168
+ return fromEnv;
169
+ }
157
170
  throw new Error("Conta Sapiens não conectada. 1) Abra https://sapiensinteticos.com/conectar-claude " +
158
171
  "logado e gere o código. 2) Rode a tool de login: sapiens_meta action=login code=XXXX-XXXX.");
159
172
  }
173
+ /**
174
+ * Extrai a mensagem ÚTIL de um erro do Convex.
175
+ *
176
+ * O ConvexHttpClient embrulha qualquer throw do servidor num Error cujo
177
+ * `.message` é o opaco "[Request ID: xxx] Server Error". Pra um ConvexError
178
+ * (os throws "de aplicação" — token inválido/expirado, rate limit, saldo
179
+ * insuficiente, item não encontrado, etc.) a mensagem real fica em `.data`.
180
+ *
181
+ * Sem isso, TODO erro de aplicação virava "Server Error" e era impossível
182
+ * diagnosticar (um token expirado parecia uma queda de backend). Preferimos
183
+ * `.data` quando existe; senão caímos no `.message`. Fonte única usada tanto
184
+ * pelo handler global (index.ts) quanto pelos catches locais (ex: meta health).
185
+ */
186
+ export function describeConvexError(e) {
187
+ const data = e?.data;
188
+ if (typeof data === "string" && data.trim())
189
+ return data;
190
+ if (data && typeof data === "object") {
191
+ if (typeof data.message === "string" && data.message.trim()) {
192
+ return data.message;
193
+ }
194
+ try {
195
+ return JSON.stringify(data);
196
+ }
197
+ catch {
198
+ /* cai no message abaixo */
199
+ }
200
+ }
201
+ return e?.message ?? String(e);
202
+ }
160
203
  export async function convexQuery(fnPath, args) {
161
204
  const client = getConvex();
162
205
  return (await client.query(fnPath, args));
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ import { musicator, musicatorSchema } from "./tools/musicator.js";
19
19
  import { shorts, shortsSchema } from "./tools/shorts.js";
20
20
  import { video, videoSchema } from "./tools/video.js";
21
21
  import { write, writeSchema } from "./tools/write.js";
22
+ import { stockAudio, stockAudioSchema } from "./tools/stockAudio.js";
23
+ import { describeConvexError } from "./convexClient.js";
22
24
  const TOOLS = {
23
25
  sapiens_pipeline: {
24
26
  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), 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ó). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente — menos drift, idempotente (passa productionId pra reusar). 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. 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.",
@@ -76,7 +78,7 @@ const TOOLS = {
76
78
  handler: studios,
77
79
  },
78
80
  sapiens_persona: {
79
- description: "Persona Atlas — 16 arquétipos MBTI ilustrados (v1.4 + list_generated v1.5). Sub-actions: list_codes (16 codes + grupo NT/NF/SJ/SP, estático), list_generated (v1.5, query personaArtData.getAll, do banco quais foram gerados com bunnyUrl + updatedAt), generate (qualquer logado, 450 Sinapses, retorna URL Bunny). Codes: INTJ/INTP/ENTJ/ENTP/INFJ/INFP/ENFJ/ENFP/ISTJ/ISFJ/ESTJ/ESFJ/ISTP/ISFP/ESTP/ESFP.",
81
+ description: "Persona Sapiensquiz MBTI + perfil do user + arte dos 16 arquétipos. PRIMÁRIO (de graça, qualquer logado): get_quiz (48 perguntas Likert 1..7 + escala, estático — Claude aplica conversando), submit_quiz (manda as 48 respostas {questionId,value}, scoring server-side, salva no perfil; refazer cria profile novo), my_profile (lê tipo atual + persona + breakdown dos 4 eixos com confiança + histórico). SECUNDÁRIO: list_codes (16 codes + grupo NT/NF/SJ/SP, estático), list_generated (personaArtData.getAll, quais artesexistem), generate (arte de 1 arquétipo, 450 Sinapses combina com 'gera a arte do meu tipo' depois do quiz). Codes: INTJ/INTP/ENTJ/ENTP/INFJ/INFP/ENFJ/ENFP/ISTJ/ISFJ/ESTJ/ESFJ/ISTP/ISFP/ESTP/ESFP.",
80
82
  schema: personaSchema,
81
83
  handler: persona,
82
84
  },
@@ -100,8 +102,13 @@ const TOOLS = {
100
102
  schema: videoSchema,
101
103
  handler: video,
102
104
  },
105
+ sapiens_stock_audio: {
106
+ description: "Banco de som/trilha stock (catálogo de música pronta da Helen Ailith no CDN, free/interno). Leitura pública, sem auth. Sub-actions: categories (lista os moods/usos: Ambiente & Calmo, Intenso & Dramático, Narrativo & Cabaret, Hino & Épico), list (busca faixas com filtros mood/albumSlug/durationMax/search — devolve {count, items} com title/album/url/durationSeconds/mood/tags), get (1 faixa por audioId). Use pra puxar trilha pronta em vez de gerar via Musicator: pega a `url` da faixa escolhida e usa direto no ffmpeg como BGM de /sapiens:movie, trilha de /sapiens:movie-clip ou fundo de short. Mood disponíveis: calmo, intenso, narrativo, épico, sombrio.",
107
+ schema: stockAudioSchema,
108
+ handler: stockAudio,
109
+ },
103
110
  };
104
- const server = new Server({ name: "mcp-sapiens", version: "1.8.0" }, { capabilities: { tools: {} } });
111
+ const server = new Server({ name: "mcp-sapiens", version: "1.10.0" }, { capabilities: { tools: {} } });
105
112
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
106
113
  tools: Object.entries(TOOLS).map(([name, t]) => ({
107
114
  name,
@@ -127,11 +134,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
127
134
  }
128
135
  catch (e) {
129
136
  return {
130
- content: [{ type: "text", text: `Erro: ${e?.message ?? String(e)}` }],
137
+ content: [{ type: "text", text: `Erro: ${describeConvexError(e)}` }],
131
138
  isError: true,
132
139
  };
133
140
  }
134
141
  });
135
142
  const transport = new StdioServerTransport();
136
143
  await server.connect(transport);
137
- console.error("mcp-sapiens v1.8.0 rodando via stdio (16 tools)");
144
+ console.error("mcp-sapiens v1.10.0 rodando via stdio (17 tools)");
@@ -8,6 +8,15 @@ const MODELS = [
8
8
  "nano-banana-2", // gemini-3.1-flash-image-preview (V2, Flash 3.1) · 450 + adder · COM refs · DEFAULT
9
9
  "gpt-image-2-low", // Azure gpt-image-2 quality=low · 250
10
10
  "gpt-image-2-high", // Azure gpt-image-2 quality=high · 800
11
+ // Degen (uncensored, gate +18 na galeria). WaveSpeed = rápido (6-25s):
12
+ "wavespeed-chroma", // Chroma uncensored fotorrealista · 600
13
+ "wavespeed-flux2", // Flux.2 Klein 9B · 600
14
+ "wavespeed-flux-nsfw", // Flux dev + LoRA NSFW (AIDMA) · 600
15
+ // Civitai (sdcpp, rápido). Família FLUX no Civitai saiu: lenta demais (>5min,
16
+ // estoura o poll). Pra flux uncensored use wavespeed-flux-nsfw.
17
+ "civitai-wai-illustrious", // anime Illustrious · 400
18
+ "civitai-nova-anime-xl", // anime Illustrious · 400
19
+ "civitai-pony-v6", // Pony Diffusion V6 XL, a base nº1 do Civitai · 400
11
20
  ];
12
21
  export const imageSchema = z.object({
13
22
  action: z.enum(["generate", "request_generation", "compose"]),
@@ -15,7 +24,7 @@ export const imageSchema = z.object({
15
24
  model: z
16
25
  .enum(MODELS)
17
26
  .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."),
27
+ .describe("Default 'nano-banana-2' (Flash 3.1 com refs). 'nano-banana-max' (Pro 3) = qualidade alta. 'gpt-image-2-low/high' = Azure. DEGEN (uncensored, gate +18): 'wavespeed-chroma' (fotorrealista rápido), 'wavespeed-flux2' (Flux.2 Klein), 'wavespeed-flux-nsfw' (flux+LoRA NSFW) = WaveSpeed rápido; 'civitai-wai-illustrious'/'civitai-nova-anime-xl' (anime), 'civitai-pony-v6' (Pony V6 XL, base nº1) = Civitai sdcpp rápido."),
19
28
  aspectRatio: z
20
29
  .enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"])
21
30
  .optional()
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, } from "../convexClient.js";
2
+ import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, } from "../convexClient.js";
3
3
  export const metaSchema = z.object({
4
4
  action: z.enum([
5
5
  "login",
@@ -146,6 +146,15 @@ export async function meta(args) {
146
146
  catch {
147
147
  // best-effort: o token foi salvo mesmo que o whoami falhe
148
148
  }
149
+ // Detecta um SAPIENS_DESKTOP_SESSION_TOKEN no ambiente que difere do token
150
+ // recém-salvo. Desde a v1.9.1 o login em disco tem prioridade, então esse
151
+ // env é ignorado — mas avisamos pra ninguém ficar confuso com um token
152
+ // velho de .env.local sobrando (era exatamente o que mascarava o login
153
+ // funcionando "pela metade").
154
+ const envTok = process.env.SAPIENS_DESKTOP_SESSION_TOKEN;
155
+ const shadowingEnv = !!envTok &&
156
+ envTok !== "PASTE_HERE_AFTER_RUNNING_auth.mjs" &&
157
+ envTok !== token;
149
158
  return {
150
159
  ok: true,
151
160
  message: "Conta Sapiens conectada. Já dá pra pedir pra gerar imagem, escrever artigo, etc. (gasta as suas Sinapses).",
@@ -153,6 +162,13 @@ export async function meta(args) {
153
162
  tier: who?.user?.isAdmin ? "admin" : "user",
154
163
  balance: who?.balance?.total ?? null,
155
164
  savedTo: saved.path,
165
+ ...(shadowingEnv
166
+ ? {
167
+ note: "Achei um SAPIENS_DESKTOP_SESSION_TOKEN no ambiente (.env.local ou config do MCP). " +
168
+ "Desde a v1.9.1 o login em disco tem prioridade, então essa variável é ignorada — pode " +
169
+ "remover essa linha do .env.local pra evitar confusão (ou rode logout pra forçar o uso do env).",
170
+ }
171
+ : {}),
156
172
  };
157
173
  }
158
174
  if (args.action === "logout") {
@@ -235,7 +251,7 @@ export async function meta(args) {
235
251
  };
236
252
  }
237
253
  catch (err) {
238
- return { ok: false, error: err?.message ?? String(err) };
254
+ return { ok: false, error: describeConvexError(err) };
239
255
  }
240
256
  }
241
257
  }
@@ -1,16 +1,27 @@
1
1
  import { z } from "zod";
2
- import { convexAction, convexQuery, getSessionToken } from "../convexClient.js";
2
+ import { convexAction, convexQuery, convexMutation, getSessionToken, } from "../convexClient.js";
3
3
  /**
4
- * Persona Atlasgera 1 ilustração MBTI ou lista códigos.
4
+ * Persona Sapiensquiz MBTI + perfil do user + arte dos 16 arquétipos.
5
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.
6
+ * PRIMÁRIO (de graça, qualquer conta logada): a pessoa interage com a PRÓPRIA
7
+ * persona, conversando.
8
+ * - get_quiz: devolve as 48 perguntas (Likert 1..7) + a escala. Estático,
9
+ * sem auth. Claude aplica o quiz no chat e coleta as respostas.
10
+ * - submit_quiz: manda as 48 respostas, calcula o resultado server-side e
11
+ * salva no perfil do user. Refazer cria um profile novo (histórico cresce).
12
+ * - my_profile: lê o tipo atual (code + persona + eixos com confiança) +
13
+ * histórico. Pra "qual meu tipo?", "me explica meu perfil", "como evoluí?".
10
14
  *
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).
15
+ * SECUNDÁRIO (catálogo + arte):
16
+ * - list_codes: os 16 codes MBTI + grupo (NT/NF/SJ/SP). Estático.
17
+ * - list_generated: quais artes de arquétipo já existem (bunnyUrl).
18
+ * - generate: gera a ilustração de UM arquétipo (450 Sinapses). Combina com
19
+ * "gera a arte do meu tipo" depois de descobrir o code no quiz.
20
+ *
21
+ * Nota: o scoring é 100% server-side (Convex). O cliente só manda
22
+ * {questionId, value}; o mapeamento eixo/direção é canônico no backend, então
23
+ * mesmo que o texto local aqui fique levemente defasado, o resultado é correto
24
+ * desde que os IDs (ei-1..jp-12) batam.
14
25
  */
15
26
  const MBTI_CODES = [
16
27
  "ESFJ", "ISFJ", "ESTJ", "ISTJ",
@@ -34,14 +45,157 @@ const MBTI_NAMES = {
34
45
  ISTP: "O Virtuoso", ISFP: "O Aventureiro",
35
46
  ESTP: "O Empreendedor", ESFP: "O Animador",
36
47
  };
48
+ // Escala Likert 1..7 (mesma do quiz no site).
49
+ const LIKERT_SCALE = {
50
+ 1: "Discordo totalmente",
51
+ 2: "Discordo",
52
+ 3: "Discordo um pouco",
53
+ 4: "Neutro",
54
+ 5: "Concordo um pouco",
55
+ 6: "Concordo",
56
+ 7: "Concordo totalmente",
57
+ };
58
+ // Descrição curta de cada eixo (pra Claude contextualizar as perguntas).
59
+ const AXES_INFO = {
60
+ EI: "Extroversão vs Introversão — de onde a pessoa tira energia.",
61
+ SN: "Sensorial vs Intuitivo — como capta informação (fatos vs padrões).",
62
+ TF: "Pensamento vs Sentimento — como decide (lógica vs impacto humano).",
63
+ JP: "Julgador vs Perceptivo — como lida com o mundo (fecha vs deixa aberto).",
64
+ };
65
+ // ============================================================
66
+ // BANCO DE 48 PERGUNTAS (12 por eixo).
67
+ //
68
+ // SYNC: cópia do texto de
69
+ // apps/sapiens/src/app/experimentos/persona-sapiens/data/questions.ts
70
+ // Os IDs + a ordem batem com QUESTIONS_CONFIG em
71
+ // apps/sapiens/convex/personalityProfiles.ts (fonte canônica do scoring).
72
+ // Se mudar pergunta no site, atualize os DOIS lá E este array. O scoring é
73
+ // server-side: só os IDs importam pro resultado, o texto é o que o user lê.
74
+ // ============================================================
75
+ const QUIZ_QUESTIONS = [
76
+ // ===== EIXO E vs I =====
77
+ { id: "ei-1", axis: "EI", text: "Em festas grandes, eu saio mais energizado do que cansado." },
78
+ { id: "ei-2", axis: "EI", text: "Antes de uma decisão importante, eu prefiro pensar sozinho a conversar com alguém." },
79
+ { id: "ei-3", axis: "EI", text: "Conhecer pessoas novas me energiza mais do que me cansa." },
80
+ { id: "ei-4", axis: "EI", text: "Conheço melhor um pequeno círculo íntimo do que muitos conhecidos." },
81
+ { id: "ei-5", axis: "EI", text: "Eu penso melhor falando em voz alta do que em silêncio." },
82
+ { id: "ei-6", axis: "EI", text: "Reuniões longas com muita gente me drenam." },
83
+ { id: "ei-7", axis: "EI", text: "Tenho facilidade pra puxar conversa com estranhos." },
84
+ { id: "ei-8", axis: "EI", text: "Recarrego minhas energias em silêncio, sozinho." },
85
+ { id: "ei-9", axis: "EI", text: "Em ambientes muito quietos, eu sinto que algo está faltando." },
86
+ { id: "ei-10", axis: "EI", text: "Eu prefiro mensagens escritas a ligações telefônicas." },
87
+ { id: "ei-11", axis: "EI", text: "Penso em voz alta com outras pessoas porque o pensamento precisa de ar." },
88
+ { id: "ei-12", axis: "EI", text: "Depois de um dia social intenso, eu preciso de horas sozinho pra processar." },
89
+ // ===== EIXO S vs N =====
90
+ { id: "sn-1", axis: "SN", text: "Eu confio mais em fatos verificáveis do que em palpites." },
91
+ { id: "sn-2", axis: "SN", text: "Frequentemente percebo conexões e padrões que outros não enxergam." },
92
+ { id: "sn-3", axis: "SN", text: "Prefiro lidar com problemas concretos a especular sobre futuros possíveis." },
93
+ { id: "sn-4", axis: "SN", text: "Tenho prazer em pensar em ideias abstratas, mesmo sem aplicação imediata." },
94
+ { id: "sn-5", axis: "SN", text: "Valorizo experiência prática mais do que teoria." },
95
+ { id: "sn-6", axis: "SN", text: "Fico inquieto se passo muito tempo sem imaginar possibilidades novas." },
96
+ { id: "sn-7", axis: "SN", text: "Quando descrevo algo, prefiro ser preciso e específico." },
97
+ { id: "sn-8", axis: "SN", text: "Tenho facilidade pra falar de ideias hipotéticas que ainda não testei." },
98
+ { id: "sn-9", axis: "SN", text: "Eu noto detalhes pequenos antes de ver o quadro geral." },
99
+ { id: "sn-10", axis: "SN", text: "Metáforas e símbolos fazem mais sentido pra mim do que listas e dados." },
100
+ { id: "sn-11", axis: "SN", text: "Prefiro confiar no manual a improvisar quando aprendo algo novo." },
101
+ { id: "sn-12", axis: "SN", text: "Eu costumo pensar em como as coisas poderiam ser diferentes do que são." },
102
+ // ===== EIXO T vs F =====
103
+ { id: "tf-1", axis: "TF", text: "Em decisão difícil, eu listo prós e contras antes de sentir o que quero." },
104
+ { id: "tf-2", axis: "TF", text: "Quando alguém me conta um problema, primeiro me coloco no lugar antes de raciocinar." },
105
+ { id: "tf-3", axis: "TF", text: "Prefiro um diagnóstico honesto, mesmo brusco, a um conforto vago." },
106
+ { id: "tf-4", axis: "TF", text: "Harmonia no grupo é tão importante quanto chegar à decisão certa." },
107
+ { id: "tf-5", axis: "TF", text: "Quando dou feedback, vou direto ao problema antes de cuidar do clima." },
108
+ { id: "tf-6", axis: "TF", text: "Em conflitos, eu tento entender o que cada lado está sentindo antes de tomar partido." },
109
+ { id: "tf-7", axis: "TF", text: "Eu tendo a aplicar o mesmo critério pra todo mundo, em vez de pesar caso a caso." },
110
+ { id: "tf-8", axis: "TF", text: "Eu peso o impacto emocional de uma decisão tanto quanto a lógica dela." },
111
+ { id: "tf-9", axis: "TF", text: "Quando alguém defende uma ideia com paixão, isso pesa pouco no que eu acho da ideia." },
112
+ { id: "tf-10", axis: "TF", text: "Eu reconheço o tom emocional de uma sala antes mesmo das palavras." },
113
+ { id: "tf-11", axis: "TF", text: "Decidir o que é melhor pra alguém fica mais fácil quando eu deixo o que ela sente de fora." },
114
+ { id: "tf-12", axis: "TF", text: "Eu tendo a perdoar quando entendo a história por trás do erro." },
115
+ // ===== EIXO J vs P =====
116
+ { id: "jp-1", axis: "JP", text: "Gosto de fechar decisões logo, em vez de deixar abertas." },
117
+ { id: "jp-2", axis: "JP", text: "Mantenho opções em aberto até o último momento, porque algo melhor pode aparecer." },
118
+ { id: "jp-3", axis: "JP", text: "Listas, agendas e prazos me fazem render mais." },
119
+ { id: "jp-4", axis: "JP", text: "Estou confortável quando o plano muda no meio do caminho." },
120
+ { id: "jp-5", axis: "JP", text: "Espaço bagunçado me incomoda até eu organizar." },
121
+ { id: "jp-6", axis: "JP", text: "Prefiro começar muitas coisas a terminar uma só." },
122
+ { id: "jp-7", axis: "JP", text: "Eu prefiro entregar antes do prazo do que no limite dele." },
123
+ { id: "jp-8", axis: "JP", text: "Rotinas rígidas me sufocam, eu rendo melhor com estrutura solta." },
124
+ { id: "jp-9", axis: "JP", text: "Quando começo um projeto, já visualizo a entrega e o prazo final." },
125
+ { id: "jp-10", axis: "JP", text: "Adiar decisões pequenas me dá uma sensação de liberdade." },
126
+ { id: "jp-11", axis: "JP", text: "Programas de viagem detalhados me deixam tranquilo, não entediado." },
127
+ { id: "jp-12", axis: "JP", text: "Eu tendo a deixar várias abas mentais abertas ao mesmo tempo." },
128
+ ];
37
129
  export const personaSchema = z.object({
38
- action: z.enum(["list_codes", "list_generated", "generate"]),
130
+ action: z.enum([
131
+ "my_profile",
132
+ "get_quiz",
133
+ "submit_quiz",
134
+ "list_codes",
135
+ "list_generated",
136
+ "generate",
137
+ ]),
39
138
  code: z
40
139
  .enum(MBTI_CODES)
41
140
  .optional()
42
- .describe("Pra action=generate: código MBTI a gerar. Case-insensitive (normaliza pra maiúscula)."),
141
+ .describe("Pra action=generate: código MBTI a gerar a arte. Case-insensitive (normaliza pra maiúscula)."),
142
+ answers: z
143
+ .array(z.object({
144
+ questionId: z.string(),
145
+ value: z.number().int().min(1).max(7),
146
+ }))
147
+ .optional()
148
+ .describe("Pra action=submit_quiz: as 48 respostas { questionId, value 1..7 }. Pegue os IDs/perguntas com action=get_quiz e colete tudo antes."),
149
+ nome: z
150
+ .string()
151
+ .optional()
152
+ .describe("Pra action=submit_quiz: nome opcional pra personalizar o resultado salvo."),
43
153
  });
44
154
  export async function persona(args) {
155
+ // -------- get_quiz: as 48 perguntas pra aplicar conversando (estático) --------
156
+ if (args.action === "get_quiz") {
157
+ return {
158
+ totalQuestions: QUIZ_QUESTIONS.length,
159
+ scale: LIKERT_SCALE,
160
+ axes: AXES_INFO,
161
+ instructions: "Aplique conversando: apresente as perguntas (pode ir em blocos por eixo) e " +
162
+ "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}]. " +
164
+ "Não calcule o resultado você mesmo: o scoring é server-side.",
165
+ questions: QUIZ_QUESTIONS,
166
+ howToSubmit: "sapiens_persona action=submit_quiz answers=[{questionId:'ei-1', value:5}, ...] (48 itens, value 1..7).",
167
+ };
168
+ }
169
+ // -------- submit_quiz: salva o resultado no perfil do user (de graça) --------
170
+ if (args.action === "submit_quiz") {
171
+ const answers = args.answers ?? [];
172
+ if (answers.length < QUIZ_QUESTIONS.length) {
173
+ throw new Error(`Quiz incompleto: ${answers.length}/${QUIZ_QUESTIONS.length} respostas. ` +
174
+ "Pegue as perguntas com action=get_quiz e colete todas (valor 1..7) antes de submeter.");
175
+ }
176
+ const sessionToken = getSessionToken();
177
+ const res = await convexMutation("mcpExtras:mcpSubmitPersonaQuiz", {
178
+ sessionToken,
179
+ answers: answers.map((a) => ({ questionId: a.questionId, value: a.value })),
180
+ nome: args.nome,
181
+ });
182
+ return {
183
+ ...res,
184
+ name: MBTI_NAMES[res.code] ?? null,
185
+ group: MBTI_GROUPS[res.code] ?? null,
186
+ fullUrl: res.url ? `https://sapiensinteticos.com${res.url}` : null,
187
+ note: "Resultado salvo no seu perfil. Pra ver/discutir depois: action=my_profile. " +
188
+ "Pra a arte do seu tipo: action=generate code=" +
189
+ (res.code ?? "<code>") +
190
+ " (450 Sinapses).",
191
+ };
192
+ }
193
+ // -------- my_profile: lê o tipo atual + histórico do user --------
194
+ if (args.action === "my_profile") {
195
+ const sessionToken = getSessionToken();
196
+ return await convexQuery("mcpExtras:mcpGetMyPersona", { sessionToken });
197
+ }
198
+ // -------- list_codes: catálogo estático dos 16 arquétipos --------
45
199
  if (args.action === "list_codes") {
46
200
  return {
47
201
  count: MBTI_CODES.length,
@@ -58,6 +212,7 @@ export async function persona(args) {
58
212
  },
59
213
  };
60
214
  }
215
+ // -------- list_generated: quais artes já existem no banco --------
61
216
  if (args.action === "list_generated") {
62
217
  // personaArtData.getAll é query pública sem auth check explícito
63
218
  // (read-only, tabela de 16 rows fixos). Sem session token necessário.
@@ -74,6 +229,7 @@ export async function persona(args) {
74
229
  note: "Apenas codes que já foram gerados. Pra ver os 16 possíveis (gerados ou não), use action=list_codes.",
75
230
  };
76
231
  }
232
+ // -------- generate: arte de 1 arquétipo (450 Sinapses, qualquer logado) --------
77
233
  if (args.action === "generate") {
78
234
  if (!args.code) {
79
235
  throw new Error("action=generate exige code MBTI (ex 'INTJ').");
@@ -7,9 +7,9 @@ import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../
7
7
  * só vê items públicos (isPublic !== false).
8
8
  *
9
9
  * Mutations (v1.1+): add_item, update_item, remove_item. Usam sessionToken
10
- * e exigem admin (consistente com `requireMcpAdmin`). Ownership: tudo vai
11
- * pro userId da sessão (o user logado). Caller NÃO escolhe pra quem
12
- * adicionar.
10
+ * e valem pra QUALQUER conta logada (`requireMcpUser`) cada um mexe no
11
+ * PRÓPRIO acervo. Ownership: tudo vai pro userId da sessão (o user logado).
12
+ * Caller NÃO escolhe pra quem adicionar.
13
13
  *
14
14
  * Action-based design (memoria: consolidar tools via args).
15
15
  */
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import { convexQuery } from "../convexClient.js";
3
+ /**
4
+ * Banco de som/trilha stock (tabela stockAudio). Catálogo de música pronta pra
5
+ * usar como trilha em movie/movie-clip/shorts. Leitura pública (acervo free),
6
+ * sem auth. Espelha o padrão do stock de imagem.
7
+ *
8
+ * Fluxo típico numa skill de vídeo:
9
+ * 1. action=categories -> ver os moods/usos disponíveis
10
+ * 2. action=list (mood=.. durationMax=..) -> achar candidatas
11
+ * 3. pega a `url` da escolhida -> usa direto no ffmpeg como trilha
12
+ */
13
+ export const stockAudioSchema = z.object({
14
+ action: z
15
+ .enum(["list", "get", "categories"])
16
+ .describe("list = busca faixas; get = 1 faixa por id; categories = moods/usos."),
17
+ limit: z
18
+ .number()
19
+ .int()
20
+ .positive()
21
+ .max(100)
22
+ .optional()
23
+ .describe("Default 20, max 100. Só action=list."),
24
+ categoryId: z
25
+ .string()
26
+ .optional()
27
+ .describe("stockAudioCategories:_id pra filtrar por mood/uso (action=list)."),
28
+ search: z
29
+ .string()
30
+ .optional()
31
+ .describe("Busca em título/álbum/mood/tags (action=list)."),
32
+ mood: z
33
+ .string()
34
+ .optional()
35
+ .describe("Filtra por mood exato: calmo, intenso, narrativo, épico, sombrio."),
36
+ albumSlug: z
37
+ .string()
38
+ .optional()
39
+ .describe("Filtra por lançamento: pulso-lento, meu-mundo-em-colapso, contos-de-dados, singles."),
40
+ durationMax: z
41
+ .number()
42
+ .positive()
43
+ .optional()
44
+ .describe("Duração máxima em segundos (ex: 120 pra trilha de movie < 2min)."),
45
+ audioId: z.string().optional().describe("stockAudio:_id (obrigatório pra action=get)."),
46
+ });
47
+ export async function stockAudio(args) {
48
+ if (args.action === "categories") {
49
+ const cats = await convexQuery("stockAudio:getCategories", {});
50
+ return { count: cats?.length ?? 0, categories: cats ?? [] };
51
+ }
52
+ if (args.action === "get") {
53
+ if (!args.audioId)
54
+ throw new Error("action=get exige audioId.");
55
+ return await convexQuery("stockAudio:getAudioById", { id: args.audioId });
56
+ }
57
+ // action=list
58
+ return await convexQuery("stockAudio:getAudio", {
59
+ limit: args.limit ?? 20,
60
+ categoryId: args.categoryId,
61
+ search: args.search,
62
+ mood: args.mood,
63
+ albumSlug: args.albumSlug,
64
+ durationMax: args.durationMax,
65
+ });
66
+ }
@@ -43,6 +43,11 @@ export const writeSchema = z.object({
43
43
  .string()
44
44
  .optional()
45
45
  .describe("Instrução de voz custom (opcional)."),
46
+ repertorioItemIds: z
47
+ .array(z.string())
48
+ .max(5)
49
+ .optional()
50
+ .describe("Até 5 ids de obras do Repertório do usuário (repertorioItems:_id) pra IA usar como lente/referência do texto (sinopse + nota do dono entram no prompt). Ache os ids via sapiens_repertorio (action=list/search). Só do próprio usuário; ids de outros são ignorados."),
46
51
  // --- list ---
47
52
  limit: z
48
53
  .number()
@@ -92,6 +97,7 @@ export async function write(args) {
92
97
  sourceUserArticleId: args.sourceUserArticleId,
93
98
  voiceStyle: args.voiceStyle,
94
99
  customVoice: args.customVoice,
100
+ repertorioItemIds: args.repertorioItemIds,
95
101
  });
96
102
  }
97
103
  if (args.action === "list") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.10.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": {
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.0.4",
35
- "convex": "^1.39.1",
35
+ "convex": "^1.41.0",
36
36
  "zod": "^3.23.8",
37
37
  "zod-to-json-schema": "^3.23.5"
38
38
  },