sapiens-mcp 1.7.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.
- package/dist/convexClient.js +50 -7
- package/dist/index.js +12 -5
- package/dist/tools/image.js +10 -1
- package/dist/tools/meta.js +18 -2
- package/dist/tools/persona.js +167 -11
- package/dist/tools/repertorio.js +19 -4
- package/dist/tools/stockAudio.js +66 -0
- package/dist/tools/write.js +6 -0
- package/package.json +2 -2
package/dist/convexClient.js
CHANGED
|
@@ -139,24 +139,67 @@ function readStoredSessionToken() {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
export function getSessionToken() {
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
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.",
|
|
@@ -36,7 +38,7 @@ const TOOLS = {
|
|
|
36
38
|
handler: meta,
|
|
37
39
|
},
|
|
38
40
|
sapiens_repertorio: {
|
|
39
|
-
description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles (
|
|
41
|
+
description: "Acervo pessoal de filme/série/anime/jogo/livro/música (Repertório, o segundo cérebro do user). Reads: list (filtros mediaType/status), search (texto em title/genres/tags), get (detalhe), lists (listas curadas), popArticles, resolve (busca capa/ano/id nos providers server-side: OMDb/IGDB-Twitch/AniList/Google Books/iTunes). Mutations (qualquer logado, mexem no PRÓPRIO acervo): add_item, update_item (status/rating/tags/note/isPublic), remove_item. CAPTURA ONE-SHOT: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), faça add_item SEM questionário: infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped), pegue rating se citado; (2) chame action=resolve {mediaType, query} pra pegar capa+ano+externalId e grave add_item com o source do candidato (twitch/anilist/googlebooks/itunes, ou manual+imdbID pra filme/série do OMDb) + posterUrl; (3) se resolve vier providerKeyMissing ou vazio, grave source='manual' + UUID em externalId com title/year/genres/overview que você conhece. Upsert/dedup por (userId, source, externalId). Só pergunte se o título for ambíguo.",
|
|
40
42
|
schema: repertorioSchema,
|
|
41
43
|
handler: repertorio,
|
|
42
44
|
},
|
|
@@ -76,7 +78,7 @@ const TOOLS = {
|
|
|
76
78
|
handler: studios,
|
|
77
79
|
},
|
|
78
80
|
sapiens_persona: {
|
|
79
|
-
description: "Persona
|
|
81
|
+
description: "Persona Sapiens — quiz 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 artes já existem), 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.
|
|
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: ${
|
|
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.
|
|
144
|
+
console.error("mcp-sapiens v1.10.0 rodando via stdio (17 tools)");
|
package/dist/tools/image.js
CHANGED
|
@@ -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
|
|
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()
|
package/dist/tools/meta.js
CHANGED
|
@@ -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:
|
|
254
|
+
return { ok: false, error: describeConvexError(err) };
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
}
|
package/dist/tools/persona.js
CHANGED
|
@@ -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
|
|
4
|
+
* Persona Sapiens — quiz MBTI + perfil do user + arte dos 16 arquétipos.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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([
|
|
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').");
|
package/dist/tools/repertorio.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
|
|
2
|
+
import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
|
|
3
3
|
/**
|
|
4
4
|
* Acesso ao Repertório do Sapiens (acervo pessoal de filme/série/anime/jogo/livro/música).
|
|
5
5
|
*
|
|
@@ -7,9 +7,9 @@ import { convexMutation, convexQuery, getSessionToken, } from "../convexClient.j
|
|
|
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
|
|
11
|
-
* pro userId da sessão (o user logado).
|
|
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
|
*/
|
|
@@ -29,6 +29,7 @@ export const repertorioSchema = z.object({
|
|
|
29
29
|
"get",
|
|
30
30
|
"lists",
|
|
31
31
|
"popArticles",
|
|
32
|
+
"resolve",
|
|
32
33
|
"add_item",
|
|
33
34
|
"update_item",
|
|
34
35
|
"remove_item",
|
|
@@ -106,6 +107,20 @@ export async function repertorio(args) {
|
|
|
106
107
|
})),
|
|
107
108
|
};
|
|
108
109
|
}
|
|
110
|
+
// resolve: busca metadado (capa/ano/id) nos providers server-side, com as
|
|
111
|
+
// keys que vivem no env do Convex. Faz a captura por conversa de filme/série/
|
|
112
|
+
// jogo entrar COM capa (anime/livro/música já vêm keyless). Devolve
|
|
113
|
+
// { candidates: [...], providerKeyMissing } pro Claude escolher e gravar via
|
|
114
|
+
// add_item; se providerKeyMissing, cai em manual.
|
|
115
|
+
if (args.action === "resolve") {
|
|
116
|
+
const sessionToken = getSessionToken();
|
|
117
|
+
return await convexAction("repertorioResolve:resolveMedia", {
|
|
118
|
+
sessionToken,
|
|
119
|
+
mediaType: need(args.mediaType, "mediaType"),
|
|
120
|
+
query: need(args.query, "query"),
|
|
121
|
+
limit: 8,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
109
124
|
// Mutations: sempre exigem sessionToken
|
|
110
125
|
if (args.action === "add_item" ||
|
|
111
126
|
args.action === "update_item" ||
|
|
@@ -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
|
+
}
|
package/dist/tools/write.js
CHANGED
|
@@ -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.
|
|
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.
|
|
35
|
+
"convex": "^1.41.0",
|
|
36
36
|
"zod": "^3.23.8",
|
|
37
37
|
"zod-to-json-schema": "^3.23.5"
|
|
38
38
|
},
|