sapiens-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/convexClient.js +167 -0
- package/dist/index.js +137 -0
- package/dist/tools/article.js +155 -0
- package/dist/tools/community.js +74 -0
- package/dist/tools/gallery.js +57 -0
- package/dist/tools/helen.js +120 -0
- package/dist/tools/image.js +145 -0
- package/dist/tools/meta.js +241 -0
- package/dist/tools/musicator.js +77 -0
- package/dist/tools/persona.js +87 -0
- package/dist/tools/pipeline.js +294 -0
- package/dist/tools/quotePop.js +175 -0
- package/dist/tools/repertorio.js +217 -0
- package/dist/tools/search.js +60 -0
- package/dist/tools/shorts.js +79 -0
- package/dist/tools/studios.js +169 -0
- package/dist/tools/video.js +55 -0
- package/dist/tools/write.js +132 -0
- package/package.json +44 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexAction, convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
|
|
3
|
+
const FORMATS = [
|
|
4
|
+
"tirinha",
|
|
5
|
+
"carrossel_ig",
|
|
6
|
+
"post_social",
|
|
7
|
+
"musica",
|
|
8
|
+
"mega_grafico",
|
|
9
|
+
"post_linkedin",
|
|
10
|
+
"post_twitter",
|
|
11
|
+
"post_threads",
|
|
12
|
+
"shorts_yt",
|
|
13
|
+
"cena_visual",
|
|
14
|
+
"video_yt",
|
|
15
|
+
];
|
|
16
|
+
const STATUSES = ["draft", "ready", "finalized"];
|
|
17
|
+
export const pipelineSchema = z.object({
|
|
18
|
+
action: z.enum([
|
|
19
|
+
"list_sources",
|
|
20
|
+
"list_articles",
|
|
21
|
+
"get_source",
|
|
22
|
+
"get_production",
|
|
23
|
+
"list_versions",
|
|
24
|
+
"add_article_as_source",
|
|
25
|
+
"create_draft_article_and_source",
|
|
26
|
+
"create_production",
|
|
27
|
+
"update_production",
|
|
28
|
+
"finalize_production",
|
|
29
|
+
"remove_production",
|
|
30
|
+
"remove_source",
|
|
31
|
+
"set_source_done",
|
|
32
|
+
"update_source_notes",
|
|
33
|
+
"restore_version",
|
|
34
|
+
"set_publishable_title",
|
|
35
|
+
"backfill_via",
|
|
36
|
+
"propose_mega_grafico_plan",
|
|
37
|
+
"run_mega_grafico_full",
|
|
38
|
+
]),
|
|
39
|
+
// propose_mega_grafico_plan / run_mega_grafico_full
|
|
40
|
+
withHelen: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Se TRUE, plano reserva 1 painel pra Helen Ailith interagindo com o tema (cartoon editorial). Se FALSE, poster 100% diagramático sem figura humana. Pergunte ao user antes de definir."),
|
|
44
|
+
// run_mega_grafico_full
|
|
45
|
+
skipFinalize: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Se TRUE, gera plano + imagem + branding + salva payload mas NÃO cria publishable (deixa production em 'ready' pro admin revisar). Default false."),
|
|
49
|
+
skipBranding: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Se TRUE, pula o composite do selo Sapiens + tEXt chunks. Use só quando o Convex storage está fora ou pra debug. Default false."),
|
|
53
|
+
templateSlug: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Slug do template de receita (admin edita em /dashboard/admin/image-templates). Se omitido, usa o template marcado isDefault=true pro formato. Ex: 'mega-grafico-blueprint-v1'."),
|
|
57
|
+
specOverride: z
|
|
58
|
+
.object({
|
|
59
|
+
model: z.string().optional(),
|
|
60
|
+
aspectRatio: z.string().optional(),
|
|
61
|
+
size: z.string().optional(),
|
|
62
|
+
})
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Override de model/aspectRatio/size sobre os defaults do template. Cada um valida contra o allowedX do template; se sair do whitelist, action throws."),
|
|
65
|
+
// list_articles
|
|
66
|
+
includeDrafts: z.boolean().optional(),
|
|
67
|
+
includeArchived: z.boolean().optional(),
|
|
68
|
+
onlyAvailable: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Se true, só artigos que ainda NÃO foram virados em source"),
|
|
72
|
+
// por id
|
|
73
|
+
sourceId: z.string().optional(),
|
|
74
|
+
productionId: z.string().optional(),
|
|
75
|
+
publishableId: z.string().optional(),
|
|
76
|
+
articleId: z.string().optional(),
|
|
77
|
+
// create / add
|
|
78
|
+
format: z.enum(FORMATS).optional(),
|
|
79
|
+
payload: z.any().optional().describe("Payload livre por formato"),
|
|
80
|
+
status: z.enum(STATUSES).optional(),
|
|
81
|
+
notes: z.string().optional(),
|
|
82
|
+
// create_draft_article_and_source
|
|
83
|
+
title: z.string().optional(),
|
|
84
|
+
slug: z.string().optional(),
|
|
85
|
+
excerpt: z.string().optional(),
|
|
86
|
+
tldr: z.string().optional(),
|
|
87
|
+
content: z.string().optional(),
|
|
88
|
+
category: z.string().optional(),
|
|
89
|
+
tags: z.array(z.string()).optional(),
|
|
90
|
+
thumbnailUrl: z.string().optional(),
|
|
91
|
+
// finalize_production
|
|
92
|
+
assets: z
|
|
93
|
+
.array(z.object({
|
|
94
|
+
kind: z.string(),
|
|
95
|
+
url: z.string().optional(),
|
|
96
|
+
text: z.string().optional(),
|
|
97
|
+
meta: z.any().optional(),
|
|
98
|
+
}))
|
|
99
|
+
.optional(),
|
|
100
|
+
caption: z.string().optional(),
|
|
101
|
+
hashtags: z.array(z.string()).optional(),
|
|
102
|
+
// set_source_done
|
|
103
|
+
isDone: z.boolean().optional(),
|
|
104
|
+
// backfill_via
|
|
105
|
+
sinceCreatedAt: z.number().optional(),
|
|
106
|
+
beforeCreatedAt: z.number().optional(),
|
|
107
|
+
via: z
|
|
108
|
+
.string()
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Ex: 'claude-mcp', 'modo-antigo', 'manual'"),
|
|
111
|
+
dryRun: z.boolean().optional(),
|
|
112
|
+
});
|
|
113
|
+
function need(value, name) {
|
|
114
|
+
if (value === undefined || value === null) {
|
|
115
|
+
throw new Error(`Faltando arg "${name}" pra essa action.`);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
export async function pipeline(args) {
|
|
120
|
+
const sessionToken = getSessionToken();
|
|
121
|
+
switch (args.action) {
|
|
122
|
+
case "list_sources":
|
|
123
|
+
return await convexQuery("pipelineMcp:mcpListSources", { sessionToken });
|
|
124
|
+
case "list_articles":
|
|
125
|
+
return await convexQuery("pipelineMcp:mcpListArticles", {
|
|
126
|
+
sessionToken,
|
|
127
|
+
includeDrafts: args.includeDrafts,
|
|
128
|
+
includeArchived: args.includeArchived,
|
|
129
|
+
onlyAvailable: args.onlyAvailable,
|
|
130
|
+
});
|
|
131
|
+
case "get_source":
|
|
132
|
+
return await convexQuery("pipelineMcp:mcpGetSource", {
|
|
133
|
+
sessionToken,
|
|
134
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
135
|
+
});
|
|
136
|
+
case "get_production":
|
|
137
|
+
return await convexQuery("pipelineMcp:mcpGetProduction", {
|
|
138
|
+
sessionToken,
|
|
139
|
+
productionId: need(args.productionId, "productionId"),
|
|
140
|
+
});
|
|
141
|
+
case "list_versions":
|
|
142
|
+
return await convexQuery("pipelineMcp:mcpListPublishableVersions", {
|
|
143
|
+
sessionToken,
|
|
144
|
+
productionId: need(args.productionId, "productionId"),
|
|
145
|
+
});
|
|
146
|
+
case "add_article_as_source": {
|
|
147
|
+
const id = await convexMutation("pipelineMcp:mcpAddArticleAsSource", {
|
|
148
|
+
sessionToken,
|
|
149
|
+
articleId: need(args.articleId, "articleId"),
|
|
150
|
+
notes: args.notes,
|
|
151
|
+
});
|
|
152
|
+
return { sourceId: id };
|
|
153
|
+
}
|
|
154
|
+
case "create_draft_article_and_source": {
|
|
155
|
+
const result = await convexMutation("pipelineMcp:mcpCreateDraftArticleAndSource", {
|
|
156
|
+
sessionToken,
|
|
157
|
+
title: need(args.title, "title"),
|
|
158
|
+
slug: need(args.slug, "slug"),
|
|
159
|
+
excerpt: args.excerpt,
|
|
160
|
+
tldr: args.tldr,
|
|
161
|
+
content: need(args.content, "content"),
|
|
162
|
+
category: args.category,
|
|
163
|
+
tags: args.tags,
|
|
164
|
+
thumbnailUrl: args.thumbnailUrl,
|
|
165
|
+
});
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
case "create_production": {
|
|
169
|
+
const id = await convexMutation("pipelineMcp:mcpCreateProductionDraft", {
|
|
170
|
+
sessionToken,
|
|
171
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
172
|
+
format: need(args.format, "format"),
|
|
173
|
+
payload: args.payload,
|
|
174
|
+
});
|
|
175
|
+
return { productionId: id };
|
|
176
|
+
}
|
|
177
|
+
case "update_production":
|
|
178
|
+
await convexMutation("pipelineMcp:mcpUpdateProductionPayload", {
|
|
179
|
+
sessionToken,
|
|
180
|
+
productionId: need(args.productionId, "productionId"),
|
|
181
|
+
payload: need(args.payload, "payload"),
|
|
182
|
+
status: args.status,
|
|
183
|
+
});
|
|
184
|
+
return { ok: true };
|
|
185
|
+
case "finalize_production": {
|
|
186
|
+
const id = await convexMutation("pipelineMcp:mcpFinalizeProduction", {
|
|
187
|
+
sessionToken,
|
|
188
|
+
productionId: need(args.productionId, "productionId"),
|
|
189
|
+
assets: need(args.assets, "assets"),
|
|
190
|
+
caption: args.caption,
|
|
191
|
+
hashtags: args.hashtags,
|
|
192
|
+
title: args.title,
|
|
193
|
+
});
|
|
194
|
+
return { publishableId: id };
|
|
195
|
+
}
|
|
196
|
+
case "set_publishable_title":
|
|
197
|
+
await convexMutation("pipelineMcp:mcpSetPublishableTitle", {
|
|
198
|
+
sessionToken,
|
|
199
|
+
publishableId: need(args.publishableId, "publishableId"),
|
|
200
|
+
title: need(args.title, "title"),
|
|
201
|
+
});
|
|
202
|
+
return { ok: true };
|
|
203
|
+
case "backfill_via":
|
|
204
|
+
return await convexMutation("pipelineMcp:mcpBackfillVia", {
|
|
205
|
+
sessionToken,
|
|
206
|
+
sinceCreatedAt: args.sinceCreatedAt,
|
|
207
|
+
beforeCreatedAt: args.beforeCreatedAt,
|
|
208
|
+
via: need(args.via, "via"),
|
|
209
|
+
dryRun: args.dryRun,
|
|
210
|
+
});
|
|
211
|
+
case "remove_production":
|
|
212
|
+
await convexMutation("pipelineMcp:mcpRemoveProduction", {
|
|
213
|
+
sessionToken,
|
|
214
|
+
productionId: need(args.productionId, "productionId"),
|
|
215
|
+
});
|
|
216
|
+
return { ok: true };
|
|
217
|
+
case "remove_source":
|
|
218
|
+
await convexMutation("pipelineMcp:mcpRemoveSource", {
|
|
219
|
+
sessionToken,
|
|
220
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
221
|
+
});
|
|
222
|
+
return { ok: true };
|
|
223
|
+
case "set_source_done":
|
|
224
|
+
await convexMutation("pipelineMcp:mcpSetSourceDone", {
|
|
225
|
+
sessionToken,
|
|
226
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
227
|
+
isDone: need(args.isDone, "isDone"),
|
|
228
|
+
});
|
|
229
|
+
return { ok: true };
|
|
230
|
+
case "update_source_notes":
|
|
231
|
+
await convexMutation("pipelineMcp:mcpUpdateSourceNotes", {
|
|
232
|
+
sessionToken,
|
|
233
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
234
|
+
notes: need(args.notes, "notes"),
|
|
235
|
+
});
|
|
236
|
+
return { ok: true };
|
|
237
|
+
case "restore_version": {
|
|
238
|
+
const productionId = await convexMutation("pipelineMcp:mcpRestoreVersion", {
|
|
239
|
+
sessionToken,
|
|
240
|
+
publishableId: need(args.publishableId, "publishableId"),
|
|
241
|
+
});
|
|
242
|
+
return { productionId };
|
|
243
|
+
}
|
|
244
|
+
case "propose_mega_grafico_plan": {
|
|
245
|
+
// Gemini propõe um plano denso de mega gráfico a partir do source
|
|
246
|
+
// (artigo ou brief). Devolve fullPrompt + spec recomendada (vem do
|
|
247
|
+
// template) e referenceImageUrls se withHelen=true. templateSlug e
|
|
248
|
+
// specOverride respeitam o que o admin configurou em
|
|
249
|
+
// /dashboard/admin/image-templates.
|
|
250
|
+
// Use APENAS quando quiser controle granular (revisar plano antes de
|
|
251
|
+
// gerar imagem). Pra fluxo end-to-end, prefira run_mega_grafico_full
|
|
252
|
+
// que faz tudo numa chamada só.
|
|
253
|
+
const result = await convexAction("megaGraficoActions:mcpProposeMegaGraficoPlan", {
|
|
254
|
+
sessionToken,
|
|
255
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
256
|
+
withHelen: args.withHelen ?? false,
|
|
257
|
+
templateSlug: args.templateSlug,
|
|
258
|
+
specOverride: args.specOverride,
|
|
259
|
+
});
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
case "run_mega_grafico_full": {
|
|
263
|
+
// One-shot do fluxo mega gráfico inteiro: cria production (ou reusa
|
|
264
|
+
// se productionId passado), propõe plano com guard de qualidade (1
|
|
265
|
+
// retry se fraco), gera imagem via gpt-image-2-high, aplica selo
|
|
266
|
+
// Sapiens (logo + PNG tEXt chunks), salva payload, e finaliza como
|
|
267
|
+
// publishable (skipFinalize=true pula essa etapa).
|
|
268
|
+
//
|
|
269
|
+
// Custo: ~900 sinapses da geração de imagem + 0-100 sinapses Gemini
|
|
270
|
+
// text. Total ~900-1000.
|
|
271
|
+
//
|
|
272
|
+
// ANTES de chamar: pergunte ao user se withHelen=true (Helen
|
|
273
|
+
// interage com tema, ~15-25% do poster) ou false (poster 100%
|
|
274
|
+
// diagramático, sem humanos).
|
|
275
|
+
//
|
|
276
|
+
// Receita usada: o templateSlug do payload (admin edita em
|
|
277
|
+
// /dashboard/admin/image-templates). Sem templateSlug, usa o default.
|
|
278
|
+
// specOverride permite trocar model/aspect/size dentro do whitelist
|
|
279
|
+
// do template (ex: aspectRatio="1:1" pra IG, "16:9" pra og:image).
|
|
280
|
+
const result = await convexAction("megaGraficoRunner:mcpRunMegaGraficoFull", {
|
|
281
|
+
sessionToken,
|
|
282
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
283
|
+
withHelen: args.withHelen ?? false,
|
|
284
|
+
productionId: args.productionId,
|
|
285
|
+
skipFinalize: args.skipFinalize,
|
|
286
|
+
skipBranding: args.skipBranding,
|
|
287
|
+
caption: args.caption,
|
|
288
|
+
templateSlug: args.templateSlug,
|
|
289
|
+
specOverride: args.specOverride,
|
|
290
|
+
});
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexMutation, getSessionToken } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Publish curado pra Coluna Sapiens (quote) e Coluna Repertório (pop-article)
|
|
5
|
+
* via session token. Substitui:
|
|
6
|
+
* - apps/sapiens/scripts/sapiens-quote-publish.js
|
|
7
|
+
* - apps/sapiens/scripts/sapiens-pop-article-publish.js
|
|
8
|
+
*
|
|
9
|
+
* Antes esses scripts usavam setAdminAuth (CONVEX_ADMIN_KEY). Agora o MCP
|
|
10
|
+
* publica direto via `mcpExtras.mcpPublishQuote` / `mcpPublishPopArticle`,
|
|
11
|
+
* que valida o sessionToken e exige user admin. Mesma garantia de segurança,
|
|
12
|
+
* setup zero pro user (não precisa mais ter CONVEX_ADMIN_KEY no env).
|
|
13
|
+
*
|
|
14
|
+
* Sub-actions:
|
|
15
|
+
* - publish_quote: cria entry na Coluna Sapiens. Default status="draft"
|
|
16
|
+
* (admin revisa em /dashboard/admin/blog/sapiens-curation). publishNow=true
|
|
17
|
+
* pula direto pra published.
|
|
18
|
+
* - publish_pop: cria entry na Coluna Repertório (artigo longo com obra
|
|
19
|
+
* do acervo como lente). column="repertorio", format="pop-article".
|
|
20
|
+
*/
|
|
21
|
+
const quoteImageRef = z.object({
|
|
22
|
+
url: z.string(),
|
|
23
|
+
alt: z.string(),
|
|
24
|
+
caption: z.string().optional(),
|
|
25
|
+
credit: z.string().optional(),
|
|
26
|
+
license: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
const quotePayload = z.object({
|
|
29
|
+
text: z.string(),
|
|
30
|
+
author: z.string(),
|
|
31
|
+
authorWikidataId: z.string().optional(),
|
|
32
|
+
sourceWork: z.string().optional(),
|
|
33
|
+
sourceUrl: z.string().optional(),
|
|
34
|
+
year: z.number().optional(),
|
|
35
|
+
page: z.string().optional(),
|
|
36
|
+
originalLang: z.string().optional(),
|
|
37
|
+
translation: z.string().optional(),
|
|
38
|
+
translator: z.string().optional(),
|
|
39
|
+
license: z.string().optional(),
|
|
40
|
+
referenceImage: quoteImageRef.optional(),
|
|
41
|
+
flowImage: z
|
|
42
|
+
.object({
|
|
43
|
+
url: z.string(),
|
|
44
|
+
alt: z.string(),
|
|
45
|
+
caption: z.string().optional(),
|
|
46
|
+
prompt: z.string().optional(),
|
|
47
|
+
model: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.optional(),
|
|
50
|
+
});
|
|
51
|
+
const popReferencePayload = z.object({
|
|
52
|
+
featuredItemId: z.string(),
|
|
53
|
+
relatedItemIds: z.array(z.string()).optional(),
|
|
54
|
+
lensTheme: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
const educativeReferencePayload = z.object({
|
|
57
|
+
sourceLessonId: z.string().describe("lessons:_id da aula fonte"),
|
|
58
|
+
sourceCourseSlug: z.string().optional(),
|
|
59
|
+
sourceModuleId: z.string().optional(),
|
|
60
|
+
angle: z.string().optional().describe("fundamento, case, objecao, exemplo…"),
|
|
61
|
+
partNumber: z.number().optional(),
|
|
62
|
+
totalParts: z.number().optional(),
|
|
63
|
+
});
|
|
64
|
+
export const quotePopSchema = z.object({
|
|
65
|
+
action: z.enum(["publish_quote", "publish_pop", "publish_educativo"]),
|
|
66
|
+
// Comuns
|
|
67
|
+
slug: z.string().describe("kebab-case único"),
|
|
68
|
+
title: z.string(),
|
|
69
|
+
excerpt: z.string().optional(),
|
|
70
|
+
content: z.string().describe("Markdown completo do artigo/comentário."),
|
|
71
|
+
tags: z
|
|
72
|
+
.array(z.string())
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Tags. Pra publish_quote, 'sapiens-column' é adicionada automaticamente se faltar."),
|
|
75
|
+
thumbnailUrl: z.string().optional(),
|
|
76
|
+
generatedBy: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Marker do gerador (ex: 'claude-opus-4.7-max:sapiens-quote-skill'). Default genérico se omitido."),
|
|
80
|
+
publishNow: z
|
|
81
|
+
.boolean()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Default false (cria como draft). True publica direto sem passar por curation."),
|
|
84
|
+
// Quote-specific
|
|
85
|
+
tldr: z.string().optional(),
|
|
86
|
+
category: z.string().optional().describe("Default 'filosofia-tech' pra quote, 'repertorio' pra pop"),
|
|
87
|
+
format: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Pra publish_quote: 'short' (200-600 palavras) ou 'essay' (>600). Default 'short'."),
|
|
91
|
+
readingTimeMinutes: z.number().optional(),
|
|
92
|
+
connectedSlugs: z.array(z.string()).optional(),
|
|
93
|
+
quote: quotePayload
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Obrigatório pra publish_quote."),
|
|
96
|
+
overwrite: z
|
|
97
|
+
.boolean()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Pra publish_quote: se draft com mesmo slug existe (status='draft', column='sapiens'), apaga e recria. Não sobrescreve published."),
|
|
100
|
+
// Pop-specific
|
|
101
|
+
popReference: popReferencePayload
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Obrigatório pra publish_pop. featuredItemId vem de sapiens_repertorio."),
|
|
104
|
+
// Educativo-specific
|
|
105
|
+
educativeReference: educativeReferencePayload
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Obrigatório pra publish_educativo. sourceLessonId vem da query lms:listLessonsForBlogPicker."),
|
|
108
|
+
});
|
|
109
|
+
export async function quotePop(args) {
|
|
110
|
+
const sessionToken = getSessionToken();
|
|
111
|
+
if (args.action === "publish_quote") {
|
|
112
|
+
if (!args.quote) {
|
|
113
|
+
throw new Error("publish_quote exige campo 'quote' (objeto).");
|
|
114
|
+
}
|
|
115
|
+
const tags = args.tags ?? [];
|
|
116
|
+
const result = await convexMutation("mcpExtras:mcpPublishQuote", {
|
|
117
|
+
sessionToken,
|
|
118
|
+
slug: args.slug,
|
|
119
|
+
title: args.title,
|
|
120
|
+
excerpt: args.excerpt,
|
|
121
|
+
tldr: args.tldr,
|
|
122
|
+
content: args.content,
|
|
123
|
+
category: args.category ?? "filosofia-tech",
|
|
124
|
+
tags,
|
|
125
|
+
format: args.format ?? "short",
|
|
126
|
+
readingTimeMinutes: args.readingTimeMinutes,
|
|
127
|
+
connectedSlugs: args.connectedSlugs,
|
|
128
|
+
quote: args.quote,
|
|
129
|
+
thumbnailUrl: args.thumbnailUrl,
|
|
130
|
+
generatedBy: args.generatedBy ?? "mcp-sapiens:quote",
|
|
131
|
+
overwrite: args.overwrite,
|
|
132
|
+
publishNow: args.publishNow,
|
|
133
|
+
});
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
if (args.action === "publish_pop") {
|
|
137
|
+
if (!args.popReference) {
|
|
138
|
+
throw new Error("publish_pop exige popReference { featuredItemId, relatedItemIds?, lensTheme? }.");
|
|
139
|
+
}
|
|
140
|
+
const result = await convexMutation("mcpExtras:mcpPublishPopArticle", {
|
|
141
|
+
sessionToken,
|
|
142
|
+
slug: args.slug,
|
|
143
|
+
title: args.title,
|
|
144
|
+
excerpt: args.excerpt,
|
|
145
|
+
content: args.content,
|
|
146
|
+
tags: args.tags,
|
|
147
|
+
thumbnailUrl: args.thumbnailUrl,
|
|
148
|
+
popReference: args.popReference,
|
|
149
|
+
generatedBy: args.generatedBy ?? "mcp-sapiens:pop",
|
|
150
|
+
publishNow: args.publishNow,
|
|
151
|
+
});
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
if (args.action === "publish_educativo") {
|
|
155
|
+
if (!args.educativeReference) {
|
|
156
|
+
throw new Error("publish_educativo exige educativeReference { sourceLessonId, angle?, partNumber?, totalParts? }.");
|
|
157
|
+
}
|
|
158
|
+
const result = await convexMutation("mcpExtras:mcpPublishEducativo", {
|
|
159
|
+
sessionToken,
|
|
160
|
+
slug: args.slug,
|
|
161
|
+
title: args.title,
|
|
162
|
+
excerpt: args.excerpt,
|
|
163
|
+
tldr: args.tldr,
|
|
164
|
+
content: args.content,
|
|
165
|
+
category: args.category,
|
|
166
|
+
tags: args.tags,
|
|
167
|
+
thumbnailUrl: args.thumbnailUrl,
|
|
168
|
+
readingTimeMinutes: args.readingTimeMinutes,
|
|
169
|
+
educativeReference: args.educativeReference,
|
|
170
|
+
generatedBy: args.generatedBy ?? "mcp-sapiens:educativo",
|
|
171
|
+
publishNow: args.publishNow,
|
|
172
|
+
});
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { convexMutation, convexQuery, getSessionToken, } from "../convexClient.js";
|
|
3
|
+
/**
|
|
4
|
+
* Acesso ao Repertório do Sapiens (acervo pessoal de filme/série/anime/jogo).
|
|
5
|
+
*
|
|
6
|
+
* Reads (v1.0): list, search, get, lists, popArticles. Sem auth necessária —
|
|
7
|
+
* só vê items públicos (isPublic !== false).
|
|
8
|
+
*
|
|
9
|
+
* Mutations (v1.1+): add_item, update_item, remove_item. Usam sessionToken
|
|
10
|
+
* e exigem admin (consistente com `requireMcpAdmin`). Ownership: tudo vai
|
|
11
|
+
* pro userId da sessão (o user logado). Caller NÃO escolhe pra quem
|
|
12
|
+
* adicionar.
|
|
13
|
+
*
|
|
14
|
+
* Action-based design (memoria: consolidar tools via args).
|
|
15
|
+
*/
|
|
16
|
+
const mediaTypeEnum = z.enum(["movie", "series", "anime", "game"]);
|
|
17
|
+
const statusEnum = z.enum([
|
|
18
|
+
"backlog",
|
|
19
|
+
"active",
|
|
20
|
+
"completed",
|
|
21
|
+
"paused",
|
|
22
|
+
"dropped",
|
|
23
|
+
]);
|
|
24
|
+
const sourceEnum = z.enum(["tmdb", "anilist", "rawg", "manual"]);
|
|
25
|
+
export const repertorioSchema = z.object({
|
|
26
|
+
action: z.enum([
|
|
27
|
+
"list",
|
|
28
|
+
"search",
|
|
29
|
+
"get",
|
|
30
|
+
"lists",
|
|
31
|
+
"popArticles",
|
|
32
|
+
"add_item",
|
|
33
|
+
"update_item",
|
|
34
|
+
"remove_item",
|
|
35
|
+
]),
|
|
36
|
+
// Reads
|
|
37
|
+
userId: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("users:_id (obrigatório pra list/search/lists). Descobre via sapiens_meta action=whoami."),
|
|
41
|
+
itemId: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("repertorioItems:_id (obrigatório pra get/update_item/remove_item)."),
|
|
45
|
+
query: z.string().optional().describe("Texto pra action=search."),
|
|
46
|
+
mediaType: mediaTypeEnum.optional(),
|
|
47
|
+
status: statusEnum.optional(),
|
|
48
|
+
limit: z.number().int().positive().max(500).optional().default(100),
|
|
49
|
+
// add_item — todos os campos do schema repertorioItems user-side
|
|
50
|
+
source: sourceEnum
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Pra add_item: 'tmdb'/'anilist'/'rawg' se vem de provider externo; 'manual' pra entry hand-rolled (gere externalId único via UUID)."),
|
|
53
|
+
externalId: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Pra add_item: ID externo (tmdb/anilist/rawg) ou UUID se source='manual'. Dedup por (userId, source, externalId)."),
|
|
57
|
+
title: z.string().optional(),
|
|
58
|
+
titleOriginal: z.string().optional(),
|
|
59
|
+
year: z.number().optional(),
|
|
60
|
+
posterUrl: z.string().optional(),
|
|
61
|
+
backdropUrl: z.string().optional(),
|
|
62
|
+
overview: z.string().optional(),
|
|
63
|
+
genres: z.array(z.string()).optional(),
|
|
64
|
+
runtimeMin: z.number().optional(),
|
|
65
|
+
platforms: z.array(z.string()).optional(),
|
|
66
|
+
studios: z.array(z.string()).optional(),
|
|
67
|
+
rating: z
|
|
68
|
+
.number()
|
|
69
|
+
.nullable()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("0-10. Pra update_item: passe null pra remover rating."),
|
|
72
|
+
tags: z.array(z.string()).optional(),
|
|
73
|
+
note: z.string().optional().describe("Nota pessoal sobre a obra."),
|
|
74
|
+
containsSpoilers: z.boolean().optional(),
|
|
75
|
+
isPublic: z.boolean().optional(),
|
|
76
|
+
imdbId: z.string().optional(),
|
|
77
|
+
tmdbId: z.number().optional(),
|
|
78
|
+
traktId: z.number().optional(),
|
|
79
|
+
anilistId: z.number().optional(),
|
|
80
|
+
});
|
|
81
|
+
function need(value, name) {
|
|
82
|
+
if (value === undefined || value === null) {
|
|
83
|
+
throw new Error(`Faltando arg "${name}" pra essa action.`);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
export async function repertorio(args) {
|
|
88
|
+
// popArticles: sem auth necessário (lê públicos)
|
|
89
|
+
if (args.action === "popArticles") {
|
|
90
|
+
const articles = await convexQuery("popArticles:listPublishedPopArticles", { limit: args.limit });
|
|
91
|
+
return {
|
|
92
|
+
count: Array.isArray(articles) ? articles.length : 0,
|
|
93
|
+
articles: (articles || []).map((a) => ({
|
|
94
|
+
slug: a.slug,
|
|
95
|
+
title: a.title,
|
|
96
|
+
excerpt: a.excerpt,
|
|
97
|
+
lensTheme: a.lensTheme,
|
|
98
|
+
publishedAt: a.publishedAt,
|
|
99
|
+
url: `/articles/${a.slug}`,
|
|
100
|
+
})),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Mutations: sempre exigem sessionToken
|
|
104
|
+
if (args.action === "add_item" ||
|
|
105
|
+
args.action === "update_item" ||
|
|
106
|
+
args.action === "remove_item") {
|
|
107
|
+
const sessionToken = getSessionToken();
|
|
108
|
+
if (args.action === "add_item") {
|
|
109
|
+
return await convexMutation("mcpExtras:mcpAddRepertorioItem", {
|
|
110
|
+
sessionToken,
|
|
111
|
+
mediaType: need(args.mediaType, "mediaType"),
|
|
112
|
+
source: need(args.source, "source"),
|
|
113
|
+
externalId: need(args.externalId, "externalId"),
|
|
114
|
+
title: need(args.title, "title"),
|
|
115
|
+
titleOriginal: args.titleOriginal,
|
|
116
|
+
year: args.year,
|
|
117
|
+
posterUrl: args.posterUrl,
|
|
118
|
+
backdropUrl: args.backdropUrl,
|
|
119
|
+
overview: args.overview,
|
|
120
|
+
genres: args.genres,
|
|
121
|
+
runtimeMin: args.runtimeMin,
|
|
122
|
+
platforms: args.platforms,
|
|
123
|
+
studios: args.studios,
|
|
124
|
+
status: args.status,
|
|
125
|
+
rating: args.rating === null ? undefined : args.rating,
|
|
126
|
+
tags: args.tags,
|
|
127
|
+
note: args.note,
|
|
128
|
+
isPublic: args.isPublic,
|
|
129
|
+
imdbId: args.imdbId,
|
|
130
|
+
tmdbId: args.tmdbId,
|
|
131
|
+
traktId: args.traktId,
|
|
132
|
+
anilistId: args.anilistId,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (args.action === "update_item") {
|
|
136
|
+
return await convexMutation("mcpExtras:mcpUpdateRepertorioItem", {
|
|
137
|
+
sessionToken,
|
|
138
|
+
itemId: need(args.itemId, "itemId"),
|
|
139
|
+
status: args.status,
|
|
140
|
+
rating: args.rating,
|
|
141
|
+
tags: args.tags,
|
|
142
|
+
note: args.note,
|
|
143
|
+
containsSpoilers: args.containsSpoilers,
|
|
144
|
+
isPublic: args.isPublic,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (args.action === "remove_item") {
|
|
148
|
+
return await convexMutation("mcpExtras:mcpRemoveRepertorioItem", {
|
|
149
|
+
sessionToken,
|
|
150
|
+
itemId: need(args.itemId, "itemId"),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Reads que exigem userId
|
|
155
|
+
if (!args.userId) {
|
|
156
|
+
throw new Error(`action=${args.action} exige userId. Use sapiens_meta action=whoami pra descobrir o user atual.`);
|
|
157
|
+
}
|
|
158
|
+
if (args.action === "lists") {
|
|
159
|
+
const lists = await convexQuery("repertorio:listLists", {
|
|
160
|
+
userId: args.userId,
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
count: Array.isArray(lists) ? lists.length : 0,
|
|
164
|
+
lists: (lists || []).map((l) => ({
|
|
165
|
+
_id: l._id,
|
|
166
|
+
name: l.name,
|
|
167
|
+
slug: l.slug,
|
|
168
|
+
description: l.description,
|
|
169
|
+
isPublic: l.isPublic,
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (args.action === "get") {
|
|
174
|
+
const item = await convexQuery("repertorio:getById", {
|
|
175
|
+
itemId: need(args.itemId, "itemId"),
|
|
176
|
+
});
|
|
177
|
+
if (!item)
|
|
178
|
+
return { error: "not found" };
|
|
179
|
+
return slimItem(item);
|
|
180
|
+
}
|
|
181
|
+
// list / search
|
|
182
|
+
const queryArgs = { userId: args.userId, limit: args.limit };
|
|
183
|
+
if (args.mediaType)
|
|
184
|
+
queryArgs.mediaType = args.mediaType;
|
|
185
|
+
if (args.status)
|
|
186
|
+
queryArgs.status = args.status;
|
|
187
|
+
const items = await convexQuery("repertorio:listByUser", queryArgs);
|
|
188
|
+
let filtered = items || [];
|
|
189
|
+
if (args.action === "search" && args.query?.trim()) {
|
|
190
|
+
const q = args.query.toLowerCase();
|
|
191
|
+
filtered = filtered.filter((it) => (it.title || "").toLowerCase().includes(q) ||
|
|
192
|
+
(it.titleOriginal || "").toLowerCase().includes(q) ||
|
|
193
|
+
(it.genres || []).some((g) => g.toLowerCase().includes(q)) ||
|
|
194
|
+
(it.tags || []).some((t) => t.toLowerCase().includes(q)));
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
count: filtered.length,
|
|
198
|
+
items: filtered.map(slimItem),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function slimItem(it) {
|
|
202
|
+
return {
|
|
203
|
+
_id: it._id,
|
|
204
|
+
title: it.title,
|
|
205
|
+
titleOriginal: it.titleOriginal,
|
|
206
|
+
year: it.year,
|
|
207
|
+
mediaType: it.mediaType,
|
|
208
|
+
genres: it.genres || [],
|
|
209
|
+
tags: it.tags || [],
|
|
210
|
+
rating: it.rating,
|
|
211
|
+
note: it.note,
|
|
212
|
+
status: it.status,
|
|
213
|
+
posterUrl: it.posterUrl,
|
|
214
|
+
source: it.source,
|
|
215
|
+
externalId: it.externalId,
|
|
216
|
+
};
|
|
217
|
+
}
|