sapiens-mcp 1.14.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -5
- package/dist/tools/image.js +30 -48
- package/dist/tools/meta.js +61 -0
- package/dist/tools/profile.js +101 -0
- package/dist/tools/repertorio.js +16 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { write, writeSchema } from "./tools/write.js";
|
|
|
22
22
|
import { stockAudio, stockAudioSchema } from "./tools/stockAudio.js";
|
|
23
23
|
import { brand, brandSchema } from "./tools/brand.js";
|
|
24
24
|
import { character, characterSchema } from "./tools/character.js";
|
|
25
|
+
import { profile, profileSchema } from "./tools/profile.js";
|
|
25
26
|
import { describeConvexError } from "./convexClient.js";
|
|
26
27
|
const TOOLS = {
|
|
27
28
|
sapiens_pipeline: {
|
|
@@ -30,17 +31,17 @@ const TOOLS = {
|
|
|
30
31
|
handler: pipeline,
|
|
31
32
|
},
|
|
32
33
|
sapiens_image: {
|
|
33
|
-
description: "Operações de imagem via Sapiens (Gemini, Azure, Veo). Sub-actions: 'generate' (gera imagem completa imediato —
|
|
34
|
+
description: "Operações de imagem via Sapiens (Gemini, Azure gpt-image-2, Grok, Veo). Sub-actions: 'generate' (gera imagem completa imediato — prompt+model+aspectRatio+size; suporta mode=edit/variation e MULTI-REFERÊNCIA: combine até 4 imagens como referência numa geração só, igual ao modal 'Selecionar Referência' do web — via referenceImageUrls (sua galeria + Acervo + personagens públicos de sapiens_character) e/ou sourceImageIds (ids da sua galeria); refs valem pros modelos robustos nano-banana-2/gpt-image-2-*/grok-2-image*), 'request_generation' (v1.7, cria APENAS row pendente em generatedImages + debita créditos — pra modelos sapiens-video-* ANTES de sapiens_shorts/sapiens_video; whitelist, rate limit 3/min), 'compose' (v1.8, combina persona+screen via Gemini pra app-demo Shorts; 25 sinapses, rate limit 10/min). generate=image one-shot, request_generation=criar row video, compose=montar start frame app-demo. TEMPLATE (v1.17): passe templateSlug numa generate pra usar um super-prompt travado da casa — o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha estilo+fundo+enquadramento+ref de traço. 'retrato-sapiens-v1' = retrato editorial cartoon de um personagem no grid verde Sapiens (mesma 'mão' dos artigos); sem ref própria, injeta a Helen como âncora de traço (passar referenceImageUrls troca quem aparece). Mutuamente exclusivo com brandSlug.",
|
|
34
35
|
schema: imageSchema,
|
|
35
36
|
handler: image,
|
|
36
37
|
},
|
|
37
38
|
sapiens_meta: {
|
|
38
|
-
description: "Utilitários transversais: login (conecta a conta com o código de sapiensinteticos.com/conectar-claude, salva sessão de 30 dias localmente), logout, whoami (tier user/admin + saldo + email), credits (saldo agregado), subscription (v1.2 — plan + status + saldo por bucket subscription/grants/free + warnings low/critical), formats (schemas por formato), health, app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
|
|
39
|
+
description: "Utilitários transversais: login (conecta a conta com o código de sapiensinteticos.com/conectar-claude, salva sessão de 30 dias localmente), logout, whoami (tier user/admin + saldo + email), credits (saldo agregado), subscription (v1.2 — plan + status + saldo por bucket subscription/grants/free + warnings low/critical), formats (schemas por formato), health (inclui a versão do MCP), version (qual versão do sapiens-mcp está REALMENTE rodando + se é a última do npm; não exige login; use pra saber se o client pegou a versão nova ou ficou preso em cache do npx), app_url (URLs canônicas). Use credits/subscription antes de gerar imagem pra avisar se vai estourar.",
|
|
39
40
|
schema: metaSchema,
|
|
40
41
|
handler: meta,
|
|
41
42
|
},
|
|
42
43
|
sapiens_repertorio: {
|
|
43
|
-
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'),
|
|
44
|
+
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 travada na lista de providers: quando o user fala natural ('acabei de ver Duna 2, nota 9', 'tô jogando Hollow Knight', 'li tal livro'), (1) infira mediaType e status (assisti/zerei/li=completed, quero=backlog, tô jogando/vendo=active, dropei=dropped) e rating se citado; (2) chame action=resolve {mediaType, query}, escolha o candidato certo e faça add_item passando SÓ o source + externalId DELE + os campos pessoais (status/rating/tags/note). O servidor re-resolve no provider e grava título/capa/ano canônicos — você NÃO manda título/capa nem inventa externalId. (3) Se o resolve não achar (lista vazia/providerKeyMissing), NÃO dá pra adicionar: diga ao user que não encontrou nos providers (não fabrique entry manual). Upsert/dedup por (userId, source, externalId). Só pergunte se ambíguo entre candidatos.",
|
|
44
45
|
schema: repertorioSchema,
|
|
45
46
|
handler: repertorio,
|
|
46
47
|
},
|
|
@@ -119,8 +120,13 @@ const TOOLS = {
|
|
|
119
120
|
schema: characterSchema,
|
|
120
121
|
handler: character,
|
|
121
122
|
},
|
|
123
|
+
sapiens_profile: {
|
|
124
|
+
description: "O 'tudo junto' do perfil do user (/u/<username>), leitura user-tier. Agrega o que mora no perfil mas estava fora do MCP: identidade + nível/XP + saldo, badges (conquistas) e golden tools (favoritos do aitag). Sub-actions: get (card completo: identidade + nível + saldo + badges + golden tools), badges (só as conquistas, lista cheia), golden_tools (só os favoritos do aitag, lista cheia). As partes grandes do perfil NÃO são duplicadas aqui, têm tool própria: imagens geradas/publicadas=sapiens_gallery, repertório (filmes/séries/jogos/livros/música)=sapiens_repertorio, personagens=sapiens_character, persona/arquétipo MBTI=sapiens_persona action=my_profile, saldo detalhado por bucket=sapiens_meta action=subscription. Monta a partir de queries já em prod (sem custo).",
|
|
125
|
+
schema: profileSchema,
|
|
126
|
+
handler: profile,
|
|
127
|
+
},
|
|
122
128
|
};
|
|
123
|
-
const server = new Server({ name: "mcp-sapiens", version: "1.
|
|
129
|
+
const server = new Server({ name: "mcp-sapiens", version: "1.17.0" }, { capabilities: { tools: {} } });
|
|
124
130
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
125
131
|
tools: Object.entries(TOOLS).map(([name, t]) => ({
|
|
126
132
|
name,
|
|
@@ -153,4 +159,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
153
159
|
});
|
|
154
160
|
const transport = new StdioServerTransport();
|
|
155
161
|
await server.connect(transport);
|
|
156
|
-
console.error("mcp-sapiens v1.
|
|
162
|
+
console.error("mcp-sapiens v1.17.0 rodando via stdio (20 tools)");
|
package/dist/tools/image.js
CHANGED
|
@@ -44,7 +44,11 @@ export const imageSchema = z.object({
|
|
|
44
44
|
referenceImageUrls: z
|
|
45
45
|
.array(z.string())
|
|
46
46
|
.optional()
|
|
47
|
-
.describe("URLs públicas
|
|
47
|
+
.describe("Até 4 URLs públicas de referência pra combinar numa geração só (character/style lock), igual ao modal 'Selecionar Referência' do gerador web. Fontes: sua galeria (sapiens_gallery, campo url), o Acervo, e personagens públicos (sapiens_character action=list_public → mainImageUrl/imageUrls). Restrito a hosts do Sapiens (Bunny CDN / Convex) + Wikimedia. Requer model com refs: nano-banana-2, gpt-image-2-* ou grok-2-image*. Soma com sourceImageIds (teto total de 4)."),
|
|
48
|
+
sourceImageIds: z
|
|
49
|
+
.array(z.string())
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Até 4 IDs de imagens da SUA galeria (`generatedImages:_id` via sapiens_gallery) usadas como referência. Alternativa por-id ao referenceImageUrls pras suas próprias imagens (ownership checado). Soma com referenceImageUrls (teto total de 4)."),
|
|
48
52
|
brandSlug: z
|
|
49
53
|
.string()
|
|
50
54
|
.optional()
|
|
@@ -53,6 +57,10 @@ export const imageSchema = z.object({
|
|
|
53
57
|
.enum(["persona", "logo", "none"])
|
|
54
58
|
.optional()
|
|
55
59
|
.describe("Marca na imagem do brand: 'persona' (personagem do brand via character sheets, ex: Helen), 'logo' (carimba a logo no canto), 'none' (só o estilo). Default 'none'. Só aplica se brandSlug setado e o brand oferecer a marca."),
|
|
60
|
+
templateSlug: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Slug de um template de imagem (super-prompt travado da casa). Quando setado, o `prompt` vira só a CENA (quem + pose + objeto-conceito) e o template embrulha com o estilo + fundo + enquadramento + ref de traço da casa. Ex: 'retrato-sapiens-v1' = retrato editorial cartoon de UM personagem no grid verde Sapiens (mesma 'mão' dos artigos). O template define model/aspect/size default (sobreponíveis) e injeta a ref da Helen como âncora de traço quando você não passa referenceImageUrls própria (passar refs = trocar quem aparece, mantendo o estilo). Mutuamente exclusivo com brandSlug. Vale em action=generate, mode=create."),
|
|
56
64
|
mode: z
|
|
57
65
|
.enum(["create", "edit", "variation"])
|
|
58
66
|
.optional()
|
|
@@ -127,66 +135,40 @@ export async function image(args) {
|
|
|
127
135
|
// sempre desktopMcp.generateImageOnline a partir da v1 do plugin, porque
|
|
128
136
|
// ele cobre os 3 modos uniformemente e tem a coreografia completa (auth,
|
|
129
137
|
// créditos, upload Bunny, patch row).
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const result = await convexAction("desktopMcp:generateImageOnline", {
|
|
136
|
-
sessionToken,
|
|
137
|
-
prompt: args.prompt,
|
|
138
|
-
aspectRatio: args.aspectRatio,
|
|
139
|
-
size: args.size,
|
|
140
|
-
model: args.model ?? "nano-banana-2",
|
|
141
|
-
negativePrompt: args.negativePrompt,
|
|
142
|
-
mode: args.mode ?? "edit",
|
|
143
|
-
sourceImageId: args.sourceImageId,
|
|
144
|
-
brandSlug: args.brandSlug,
|
|
145
|
-
brandMark: args.brandMark,
|
|
146
|
-
});
|
|
147
|
-
return {
|
|
148
|
-
imageId: result?.imageId,
|
|
149
|
-
url: result?.url,
|
|
150
|
-
mimeType: result?.mimeType,
|
|
151
|
-
// Não incluir base64 no retorno (polui contexto). Quem quiser bytes
|
|
152
|
-
// chama sapiens_gallery action=get com includeBase64=true.
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
// Create COM referências externas (URLs): pipelineMcpImage (aceita URLs).
|
|
156
|
-
// Admin-only por enquanto. O caso comum (sem refs externas) cai no caminho
|
|
157
|
-
// user-tier abaixo; a persona do brand NÃO precisa de refs externas (são
|
|
158
|
-
// buscadas server-side a partir do próprio brand).
|
|
159
|
-
if (args.referenceImageUrls && args.referenceImageUrls.length > 0) {
|
|
160
|
-
return await convexAction("pipelineMcpImage:mcpGenerateImage", {
|
|
161
|
-
sessionToken,
|
|
162
|
-
prompt: args.prompt,
|
|
163
|
-
model: args.model ?? "nano-banana-2",
|
|
164
|
-
aspectRatio: args.aspectRatio,
|
|
165
|
-
size: args.size,
|
|
166
|
-
styleId: args.styleId,
|
|
167
|
-
negativePrompt: args.negativePrompt,
|
|
168
|
-
referenceImageUrls: args.referenceImageUrls,
|
|
169
|
-
brandSlug: args.brandSlug,
|
|
170
|
-
brandMark: args.brandMark,
|
|
171
|
-
});
|
|
138
|
+
const hasRefs = !!args.sourceImageId ||
|
|
139
|
+
(args.sourceImageIds?.length ?? 0) > 0 ||
|
|
140
|
+
(args.referenceImageUrls?.length ?? 0) > 0;
|
|
141
|
+
if ((args.mode === "edit" || args.mode === "variation") && !hasRefs) {
|
|
142
|
+
throw new Error(`mode='${args.mode}' exige referência: sourceImageId, sourceImageIds ou referenceImageUrls. Use sapiens_gallery / sapiens_character pra descobrir.`);
|
|
172
143
|
}
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
144
|
+
// Caminho ÚNICO user-tier (desktopMcp.generateImageOnline). Cobre create,
|
|
145
|
+
// edit, variation e multi-ref (galeria por id + URLs públicas do Sapiens),
|
|
146
|
+
// resolvendo as referências server-side e cobrando do dono do token. O brand
|
|
147
|
+
// (estilo + persona via refs server-side + logo) aplica dentro do
|
|
148
|
+
// generateImageAction, igual ao gerador web. Substitui o antigo branch
|
|
149
|
+
// referenceImageUrls→pipelineMcpImage (que era admin-only).
|
|
176
150
|
const result = await convexAction("desktopMcp:generateImageOnline", {
|
|
177
151
|
sessionToken,
|
|
178
152
|
prompt: args.prompt,
|
|
179
153
|
aspectRatio: args.aspectRatio,
|
|
180
154
|
size: args.size,
|
|
181
|
-
|
|
155
|
+
// Com templateSlug, deixa o model em branco pro default do template valer
|
|
156
|
+
// (sem template, mantém o default nano-banana-2 da tool).
|
|
157
|
+
model: args.model ?? (args.templateSlug ? undefined : "nano-banana-2"),
|
|
182
158
|
negativePrompt: args.negativePrompt,
|
|
183
|
-
mode: "create",
|
|
159
|
+
mode: args.mode ?? "create",
|
|
160
|
+
sourceImageId: args.sourceImageId,
|
|
161
|
+
sourceImageIds: args.sourceImageIds,
|
|
162
|
+
referenceImageUrls: args.referenceImageUrls,
|
|
184
163
|
brandSlug: args.brandSlug,
|
|
185
164
|
brandMark: args.brandMark,
|
|
165
|
+
templateSlug: args.templateSlug,
|
|
186
166
|
});
|
|
187
167
|
return {
|
|
188
168
|
imageId: result?.imageId,
|
|
189
169
|
url: result?.url,
|
|
190
170
|
mimeType: result?.mimeType,
|
|
171
|
+
// Não incluir base64 no retorno (polui contexto). Quem quiser bytes
|
|
172
|
+
// chama sapiens_gallery action=get com includeBase64=true.
|
|
191
173
|
};
|
|
192
174
|
}
|
package/dist/tools/meta.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { convexQuery, convexMutation, convexAction, getSessionToken, saveSessionToken, clearSessionToken, describeConvexError, } from "../convexClient.js";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Lê o `version` do package.json do PRÓPRIO binário em runtime. Reflete o que
|
|
9
|
+
* está REALMENTE rodando (não um valor cravado no código nem server-side), então
|
|
10
|
+
* serve pra flagrar client preso em cache antigo do npx. dist/tools/meta.js ->
|
|
11
|
+
* ../../package.json (raiz do pacote); em dev (src/tools) o caminho bate igual.
|
|
12
|
+
*/
|
|
13
|
+
function readMcpVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
17
|
+
return typeof pkg?.version === "string" ? pkg.version : "unknown";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
3
23
|
export const metaSchema = z.object({
|
|
4
24
|
action: z.enum([
|
|
5
25
|
"login",
|
|
@@ -10,6 +30,7 @@ export const metaSchema = z.object({
|
|
|
10
30
|
"credits",
|
|
11
31
|
"app_url",
|
|
12
32
|
"subscription",
|
|
33
|
+
"version",
|
|
13
34
|
]),
|
|
14
35
|
code: z
|
|
15
36
|
.string()
|
|
@@ -123,6 +144,45 @@ export async function meta(args) {
|
|
|
123
144
|
adminPipeline: `${APP_URL}/dashboard/admin/content`,
|
|
124
145
|
};
|
|
125
146
|
}
|
|
147
|
+
// version: qual versão do sapiens-mcp está rodando AGORA (lida do package.json
|
|
148
|
+
// do binário) + compara com a última do npm (best-effort, com timeout). Não
|
|
149
|
+
// exige login: é sobre o binário, não a conta. É o teste limpo pra saber se o
|
|
150
|
+
// client pegou a versão nova ou ficou preso em cache do npx.
|
|
151
|
+
if (args.action === "version") {
|
|
152
|
+
const version = readMcpVersion();
|
|
153
|
+
let latest = null;
|
|
154
|
+
let upToDate = null;
|
|
155
|
+
try {
|
|
156
|
+
const ctrl = new AbortController();
|
|
157
|
+
const timer = setTimeout(() => ctrl.abort(), 2500);
|
|
158
|
+
const resp = await fetch("https://registry.npmjs.org/sapiens-mcp/latest", {
|
|
159
|
+
signal: ctrl.signal,
|
|
160
|
+
});
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
if (resp.ok) {
|
|
163
|
+
const data = await resp.json();
|
|
164
|
+
if (typeof data?.version === "string") {
|
|
165
|
+
latest = data.version;
|
|
166
|
+
upToDate = latest === version;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// offline / timeout: reporta só a versão local (ainda útil)
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
name: "sapiens-mcp",
|
|
175
|
+
version,
|
|
176
|
+
latest,
|
|
177
|
+
upToDate,
|
|
178
|
+
runtime: `node ${process.version}`,
|
|
179
|
+
note: latest && upToDate === false
|
|
180
|
+
? `Rodando ${version}, mas a última no npm é ${latest}: o client está ATRASADO (provável cache do npx). Pra atualizar: pinar sapiens-mcp@${latest} na config do MCP ou recriar o container (restart sozinho não basta).`
|
|
181
|
+
: latest && upToDate
|
|
182
|
+
? `Na última versão (${version}).`
|
|
183
|
+
: `Versão do binário rodando agora: ${version}. Não consegui consultar o npm pra comparar (offline/timeout).`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
126
186
|
// Login: troca o código de uso único (gerado no site, logado) por um
|
|
127
187
|
// sessionToken de 30 dias e salva localmente. NÃO exige token prévio.
|
|
128
188
|
if (args.action === "login") {
|
|
@@ -243,6 +303,7 @@ export async function meta(args) {
|
|
|
243
303
|
}
|
|
244
304
|
return {
|
|
245
305
|
ok: true,
|
|
306
|
+
version: readMcpVersion(),
|
|
246
307
|
tier: isAdmin ? "admin" : "user",
|
|
247
308
|
email: sub?.user?.email ?? null,
|
|
248
309
|
balance: sub?.balance ?? null,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexQuery, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* sapiens_profile — o "tudo junto" do perfil do user (/u/<username>), leitura
|
|
5
|
+
* user-tier. Agrega o que mora no perfil mas estava espalhado/fora do MCP:
|
|
6
|
+
* identidade + nível/XP, badges (conquistas) e golden tools (favoritos do aitag).
|
|
7
|
+
*
|
|
8
|
+
* Monta a partir de queries que JÁ existem em prod (sem deploy novo), igual ao
|
|
9
|
+
* roster da comunidade: getDesktopUser resolve o user pelo token, e badges /
|
|
10
|
+
* favoritos são queries públicas por userId.
|
|
11
|
+
*
|
|
12
|
+
* As partes "grandes" do perfil têm tool própria e não são duplicadas aqui:
|
|
13
|
+
* - imagens geradas/publicadas → sapiens_gallery
|
|
14
|
+
* - repertório (filmes/séries/jogos/livros/música) → sapiens_repertorio
|
|
15
|
+
* - personagens → sapiens_character
|
|
16
|
+
* - persona/arquétipo MBTI → sapiens_persona action=my_profile
|
|
17
|
+
* - saldo detalhado por bucket → sapiens_meta action=subscription
|
|
18
|
+
*
|
|
19
|
+
* Sub-actions:
|
|
20
|
+
* - get: card completo (identidade + nível + saldo + badges + golden tools).
|
|
21
|
+
* - badges: só as conquistas (lista cheia).
|
|
22
|
+
* - golden_tools: só os favoritos do aitag (lista cheia).
|
|
23
|
+
*/
|
|
24
|
+
export const profileSchema = z.object({
|
|
25
|
+
action: z.enum(["get", "badges", "golden_tools"]),
|
|
26
|
+
});
|
|
27
|
+
const APP = "https://sapiensinteticos.com";
|
|
28
|
+
async function resolveUser(sessionToken) {
|
|
29
|
+
const user = await convexQuery("desktopAuth:getDesktopUser", { sessionToken });
|
|
30
|
+
if (!user || !user._id) {
|
|
31
|
+
throw new Error("Sessão Sapiens inválida ou expirada. Rode sapiens_meta action=login pra reconectar.");
|
|
32
|
+
}
|
|
33
|
+
return user;
|
|
34
|
+
}
|
|
35
|
+
function mapBadges(rows) {
|
|
36
|
+
return (rows || []).map((b) => ({
|
|
37
|
+
name: b?.badge?.name ?? null,
|
|
38
|
+
description: b?.badge?.description ?? null,
|
|
39
|
+
xpReward: b?.badge?.xpReward ?? null,
|
|
40
|
+
earnedAt: b?.earnedAt ?? b?.awardedAt ?? b?._creationTime ?? null,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function mapTools(rows) {
|
|
44
|
+
return (rows || []).filter(Boolean).map((t) => ({
|
|
45
|
+
name: t?.name ?? null,
|
|
46
|
+
slug: t?.slug ?? null,
|
|
47
|
+
category: t?.category ?? t?.categorySlug ?? null,
|
|
48
|
+
url: t?.websiteUrl ?? t?.url ?? null,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
export async function profile(args) {
|
|
52
|
+
const sessionToken = getSessionToken();
|
|
53
|
+
const user = await resolveUser(sessionToken);
|
|
54
|
+
const userId = user._id;
|
|
55
|
+
if (args.action === "badges") {
|
|
56
|
+
const rows = await convexQuery("gamification:getUserBadges", { userId });
|
|
57
|
+
const badges = mapBadges(rows);
|
|
58
|
+
return { count: badges.length, badges };
|
|
59
|
+
}
|
|
60
|
+
if (args.action === "golden_tools") {
|
|
61
|
+
const rows = await convexQuery("aitag/favorites:listFavoritesByUserId", {
|
|
62
|
+
userId,
|
|
63
|
+
});
|
|
64
|
+
const tools = mapTools(rows);
|
|
65
|
+
return {
|
|
66
|
+
count: tools.length,
|
|
67
|
+
tools,
|
|
68
|
+
note: "Favoritos do aitag (golden tools). Marque/desmarque pelo aitag.app.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// get: card completo
|
|
72
|
+
const [badgeRows, toolRows] = await Promise.all([
|
|
73
|
+
convexQuery("gamification:getUserBadges", { userId }),
|
|
74
|
+
convexQuery("aitag/favorites:listFavoritesByUserId", { userId }),
|
|
75
|
+
]);
|
|
76
|
+
const badges = mapBadges(badgeRows);
|
|
77
|
+
const tools = mapTools(toolRows);
|
|
78
|
+
return {
|
|
79
|
+
identity: {
|
|
80
|
+
username: user.username ?? null,
|
|
81
|
+
name: user.name ?? null,
|
|
82
|
+
avatarUrl: user.avatarUrl ?? user.image ?? null,
|
|
83
|
+
level: user.level ?? null,
|
|
84
|
+
xp: user.xp ?? null,
|
|
85
|
+
isPremium: user.isPremium === true,
|
|
86
|
+
profileUrl: user.username ? `${APP}/u/${user.username}` : null,
|
|
87
|
+
},
|
|
88
|
+
credits: typeof user.totalCredits === "number" ? user.totalCredits : null,
|
|
89
|
+
badges: {
|
|
90
|
+
count: badges.length,
|
|
91
|
+
items: badges.slice(0, 30),
|
|
92
|
+
},
|
|
93
|
+
goldenTools: {
|
|
94
|
+
count: tools.length,
|
|
95
|
+
items: tools.slice(0, 30),
|
|
96
|
+
},
|
|
97
|
+
note: "Card do perfil: identidade + nível + saldo + badges + golden tools (favoritos do aitag). " +
|
|
98
|
+
"O resto do perfil tem tool própria: imagens=sapiens_gallery, repertório=sapiens_repertorio, " +
|
|
99
|
+
"personagens=sapiens_character, persona/MBTI=sapiens_persona action=my_profile.",
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/tools/repertorio.js
CHANGED
|
@@ -11,6 +11,14 @@ import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../
|
|
|
11
11
|
* PRÓPRIO acervo. Ownership: tudo vai pro userId da sessão (o user logado).
|
|
12
12
|
* Caller NÃO escolhe pra quem adicionar.
|
|
13
13
|
*
|
|
14
|
+
* add_item é TRAVADO na lista de providers (anti-fabricação): você passa só
|
|
15
|
+
* mediaType + source + externalId vindos de um `resolve` anterior + campos
|
|
16
|
+
* pessoais (status/rating/tags/note/isPublic). O servidor RE-RESOLVE no
|
|
17
|
+
* provider por id e grava o metadado canônico (título/capa/ano). Não dá pra
|
|
18
|
+
* inventar título/capa nem criar entry "manual" à mão — externalId que não
|
|
19
|
+
* resolve é rejeitado. Fluxo: resolve → escolhe candidato → add_item com o
|
|
20
|
+
* source+externalId dele.
|
|
21
|
+
*
|
|
14
22
|
* Action-based design (memoria: consolidar tools via args).
|
|
15
23
|
*/
|
|
16
24
|
const mediaTypeEnum = z.enum(["movie", "series", "anime", "game", "book", "music"]);
|
|
@@ -47,14 +55,14 @@ export const repertorioSchema = z.object({
|
|
|
47
55
|
mediaType: mediaTypeEnum.optional(),
|
|
48
56
|
status: statusEnum.optional(),
|
|
49
57
|
limit: z.number().int().positive().max(500).optional().default(100),
|
|
50
|
-
// add_item —
|
|
58
|
+
// add_item — só identifica o item; o servidor re-resolve e grava o metadado.
|
|
51
59
|
source: sourceEnum
|
|
52
60
|
.optional()
|
|
53
|
-
.describe("Pra add_item:
|
|
61
|
+
.describe("Pra add_item: o source EXATO de um candidato do action=resolve (manual=filme/série via OMDb, anilist, twitch=jogo, googlebooks, itunes). Não invente."),
|
|
54
62
|
externalId: z
|
|
55
63
|
.string()
|
|
56
64
|
.optional()
|
|
57
|
-
.describe("Pra add_item:
|
|
65
|
+
.describe("Pra add_item: o externalId EXATO do candidato do resolve (imdbID p/ manual, id do provider p/ resto). NÃO fabrique/UUID — id que não resolve no provider é rejeitado. Dedup por (userId, source, externalId)."),
|
|
58
66
|
title: z.string().optional(),
|
|
59
67
|
titleOriginal: z.string().optional(),
|
|
60
68
|
year: z.number().optional(),
|
|
@@ -127,33 +135,20 @@ export async function repertorio(args) {
|
|
|
127
135
|
args.action === "remove_item") {
|
|
128
136
|
const sessionToken = getSessionToken();
|
|
129
137
|
if (args.action === "add_item") {
|
|
130
|
-
|
|
138
|
+
// Travado na lista de providers: manda só o identificador (source +
|
|
139
|
+
// externalId de um resolve) + campos pessoais. O servidor re-resolve no
|
|
140
|
+
// provider e grava título/capa/ano canônicos; título/capa do caller são
|
|
141
|
+
// ignorados de propósito (anti-fabricação).
|
|
142
|
+
return await convexAction("repertorioResolve:addResolvedItem", {
|
|
131
143
|
sessionToken,
|
|
132
144
|
mediaType: need(args.mediaType, "mediaType"),
|
|
133
145
|
source: need(args.source, "source"),
|
|
134
146
|
externalId: need(args.externalId, "externalId"),
|
|
135
|
-
title: need(args.title, "title"),
|
|
136
|
-
titleOriginal: args.titleOriginal,
|
|
137
|
-
year: args.year,
|
|
138
|
-
posterUrl: args.posterUrl,
|
|
139
|
-
backdropUrl: args.backdropUrl,
|
|
140
|
-
overview: args.overview,
|
|
141
|
-
genres: args.genres,
|
|
142
|
-
runtimeMin: args.runtimeMin,
|
|
143
|
-
platforms: args.platforms,
|
|
144
|
-
studios: args.studios,
|
|
145
|
-
authors: args.authors,
|
|
146
|
-
pageCount: args.pageCount,
|
|
147
|
-
isbn: args.isbn,
|
|
148
147
|
status: args.status,
|
|
149
148
|
rating: args.rating === null ? undefined : args.rating,
|
|
150
149
|
tags: args.tags,
|
|
151
150
|
note: args.note,
|
|
152
151
|
isPublic: args.isPublic,
|
|
153
|
-
imdbId: args.imdbId,
|
|
154
|
-
tmdbId: args.tmdbId,
|
|
155
|
-
traktId: args.traktId,
|
|
156
|
-
anilistId: args.anilistId,
|
|
157
152
|
});
|
|
158
153
|
}
|
|
159
154
|
if (args.action === "update_item") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapiens-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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": {
|