sapiens-mcp 1.8.0 → 1.11.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 +18 -5
- package/dist/tools/brand.js +190 -0
- 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 +3 -3
- package/dist/tools/stockAudio.js +66 -0
- package/dist/tools/studios.js +18 -9
- 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,9 @@ 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 { brand, brandSchema } from "./tools/brand.js";
|
|
24
|
+
import { describeConvexError } from "./convexClient.js";
|
|
22
25
|
const TOOLS = {
|
|
23
26
|
sapiens_pipeline: {
|
|
24
27
|
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.",
|
|
@@ -71,12 +74,12 @@ const TOOLS = {
|
|
|
71
74
|
handler: search,
|
|
72
75
|
},
|
|
73
76
|
sapiens_studios: {
|
|
74
|
-
description: "Catálogo dos estúdios/experimentos Sapiens (v1.3, atualizado v1.4). Sub-actions: list (todos com URL+status+tags+mcpReady), get (detalhe de 1 slug), publishable_url (formata URL /articles/<slug>). Use quando user pergunta 'que estúdios existem'. Estúdios cobertos: helen-voice (v1.4 mcpReady), musicator (v1.4 mcpReady), persona-
|
|
77
|
+
description: "Catálogo dos estúdios/experimentos Sapiens (v1.3, atualizado v1.4). Sub-actions: list (todos com URL+status+tags+mcpReady), get (detalhe de 1 slug), publishable_url (formata URL /articles/<slug>). Use quando user pergunta 'que estúdios existem'. Estúdios cobertos: helen-voice (v1.4 mcpReady), musicator (v1.4 mcpReady), persona-sapiens (teste de autoconhecimento + arte, mcpReady), personagem-atlas (dossiê de personagem, UI-only), sapiens-shorts, sapiens-video, cena-visual, text-post-builder, carrosel-editorial, comunidade, repertorio.",
|
|
75
78
|
schema: studiosSchema,
|
|
76
79
|
handler: studios,
|
|
77
80
|
},
|
|
78
81
|
sapiens_persona: {
|
|
79
|
-
description: "Persona
|
|
82
|
+
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
83
|
schema: personaSchema,
|
|
81
84
|
handler: persona,
|
|
82
85
|
},
|
|
@@ -100,8 +103,18 @@ const TOOLS = {
|
|
|
100
103
|
schema: videoSchema,
|
|
101
104
|
handler: video,
|
|
102
105
|
},
|
|
106
|
+
sapiens_stock_audio: {
|
|
107
|
+
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.",
|
|
108
|
+
schema: stockAudioSchema,
|
|
109
|
+
handler: stockAudio,
|
|
110
|
+
},
|
|
111
|
+
sapiens_brand: {
|
|
112
|
+
description: "Brand Sapiens (= Design System) — paleta + tipografia + voz + estilo de imagem + persona como FONTE ÚNICA de estilo (espelha o Estúdio de Brand do app). Sub-actions: list (oficiais curados + os custom do user, leve, grátis), get (1 brand completo por slug: voz + imageStyle + persona/logo; só oficial ou o próprio), generate (CRIA design system novo a partir de descrição em texto livre; Gemini monta tudo e já nasce com card premium gpt-image-2; ~950 Sinapses, reembolsa se falhar), refine (ajusta brand custom por feedback livre tipo 'fundo mais escuro'/'voz mais seca', ~75 Sinapses), reroll (regenera SÓ 1 peça voice|palette|imageStyle numa direção diferente, grátis), card ((re)gera o card premium, preço de catálogo do modelo), delete (apaga brand custom do próprio user). Qualquer conta logada; Sinapses saem do dono do sessionToken. Pra generate: converse com o user e monte uma description rica (vibe, cores, voz, públicos, refs; se ele colar amostra de texto dele, inclua pra a voz sair dali) ANTES de chamar. Confirme o custo (~950 Sinapses) antes de gerar.",
|
|
113
|
+
schema: brandSchema,
|
|
114
|
+
handler: brand,
|
|
115
|
+
},
|
|
103
116
|
};
|
|
104
|
-
const server = new Server({ name: "mcp-sapiens", version: "1.
|
|
117
|
+
const server = new Server({ name: "mcp-sapiens", version: "1.11.0" }, { capabilities: { tools: {} } });
|
|
105
118
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
106
119
|
tools: Object.entries(TOOLS).map(([name, t]) => ({
|
|
107
120
|
name,
|
|
@@ -127,11 +140,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
127
140
|
}
|
|
128
141
|
catch (e) {
|
|
129
142
|
return {
|
|
130
|
-
content: [{ type: "text", text: `Erro: ${
|
|
143
|
+
content: [{ type: "text", text: `Erro: ${describeConvexError(e)}` }],
|
|
131
144
|
isError: true,
|
|
132
145
|
};
|
|
133
146
|
}
|
|
134
147
|
});
|
|
135
148
|
const transport = new StdioServerTransport();
|
|
136
149
|
await server.connect(transport);
|
|
137
|
-
console.error("mcp-sapiens v1.
|
|
150
|
+
console.error("mcp-sapiens v1.11.0 rodando via stdio (18 tools)");
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, convexQuery, convexMutation, getSessionToken, } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Brand Sapiens (= Design System). Espelha o Estúdio de Brand do app: um Brand
|
|
5
|
+
* é a FONTE ÚNICA de estilo (paleta, tipografia, voz, estilo de imagem,
|
|
6
|
+
* persona). O conteúdo (Source) diz O QUÊ; o Brand diz o COMO. Depois de criado,
|
|
7
|
+
* o brand aparece em todos os seletores do app (pipeline, /escrever, vitrine) e
|
|
8
|
+
* pode virar o padrão da conta.
|
|
9
|
+
*
|
|
10
|
+
* Sub-actions:
|
|
11
|
+
* - list: oficiais (curados) + os custom do user, versão leve (paleta,
|
|
12
|
+
* tipografia, card). De graça.
|
|
13
|
+
* - get: um brand COMPLETO por slug (voz + imageStyle + persona/logo).
|
|
14
|
+
* Só oficial ou custom do próprio user. De graça.
|
|
15
|
+
* - generate: cria um design system NOVO a partir de descrição em texto livre
|
|
16
|
+
* (Gemini monta paleta+tipografia+voz+imageStyle e já nasce com
|
|
17
|
+
* card premium gpt-image-2). Cobra ~950 Sinapses (texto + card),
|
|
18
|
+
* reembolsa se falhar. Identidade vem do sessionToken.
|
|
19
|
+
* - refine: ajusta um brand custom existente por feedback em texto livre
|
|
20
|
+
* ("fundo mais escuro", "voz menos professoral"). ~75 Sinapses.
|
|
21
|
+
* - reroll: regenera SÓ uma peça (voice | palette | imageStyle) numa direção
|
|
22
|
+
* diferente, mantendo o resto. De graça (rate-limited).
|
|
23
|
+
* - card: (re)gera o card premium (gpt-image-2). Cobra o preço de catálogo
|
|
24
|
+
* do modelo (gpt-image-2-high default, ou gpt-image-2-low).
|
|
25
|
+
* - delete: apaga um brand custom do próprio user (oficiais são intocáveis).
|
|
26
|
+
*
|
|
27
|
+
* Auth: qualquer conta logada (sessionToken). As Sinapses saem do dono do token.
|
|
28
|
+
* NÃO mexe em brand de terceiro: custom é privado por dono.
|
|
29
|
+
*/
|
|
30
|
+
const APP = "https://sapiensinteticos.com";
|
|
31
|
+
export const brandSchema = z.object({
|
|
32
|
+
action: z.enum([
|
|
33
|
+
"list",
|
|
34
|
+
"get",
|
|
35
|
+
"generate",
|
|
36
|
+
"refine",
|
|
37
|
+
"reroll",
|
|
38
|
+
"card",
|
|
39
|
+
"delete",
|
|
40
|
+
]),
|
|
41
|
+
slug: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Slug do brand. Obrigatório em get/refine/reroll/card/delete. Pegue via action=list."),
|
|
45
|
+
description: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Pra action=generate: descrição em texto livre do brand (o que o projeto/pessoa é, vibe, cores, voz, referências). Mínimo 30 chars; quanto mais rico, melhor o design system. Se o user colou um texto de amostra dele, inclua aqui pra a voz sair dali."),
|
|
49
|
+
name: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Pra action=generate: nome sugerido pro brand (opcional, ≤40 chars). Sem isso o Gemini sugere um."),
|
|
53
|
+
feedback: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Pra action=refine: o que ajustar, em texto livre ('escurece o fundo', 'fonte do título mais bruta', 'voz mais seca'). 5..2000 chars."),
|
|
57
|
+
piece: z
|
|
58
|
+
.enum(["voice", "palette", "imageStyle"])
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Pra action=reroll: qual peça regenerar numa direção diferente."),
|
|
61
|
+
model: z
|
|
62
|
+
.enum(["gpt-image-2-high", "gpt-image-2-low"])
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Pra action=card: tier do card premium (default gpt-image-2-high)."),
|
|
65
|
+
});
|
|
66
|
+
export async function brand(args) {
|
|
67
|
+
const sessionToken = getSessionToken();
|
|
68
|
+
// -------- list: oficiais + custom do user (leve, grátis) --------
|
|
69
|
+
if (args.action === "list") {
|
|
70
|
+
const brands = await convexQuery("brands:mcpListBrands", { sessionToken });
|
|
71
|
+
return {
|
|
72
|
+
count: Array.isArray(brands) ? brands.length : 0,
|
|
73
|
+
brands: (brands || []).map((b) => ({
|
|
74
|
+
slug: b.slug,
|
|
75
|
+
name: b.name,
|
|
76
|
+
tagline: b.tagline ?? null,
|
|
77
|
+
palette: b.palette,
|
|
78
|
+
typography: b.typography ?? null,
|
|
79
|
+
cardUrl: b.card?.url ?? null,
|
|
80
|
+
persona: b.persona?.label ?? null,
|
|
81
|
+
})),
|
|
82
|
+
note: "Oficiais (curados) + os seus custom. Pra detalhe completo (voz + estilo " +
|
|
83
|
+
"de imagem): action=get slug=<slug>. Pra criar um novo: action=generate.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// -------- get: brand completo por slug (grátis) --------
|
|
87
|
+
if (args.action === "get") {
|
|
88
|
+
if (!args.slug)
|
|
89
|
+
throw new Error("action=get exige slug (pegue via action=list).");
|
|
90
|
+
const b = await convexQuery("brands:mcpGetBrand", {
|
|
91
|
+
sessionToken,
|
|
92
|
+
slug: args.slug.trim(),
|
|
93
|
+
});
|
|
94
|
+
if (!b) {
|
|
95
|
+
throw new Error(`Brand "${args.slug}" não encontrado (ou é custom de outro user). Veja os seus em action=list.`);
|
|
96
|
+
}
|
|
97
|
+
return b;
|
|
98
|
+
}
|
|
99
|
+
// -------- generate: cria design system novo (cobra Sinapses) --------
|
|
100
|
+
if (args.action === "generate") {
|
|
101
|
+
const description = (args.description || "").trim();
|
|
102
|
+
if (description.length < 30) {
|
|
103
|
+
throw new Error("action=generate exige description com pelo menos 30 chars (vibe, cores, voz, referências). " +
|
|
104
|
+
"Quanto mais rico, melhor o brand. Converse com o user e monte a descrição antes de chamar.");
|
|
105
|
+
}
|
|
106
|
+
const res = await convexAction("customBrandsActions:mcpGenerateBrand", {
|
|
107
|
+
sessionToken,
|
|
108
|
+
description,
|
|
109
|
+
name: args.name?.trim() || undefined,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...res,
|
|
113
|
+
cardNote: res.cardTier === "premium"
|
|
114
|
+
? "Card premium gerado (gpt-image-2)."
|
|
115
|
+
: "Card premium falhou, caiu no pôster SVG (grátis). Pra tentar de novo: action=card slug=" +
|
|
116
|
+
res.slug,
|
|
117
|
+
viewUrl: `${APP}/experimentos/brands`,
|
|
118
|
+
next: "Ajustar: action=refine slug=" +
|
|
119
|
+
res.slug +
|
|
120
|
+
" feedback='...'. Regenerar uma peça: action=reroll slug=" +
|
|
121
|
+
res.slug +
|
|
122
|
+
" piece=voice|palette|imageStyle.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// -------- refine: ajusta brand custom existente (cobra Sinapses) --------
|
|
126
|
+
if (args.action === "refine") {
|
|
127
|
+
if (!args.slug)
|
|
128
|
+
throw new Error("action=refine exige slug do brand custom.");
|
|
129
|
+
const feedback = (args.feedback || "").trim();
|
|
130
|
+
if (feedback.length < 5) {
|
|
131
|
+
throw new Error("action=refine exige feedback (ex: 'fundo mais escuro', 'voz mais seca').");
|
|
132
|
+
}
|
|
133
|
+
const res = await convexAction("customBrandsActions:mcpRefineBrand", {
|
|
134
|
+
sessionToken,
|
|
135
|
+
slug: args.slug.trim(),
|
|
136
|
+
feedback,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
...res,
|
|
140
|
+
viewUrl: `${APP}/experimentos/brands`,
|
|
141
|
+
note: "Brand ajustado. Veja o resultado com action=get slug=" + res.slug,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// -------- reroll: regenera 1 peça numa direção diferente (grátis) --------
|
|
145
|
+
if (args.action === "reroll") {
|
|
146
|
+
if (!args.slug)
|
|
147
|
+
throw new Error("action=reroll exige slug do brand custom.");
|
|
148
|
+
if (!args.piece)
|
|
149
|
+
throw new Error("action=reroll exige piece: voice | palette | imageStyle.");
|
|
150
|
+
const res = await convexAction("customBrandsActions:mcpRerollBrandPiece", {
|
|
151
|
+
sessionToken,
|
|
152
|
+
slug: args.slug.trim(),
|
|
153
|
+
piece: args.piece,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
...res,
|
|
157
|
+
note: "Peça '" +
|
|
158
|
+
args.piece +
|
|
159
|
+
"' regenerada (grátis). Não gostou? Roda de novo. Veja com action=get slug=" +
|
|
160
|
+
res.slug,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// -------- card: (re)gera o card premium (cobra Sinapses) --------
|
|
164
|
+
if (args.action === "card") {
|
|
165
|
+
if (!args.slug)
|
|
166
|
+
throw new Error("action=card exige slug do brand custom.");
|
|
167
|
+
const res = await convexAction("customBrandsActions:mcpGenerateBrandCard", {
|
|
168
|
+
sessionToken,
|
|
169
|
+
slug: args.slug.trim(),
|
|
170
|
+
model: args.model || undefined,
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
...res,
|
|
174
|
+
note: `Card premium gerado (${res.cost} Sinapses). URL: ${res.url}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// -------- delete: apaga brand custom do próprio user --------
|
|
178
|
+
if (args.action === "delete") {
|
|
179
|
+
if (!args.slug)
|
|
180
|
+
throw new Error("action=delete exige slug do brand custom.");
|
|
181
|
+
const res = await convexMutation("brands:mcpDeleteBrand", {
|
|
182
|
+
sessionToken,
|
|
183
|
+
slug: args.slug.trim(),
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
...res,
|
|
187
|
+
note: "Brand apagado. Sources/artigos que apontavam pra ele caem no fallback (sapiens).",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
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
|
@@ -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
|
|
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
|
*/
|
|
@@ -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/studios.js
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
* certo quando ele pede coisa que não está coberta no plugin v1.x.
|
|
9
9
|
*
|
|
10
10
|
* v1.4 vai adicionar wrappers session-token-auth pros estúdios que justificam
|
|
11
|
-
* (Helen Voice TTS, Musicator letras, Sapiens Shorts render, Persona
|
|
11
|
+
* (Helen Voice TTS, Musicator letras, Sapiens Shorts render, Persona Sapiens).
|
|
12
12
|
* Por hora skills usam Claude-side + dashboard URL.
|
|
13
13
|
*/
|
|
14
14
|
export const studiosSchema = z.object({
|
|
@@ -16,7 +16,7 @@ export const studiosSchema = z.object({
|
|
|
16
16
|
studio: z
|
|
17
17
|
.string()
|
|
18
18
|
.optional()
|
|
19
|
-
.describe("Pra action=get: slug do estúdio (ex 'helen-voice', 'musicator', 'persona-
|
|
19
|
+
.describe("Pra action=get: slug do estúdio (ex 'helen-voice', 'musicator', 'persona-sapiens', 'sapiens-shorts')."),
|
|
20
20
|
publishableId: z
|
|
21
21
|
.string()
|
|
22
22
|
.optional()
|
|
@@ -48,15 +48,24 @@ const STUDIOS = {
|
|
|
48
48
|
mcpReady: true,
|
|
49
49
|
mcpNote: "Coberto via sapiens_musicator (lyrics). Render do áudio continua UI-driven (precisa criar track + scheduler).",
|
|
50
50
|
},
|
|
51
|
-
"persona-
|
|
52
|
-
name: "Persona
|
|
53
|
-
description: "
|
|
54
|
-
url: `${APP}/experimentos/
|
|
51
|
+
"persona-sapiens": {
|
|
52
|
+
name: "Persona Sapiens",
|
|
53
|
+
description: "Teste de personalidade pra AUTOCONHECIMENTO (não é gerador de imagem): quiz que cruza MBTI com humores antigos, elementos e estilos. 48 perguntas Likert (~10 min), resultado com persona/animal cultural/sombra/espelho, salvo no perfil do user. Secundário: arte ilustrada dos 16 arquétipos MBTI (full-bleed, Sapiens style).",
|
|
54
|
+
url: `${APP}/experimentos/persona-sapiens`,
|
|
55
55
|
status: "stable",
|
|
56
|
-
tags: ["persona", "mbti", "
|
|
57
|
-
convex: "
|
|
56
|
+
tags: ["persona", "mbti", "quiz", "autoconhecimento", "perfil", "personalidade", "teste"],
|
|
57
|
+
convex: "personalityProfiles",
|
|
58
58
|
mcpReady: true,
|
|
59
|
-
mcpNote: "Coberto via sapiens_persona (generate + list_codes).",
|
|
59
|
+
mcpNote: "Coberto via sapiens_persona. PRIMÁRIO (de graça, qualquer logado): get_quiz + submit_quiz + my_profile — aplica o teste conversando e ALIMENTA o personalityProfile do user. SECUNDÁRIO: generate + list_codes (arte dos arquétipos, 450 Sinapses).",
|
|
60
|
+
},
|
|
61
|
+
"personagem-atlas": {
|
|
62
|
+
name: "Personagem-Atlas",
|
|
63
|
+
description: "Dossiê editorial premium de personagens sintéticos: 10 slides com tipografia refinada, paleta cromática e geração de conteúdo via IA. É construção de PERSONAGEM ficcional, não o teste de personalidade do user (esse é o persona-sapiens).",
|
|
64
|
+
url: `${APP}/experimentos/personagem-atlas`,
|
|
65
|
+
status: "stable",
|
|
66
|
+
tags: ["personagem", "dossie", "editorial", "slides", "atlas", "character"],
|
|
67
|
+
convex: "characterAtlas",
|
|
68
|
+
mcpReady: false,
|
|
60
69
|
},
|
|
61
70
|
"sapiens-shorts": {
|
|
62
71
|
name: "Sapiens Shorts (UGC / Reflexão)",
|
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.11.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
|
},
|