strapi-plugin-mcp-chat 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -0
  3. package/admin/src/components/AdminOverlays.tsx +190 -0
  4. package/admin/src/components/FloatingChat.tsx +370 -0
  5. package/admin/src/components/PreviewPanel.tsx +188 -0
  6. package/admin/src/index.tsx +49 -0
  7. package/admin/src/pages/App.tsx +14 -0
  8. package/admin/src/pages/HomePage.tsx +333 -0
  9. package/admin/src/pages/ProvisionPage.tsx +391 -0
  10. package/admin/src/pluginId.ts +1 -0
  11. package/dist/server/index.js +3511 -0
  12. package/package.json +77 -0
  13. package/server/src/content-tools.ts +520 -0
  14. package/server/src/controllers/audio.ts +45 -0
  15. package/server/src/controllers/chat.ts +22 -0
  16. package/server/src/controllers/frontend.ts +310 -0
  17. package/server/src/index.ts +43 -0
  18. package/server/src/mcp/index.ts +24 -0
  19. package/server/src/mcp/tools/buscar-texto.ts +28 -0
  20. package/server/src/mcp/tools/criar-locale.ts +30 -0
  21. package/server/src/mcp/tools/editar-campo.ts +39 -0
  22. package/server/src/mcp/tools/habilitar-i18n.ts +33 -0
  23. package/server/src/mcp/tools/index.ts +17 -0
  24. package/server/src/mcp/tools/listar-locales.ts +27 -0
  25. package/server/src/mcp/tools/publicar.ts +31 -0
  26. package/server/src/mcp/tools/traduzir.ts +36 -0
  27. package/server/src/mcp/types.ts +11 -0
  28. package/server/src/mcp-client.ts +96 -0
  29. package/server/src/provision/adapters.ts +91 -0
  30. package/server/src/provision/enable-i18n.ts +129 -0
  31. package/server/src/provision/generate.ts +216 -0
  32. package/server/src/provision/infer.ts +495 -0
  33. package/server/src/provision/integrate.ts +963 -0
  34. package/server/src/provision/link.ts +203 -0
  35. package/server/src/provision/manifest.ts +281 -0
  36. package/server/src/provision/orchestrate.ts +236 -0
  37. package/server/src/provision/permissions.ts +58 -0
  38. package/server/src/provision/runner.ts +176 -0
  39. package/server/src/provision/seed.ts +115 -0
  40. package/server/src/provision/translate.ts +153 -0
  41. package/server/src/provision/types-gen.ts +117 -0
  42. package/server/src/provision/write.ts +136 -0
  43. package/server/src/register.ts +17 -0
  44. package/server/src/routes/index.ts +66 -0
  45. package/server/src/services/audio.ts +53 -0
  46. package/server/src/services/chat.ts +263 -0
@@ -0,0 +1,3511 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // server/src/index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ default: () => index_default
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // server/src/controllers/chat.ts
37
+ var chat_default = ({ strapi }) => ({
38
+ async message(ctx) {
39
+ const { messages, image, lang, previewUrl } = ctx.request.body || {};
40
+ if (!Array.isArray(messages) || messages.length === 0) {
41
+ return ctx.badRequest('Campo "messages" (array) \xE9 obrigat\xF3rio.');
42
+ }
43
+ try {
44
+ const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl });
45
+ ctx.body = result;
46
+ } catch (e) {
47
+ strapi.log.error(`[mcp-chat] ${e?.message || e}`);
48
+ return ctx.internalServerError(e?.message || "Erro ao processar o chat.");
49
+ }
50
+ }
51
+ });
52
+
53
+ // server/src/controllers/audio.ts
54
+ var import_fs = require("fs");
55
+ var audio_default = ({ strapi }) => ({
56
+ async stt(ctx) {
57
+ const files = ctx.request.files || {};
58
+ const file = files.audio || files.file;
59
+ if (!file) return ctx.badRequest('Envie um arquivo de \xE1udio no campo "audio".');
60
+ const f = Array.isArray(file) ? file[0] : file;
61
+ const buffer = f.filepath ? (0, import_fs.readFileSync)(f.filepath) : f.buffer;
62
+ const mimetype = f.mimetype || f.type || "audio/webm";
63
+ const language = ctx.query?.language || ctx.request.body?.language;
64
+ try {
65
+ const result = await strapi.plugin("mcp-chat").service("audio").transcribe(buffer, mimetype, language);
66
+ ctx.body = result;
67
+ } catch (e) {
68
+ strapi.log.error(`[mcp-chat:stt] ${e?.message || e}`);
69
+ return ctx.internalServerError(e?.message || "Erro na transcri\xE7\xE3o.");
70
+ }
71
+ },
72
+ async tts(ctx) {
73
+ const { text, voice } = ctx.request.body || {};
74
+ if (!text) return ctx.badRequest('Campo "text" \xE9 obrigat\xF3rio.');
75
+ try {
76
+ const result = await strapi.plugin("mcp-chat").service("audio").synthesize(text, voice);
77
+ ctx.body = result;
78
+ } catch (e) {
79
+ strapi.log.error(`[mcp-chat:tts] ${e?.message || e}`);
80
+ return ctx.internalServerError(e?.message || "Erro na s\xEDntese de voz.");
81
+ }
82
+ }
83
+ });
84
+
85
+ // server/src/controllers/frontend.ts
86
+ var import_node_fs7 = __toESM(require("node:fs"));
87
+ var import_node_path7 = __toESM(require("node:path"));
88
+ var import_jszip = __toESM(require("jszip"));
89
+
90
+ // server/src/provision/orchestrate.ts
91
+ var import_node_fs3 = __toESM(require("node:fs"));
92
+ var import_node_path3 = __toESM(require("node:path"));
93
+
94
+ // server/src/provision/manifest.ts
95
+ var import_utils = require("@strapi/utils");
96
+ var kebab = import_utils.z.string().min(1).max(48).regex(/^[a-z][a-z0-9-]*$/, 'use kebab-case (ex.: "produto", "post-blog")');
97
+ var attrKey = import_utils.z.string().min(1).max(48).regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, "nome de campo inv\xE1lido (use letras/n\xFAmeros/_)");
98
+ var RESERVED_ATTRS = /* @__PURE__ */ new Set([
99
+ "id",
100
+ "documentId",
101
+ "createdAt",
102
+ "updatedAt",
103
+ "publishedAt",
104
+ "createdBy",
105
+ "updatedBy",
106
+ "locale",
107
+ "localizations"
108
+ ]);
109
+ var SCALAR_TYPES = [
110
+ "string",
111
+ "text",
112
+ "richtext",
113
+ "blocks",
114
+ "email",
115
+ "integer",
116
+ "biginteger",
117
+ "float",
118
+ "decimal",
119
+ "boolean",
120
+ "date",
121
+ "datetime",
122
+ "time",
123
+ "json"
124
+ ];
125
+ var commonOpts = {
126
+ required: import_utils.z.boolean().optional(),
127
+ unique: import_utils.z.boolean().optional(),
128
+ private: import_utils.z.boolean().optional(),
129
+ default: import_utils.z.union([import_utils.z.string(), import_utils.z.number(), import_utils.z.boolean()]).optional(),
130
+ /**
131
+ * Marca o campo como traduzível por locale (i18n). Opt-in: ausente ⇒ o campo
132
+ * é compartilhado entre locales (comportamento atual). Vira
133
+ * `pluginOptions.i18n.localized:true` no schema gerado. Só faz efeito se a
134
+ * própria content-type também tiver `localized:true`.
135
+ */
136
+ localized: import_utils.z.boolean().optional()
137
+ };
138
+ var scalarAttr = import_utils.z.object({
139
+ type: import_utils.z.enum(SCALAR_TYPES),
140
+ ...commonOpts
141
+ });
142
+ var uidAttr = import_utils.z.object({
143
+ type: import_utils.z.literal("uid"),
144
+ targetField: attrKey.optional(),
145
+ required: import_utils.z.boolean().optional()
146
+ });
147
+ var enumAttr = import_utils.z.object({
148
+ type: import_utils.z.literal("enumeration"),
149
+ enum: import_utils.z.array(import_utils.z.string().min(1)).min(1),
150
+ ...commonOpts
151
+ });
152
+ var mediaAttr = import_utils.z.object({
153
+ type: import_utils.z.literal("media"),
154
+ multiple: import_utils.z.boolean().optional(),
155
+ allowedTypes: import_utils.z.array(import_utils.z.enum(["images", "videos", "files", "audios"])).optional(),
156
+ required: import_utils.z.boolean().optional()
157
+ });
158
+ var RELATION_KINDS = [
159
+ "oneToOne",
160
+ "oneToMany",
161
+ "manyToOne",
162
+ "manyToMany"
163
+ ];
164
+ var relationAttr = import_utils.z.object({
165
+ type: import_utils.z.literal("relation"),
166
+ relation: import_utils.z.enum(RELATION_KINDS),
167
+ target: kebab,
168
+ required: import_utils.z.boolean().optional()
169
+ });
170
+ var attribute = import_utils.z.union([
171
+ scalarAttr,
172
+ uidAttr,
173
+ enumAttr,
174
+ mediaAttr,
175
+ relationAttr
176
+ ]);
177
+ var contentType = import_utils.z.object({
178
+ singularName: kebab,
179
+ pluralName: kebab.optional(),
180
+ displayName: import_utils.z.string().min(1).max(64).optional(),
181
+ kind: import_utils.z.enum(["collectionType", "singleType"]).default("collectionType"),
182
+ draftAndPublish: import_utils.z.boolean().default(true),
183
+ /**
184
+ * Liga a localização (i18n) na content-type. Necessário no nível da CT
185
+ * (confirmado em @strapi/i18n: `isLocalizedContentType` checa
186
+ * `pluginOptions.i18n.localized`). Os campos a traduzir devem marcar
187
+ * `localized:true` individualmente.
188
+ */
189
+ localized: import_utils.z.boolean().optional(),
190
+ description: import_utils.z.string().max(255).optional(),
191
+ attributes: import_utils.z.record(attrKey, attribute).refine((attrs) => Object.keys(attrs).length > 0, {
192
+ message: "a content-type precisa de pelo menos 1 campo"
193
+ }).refine(
194
+ (attrs) => Object.keys(attrs).every((k) => !RESERVED_ATTRS.has(k)),
195
+ { message: "um campo usa nome reservado da Strapi (ex.: id, createdAt)" }
196
+ ),
197
+ /**
198
+ * Rota do frontend usada para o preview, com placeholders de campo entre
199
+ * dois-pontos. Ex.: "/produtos/:slug". O adapter de cada framework usa isso
200
+ * para montar a URL de preview no PreviewPanel.
201
+ */
202
+ preview: import_utils.z.object({
203
+ route: import_utils.z.string().startsWith("/", 'a rota de preview deve come\xE7ar com "/"')
204
+ }).optional()
205
+ }).superRefine((ct, ctx) => {
206
+ for (const [key, attr] of Object.entries(ct.attributes)) {
207
+ if (attr.type === "uid" && attr.targetField) {
208
+ const target = ct.attributes[attr.targetField];
209
+ if (!target) {
210
+ ctx.addIssue({
211
+ code: import_utils.z.ZodIssueCode.custom,
212
+ message: `uid "${key}" aponta para campo inexistente "${attr.targetField}"`
213
+ });
214
+ } else if (!["string", "text"].includes(target.type)) {
215
+ ctx.addIssue({
216
+ code: import_utils.z.ZodIssueCode.custom,
217
+ message: `uid "${key}" deve apontar para um campo string/text`
218
+ });
219
+ }
220
+ }
221
+ }
222
+ });
223
+ var FRAMEWORKS = ["next", "tanstack"];
224
+ var manifestSchema = import_utils.z.object({
225
+ /** versão do formato do manifest, não do app. */
226
+ manifestVersion: import_utils.z.literal(1).default(1),
227
+ name: kebab,
228
+ framework: import_utils.z.enum(FRAMEWORKS),
229
+ strapiVersion: import_utils.z.string().optional(),
230
+ contentTypes: import_utils.z.array(contentType).min(1).max(60),
231
+ /** dados demo opcionais, semeados via Document Service após o restart. */
232
+ seed: import_utils.z.array(
233
+ import_utils.z.object({
234
+ uid: import_utils.z.string().optional(),
235
+ // resolvido pelo provisionador
236
+ singularName: kebab,
237
+ entries: import_utils.z.array(import_utils.z.record(import_utils.z.string(), import_utils.z.any())).max(500)
238
+ })
239
+ ).optional(),
240
+ /** nomes de env vars que o frontend espera (o adapter escreve o .env). */
241
+ env: import_utils.z.array(import_utils.z.string()).optional()
242
+ }).superRefine((m, ctx) => {
243
+ const names = m.contentTypes.map((c) => c.singularName);
244
+ const seen = /* @__PURE__ */ new Set();
245
+ for (const n of names) {
246
+ if (seen.has(n)) {
247
+ ctx.addIssue({
248
+ code: import_utils.z.ZodIssueCode.custom,
249
+ message: `content-type duplicada: "${n}"`
250
+ });
251
+ }
252
+ seen.add(n);
253
+ }
254
+ const known = new Set(names);
255
+ for (const ct of m.contentTypes) {
256
+ for (const [key, attr] of Object.entries(ct.attributes)) {
257
+ if (attr.type === "relation" && !known.has(attr.target)) {
258
+ ctx.addIssue({
259
+ code: import_utils.z.ZodIssueCode.custom,
260
+ message: `rela\xE7\xE3o "${ct.singularName}.${key}" aponta para "${attr.target}", que n\xE3o est\xE1 no manifest`
261
+ });
262
+ }
263
+ }
264
+ }
265
+ });
266
+ function validateManifest(raw) {
267
+ const parsed = manifestSchema.safeParse(raw);
268
+ if (parsed.success) return { ok: true, data: parsed.data };
269
+ const errors = parsed.error.issues.map((i) => {
270
+ const path9 = i.path.length ? `${i.path.join(".")}: ` : "";
271
+ return `${path9}${i.message}`;
272
+ });
273
+ return { ok: false, errors };
274
+ }
275
+
276
+ // server/src/provision/generate.ts
277
+ function toPlural(singular) {
278
+ if (/[^aeiou]y$/i.test(singular)) return singular.replace(/y$/i, "ies");
279
+ if (/(s|x|z|ch|sh)$/i.test(singular)) return `${singular}es`;
280
+ return `${singular}s`;
281
+ }
282
+ function toSnake(s) {
283
+ return s.replace(/-/g, "_");
284
+ }
285
+ function toTitle(s) {
286
+ return s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
287
+ }
288
+ function apiUid(singularName) {
289
+ return `api::${singularName}.${singularName}`;
290
+ }
291
+ function i18nField(attr) {
292
+ return attr.localized ? { pluginOptions: { i18n: { localized: true } } } : {};
293
+ }
294
+ function buildAttribute(attr) {
295
+ switch (attr.type) {
296
+ case "uid":
297
+ return clean({
298
+ type: "uid",
299
+ targetField: attr.targetField,
300
+ required: attr.required
301
+ });
302
+ case "enumeration":
303
+ return clean({
304
+ type: "enumeration",
305
+ ...i18nField(attr),
306
+ enum: attr.enum,
307
+ required: attr.required,
308
+ unique: attr.unique,
309
+ private: attr.private,
310
+ default: attr.default
311
+ });
312
+ case "media":
313
+ return clean({
314
+ type: "media",
315
+ multiple: attr.multiple ?? false,
316
+ required: attr.required,
317
+ allowedTypes: attr.allowedTypes
318
+ });
319
+ case "relation":
320
+ return {
321
+ type: "relation",
322
+ relation: attr.relation,
323
+ target: apiUid(attr.target)
324
+ };
325
+ default:
326
+ return clean({
327
+ type: attr.type,
328
+ ...i18nField(attr),
329
+ required: attr.required,
330
+ unique: attr.unique,
331
+ private: attr.private,
332
+ default: attr.default
333
+ });
334
+ }
335
+ }
336
+ function clean(obj) {
337
+ for (const k of Object.keys(obj)) if (obj[k] === void 0) delete obj[k];
338
+ return obj;
339
+ }
340
+ function buildSchema(ct) {
341
+ const pluralName = ct.pluralName ?? toPlural(ct.singularName);
342
+ const attributes = {};
343
+ for (const [key, attr] of Object.entries(ct.attributes)) {
344
+ attributes[key] = buildAttribute(attr);
345
+ }
346
+ return clean({
347
+ kind: ct.kind,
348
+ collectionName: toSnake(pluralName),
349
+ info: {
350
+ singularName: ct.singularName,
351
+ pluralName,
352
+ displayName: ct.displayName ?? toTitle(ct.singularName),
353
+ description: ct.description ?? ""
354
+ },
355
+ options: {
356
+ draftAndPublish: ct.draftAndPublish
357
+ },
358
+ // Nível CT é obrigatório p/ o i18n reconhecer a content-type como localizada
359
+ // (@strapi/i18n: isLocalizedContentType lê pluginOptions.i18n.localized).
360
+ pluginOptions: ct.localized ? { i18n: { localized: true } } : void 0,
361
+ attributes
362
+ });
363
+ }
364
+ function controllerFile(singular) {
365
+ return `/**
366
+ * ${singular} controller
367
+ */
368
+ import { factories } from '@strapi/strapi';
369
+
370
+ export default factories.createCoreController('${apiUid(singular)}' as any);
371
+ `;
372
+ }
373
+ function routeFile(singular) {
374
+ return `/**
375
+ * ${singular} router
376
+ */
377
+ import { factories } from '@strapi/strapi';
378
+
379
+ export default factories.createCoreRouter('${apiUid(singular)}' as any);
380
+ `;
381
+ }
382
+ function serviceFile(singular) {
383
+ return `/**
384
+ * ${singular} service
385
+ */
386
+ import { factories } from '@strapi/strapi';
387
+
388
+ export default factories.createCoreService('${apiUid(singular)}' as any);
389
+ `;
390
+ }
391
+ function generateApi(ct) {
392
+ const s = ct.singularName;
393
+ const base = s;
394
+ return {
395
+ singularName: s,
396
+ uid: apiUid(s),
397
+ files: {
398
+ [`${base}/content-types/${s}/schema.json`]: JSON.stringify(buildSchema(ct), null, 2) + "\n",
399
+ [`${base}/controllers/${s}.ts`]: controllerFile(s),
400
+ [`${base}/routes/${s}.ts`]: routeFile(s),
401
+ [`${base}/services/${s}.ts`]: serviceFile(s)
402
+ }
403
+ };
404
+ }
405
+ function generateAll(manifest) {
406
+ return manifest.contentTypes.map(generateApi);
407
+ }
408
+
409
+ // server/src/provision/write.ts
410
+ var import_node_fs = __toESM(require("node:fs"));
411
+ var import_node_path = __toESM(require("node:path"));
412
+ function isDev() {
413
+ return process.env.NODE_ENV === "development";
414
+ }
415
+ function writeApis(apis, opts) {
416
+ const result = {
417
+ ok: false,
418
+ dryRun: !!opts.dryRun,
419
+ planned: [],
420
+ written: [],
421
+ skipped: [],
422
+ errors: []
423
+ };
424
+ if (!opts.allowOutsideDev && !isDev()) {
425
+ result.errors.push(
426
+ "Gera\xE7\xE3o de content-types s\xF3 \xE9 permitida em desenvolvimento (NODE_ENV=development). Em produ\xE7\xE3o, gere os types em dev e fa\xE7a deploy do c\xF3digo."
427
+ );
428
+ return result;
429
+ }
430
+ if (!import_node_path.default.isAbsolute(opts.apiRoot)) {
431
+ result.errors.push(`apiRoot deve ser um caminho absoluto: ${opts.apiRoot}`);
432
+ return result;
433
+ }
434
+ for (const api of apis) {
435
+ const apiDir = import_node_path.default.join(opts.apiRoot, api.singularName);
436
+ if (import_node_fs.default.existsSync(apiDir)) {
437
+ result.skipped.push({
438
+ singularName: api.singularName,
439
+ reason: `j\xE1 existe em ${import_node_path.default.relative(opts.apiRoot, apiDir)} \u2014 preservado`
440
+ });
441
+ continue;
442
+ }
443
+ for (const [rel, content] of Object.entries(api.files)) {
444
+ const full = import_node_path.default.join(opts.apiRoot, rel);
445
+ const normalized = import_node_path.default.normalize(full);
446
+ if (normalized !== opts.apiRoot && !normalized.startsWith(opts.apiRoot + import_node_path.default.sep)) {
447
+ result.errors.push(`caminho fora de apiRoot bloqueado: ${rel}`);
448
+ continue;
449
+ }
450
+ result.planned.push(rel);
451
+ if (opts.dryRun) continue;
452
+ try {
453
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(full), { recursive: true });
454
+ import_node_fs.default.writeFileSync(full, content, "utf8");
455
+ result.written.push(rel);
456
+ } catch (e) {
457
+ result.errors.push(`falha ao escrever ${rel}: ${e?.message ?? e}`);
458
+ }
459
+ }
460
+ }
461
+ result.ok = result.errors.length === 0;
462
+ return result;
463
+ }
464
+
465
+ // server/src/provision/seed.ts
466
+ async function seedContent(strapi, manifest) {
467
+ const result = {
468
+ ok: false,
469
+ created: [],
470
+ skipped: [],
471
+ errors: []
472
+ };
473
+ if (!manifest.seed?.length) {
474
+ result.ok = true;
475
+ return result;
476
+ }
477
+ const byName = new Map(
478
+ manifest.contentTypes.map((ct) => [ct.singularName, ct])
479
+ );
480
+ for (const group of manifest.seed) {
481
+ const uid = apiUid(group.singularName);
482
+ const def = byName.get(group.singularName);
483
+ if (!def) {
484
+ result.skipped.push({
485
+ uid,
486
+ reason: "singularName n\xE3o consta em contentTypes"
487
+ });
488
+ continue;
489
+ }
490
+ if (!strapi.contentTypes?.[uid]) {
491
+ result.skipped.push({
492
+ uid,
493
+ reason: "content-type ainda n\xE3o registrada (faltou restart?)"
494
+ });
495
+ continue;
496
+ }
497
+ const createOne = async (data) => {
498
+ const doc = await strapi.documents(uid).create({ data });
499
+ if (def.draftAndPublish && doc?.documentId) {
500
+ await strapi.documents(uid).publish({ documentId: doc.documentId });
501
+ }
502
+ };
503
+ try {
504
+ if (def.kind === "singleType") {
505
+ const current = await strapi.documents(uid).findFirst();
506
+ if (current) {
507
+ result.skipped.push({ uid, reason: "single type j\xE1 tem conte\xFAdo" });
508
+ continue;
509
+ }
510
+ if (group.entries[0]) {
511
+ await createOne(group.entries[0]);
512
+ result.created.push({ uid, count: 1 });
513
+ }
514
+ continue;
515
+ }
516
+ const existing = await strapi.documents(uid).findMany({ limit: 1 });
517
+ if (Array.isArray(existing) && existing.length > 0) {
518
+ result.skipped.push({ uid, reason: "cole\xE7\xE3o j\xE1 tem conte\xFAdo" });
519
+ continue;
520
+ }
521
+ let count = 0;
522
+ let failed = 0;
523
+ for (const data of group.entries) {
524
+ try {
525
+ await createOne(data);
526
+ count++;
527
+ } catch (e) {
528
+ failed++;
529
+ if (failed === 1) result.errors.push(`seed ${uid} (entrada): ${e?.message ?? e}`);
530
+ }
531
+ }
532
+ result.created.push({ uid, count });
533
+ if (failed) result.skipped.push({ uid, reason: `${failed} entrada(s) falharam` });
534
+ } catch (e) {
535
+ result.errors.push(`seed ${uid}: ${e?.message ?? e}`);
536
+ }
537
+ }
538
+ result.ok = result.errors.length === 0;
539
+ return result;
540
+ }
541
+
542
+ // server/src/provision/link.ts
543
+ var import_node_fs2 = __toESM(require("node:fs"));
544
+ var import_node_path2 = __toESM(require("node:path"));
545
+
546
+ // server/src/provision/adapters.ts
547
+ var nextAdapter = {
548
+ framework: "next",
549
+ envFileName: ".env.local",
550
+ defaultPort: 3e3,
551
+ buildEnv: ({ strapiUrl, apiToken, previewSecret, locales }) => {
552
+ const env = {
553
+ // pública: usada por Server e Client Components
554
+ NEXT_PUBLIC_STRAPI_URL: strapiUrl
555
+ };
556
+ if (locales && locales.length) env.NEXT_PUBLIC_LOCALES = locales.join(",");
557
+ if (apiToken) env.STRAPI_API_TOKEN = apiToken;
558
+ if (previewSecret) env.PREVIEW_SECRET = previewSecret;
559
+ return env;
560
+ },
561
+ previewBridgeHint: "app/_components/PreviewBridge.tsx montado no layout raiz (postMessage para o admin)"
562
+ };
563
+ var tanstackAdapter = {
564
+ framework: "tanstack",
565
+ envFileName: ".env",
566
+ defaultPort: 5173,
567
+ buildEnv: ({ strapiUrl, apiToken, previewSecret, locales }) => {
568
+ const env = {
569
+ // pública no Vite/TanStack: exposta via import.meta.env
570
+ VITE_STRAPI_URL: strapiUrl
571
+ };
572
+ if (locales && locales.length) env.VITE_LOCALES = locales.join(",");
573
+ if (apiToken) env.STRAPI_API_TOKEN = apiToken;
574
+ if (previewSecret) env.PREVIEW_SECRET = previewSecret;
575
+ return env;
576
+ },
577
+ previewBridgeHint: "src/components/PreviewBridge.tsx montado no __root (postMessage para o admin)"
578
+ };
579
+ var ADAPTERS = {
580
+ next: nextAdapter,
581
+ tanstack: tanstackAdapter
582
+ };
583
+ function getAdapter(framework) {
584
+ return ADAPTERS[framework];
585
+ }
586
+ function adapterForManifest(manifest) {
587
+ return getAdapter(manifest.framework);
588
+ }
589
+
590
+ // server/src/provision/types-gen.ts
591
+ function toPascal(singular) {
592
+ return singular.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
593
+ }
594
+ function scalarToTs(type) {
595
+ switch (type) {
596
+ case "string":
597
+ case "text":
598
+ case "richtext":
599
+ case "blocks":
600
+ case "email":
601
+ case "uid":
602
+ case "date":
603
+ case "datetime":
604
+ case "time":
605
+ return "string";
606
+ case "integer":
607
+ case "biginteger":
608
+ case "float":
609
+ case "decimal":
610
+ return "number";
611
+ case "boolean":
612
+ return "boolean";
613
+ case "json":
614
+ return "unknown";
615
+ default:
616
+ return "unknown";
617
+ }
618
+ }
619
+ function attrToTs(attr, pascalOf) {
620
+ switch (attr.type) {
621
+ case "enumeration":
622
+ return {
623
+ tsType: attr.enum.map((e) => JSON.stringify(e)).join(" | "),
624
+ optional: !attr.required
625
+ };
626
+ case "media":
627
+ return {
628
+ tsType: attr.multiple ? "StrapiMedia[]" : "StrapiMedia",
629
+ optional: !attr.required
630
+ };
631
+ case "relation": {
632
+ const target = pascalOf(attr.target);
633
+ const many = attr.relation === "oneToMany" || attr.relation === "manyToMany";
634
+ return { tsType: many ? `${target}[]` : target, optional: true };
635
+ }
636
+ case "uid":
637
+ return { tsType: "string", optional: !attr.required };
638
+ default:
639
+ return {
640
+ tsType: scalarToTs(attr.type),
641
+ optional: !attr.required
642
+ };
643
+ }
644
+ }
645
+ function buildInterface(ct) {
646
+ const name = ct.displayName ? toPascal(ct.singularName) : toPascal(ct.singularName);
647
+ const lines = [`export interface ${name} {`];
648
+ lines.push(" documentId: string;");
649
+ for (const [key, attr] of Object.entries(ct.attributes)) {
650
+ const { tsType, optional } = attrToTs(attr, toPascal);
651
+ lines.push(` ${key}${optional ? "?" : ""}: ${tsType};`);
652
+ }
653
+ lines.push(" createdAt: string;");
654
+ lines.push(" updatedAt: string;");
655
+ lines.push(" publishedAt?: string;");
656
+ lines.push("}");
657
+ return lines.join("\n");
658
+ }
659
+ var PREAMBLE = `// Tipos gerados automaticamente a partir do strapi.manifest.json.
660
+ // N\xC3O edite \xE0 m\xE3o \u2014 rode o link novamente para regenerar.
661
+
662
+ export interface StrapiMedia {
663
+ id: number;
664
+ documentId: string;
665
+ url: string;
666
+ alternativeText?: string;
667
+ width?: number;
668
+ height?: number;
669
+ mime?: string;
670
+ name?: string;
671
+ }
672
+ `;
673
+ function generateTypes(manifest) {
674
+ const interfaces = manifest.contentTypes.map(buildInterface).join("\n\n");
675
+ return `${PREAMBLE}
676
+ ${interfaces}
677
+ `;
678
+ }
679
+
680
+ // server/src/provision/link.ts
681
+ function parseEnv(content) {
682
+ const out = {};
683
+ for (const line of content.split("\n")) {
684
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=(.*)$/);
685
+ if (m) out[m[1]] = m[2];
686
+ }
687
+ return out;
688
+ }
689
+ function mergeEnv(existing, next) {
690
+ const current = parseEnv(existing);
691
+ const added = [];
692
+ const preserved = [];
693
+ const extra = [];
694
+ for (const [k, v] of Object.entries(next)) {
695
+ if (k in current) {
696
+ preserved.push(k);
697
+ } else {
698
+ added.push(k);
699
+ extra.push(`${k}=${v}`);
700
+ }
701
+ }
702
+ let content = existing;
703
+ if (extra.length) {
704
+ const block = "\n# adicionado pelo mcp-chat (link)\n" + extra.join("\n") + "\n";
705
+ content = existing.trimEnd() + "\n" + block;
706
+ }
707
+ return { content, added, preserved };
708
+ }
709
+ function buildPreviewConfig(manifest) {
710
+ const routes = {};
711
+ for (const ct of manifest.contentTypes) {
712
+ if (ct.preview?.route) routes[apiUid(ct.singularName)] = ct.preview.route;
713
+ }
714
+ const routesJson = JSON.stringify(routes, null, 2);
715
+ return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json.
716
+ // Mapa uid -> rota do frontend (placeholders :campo s\xE3o preenchidos pelo doc).
717
+ const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
718
+
719
+ export default ({ env }) => ({
720
+ auth: {
721
+ secret: env('ADMIN_JWT_SECRET'),
722
+ },
723
+ apiToken: { salt: env('API_TOKEN_SALT') },
724
+ transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
725
+ preview: {
726
+ enabled: true,
727
+ config: {
728
+ allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
729
+ async handler(uid: string, { documentId, locale, status }: any) {
730
+ const route = PREVIEW_ROUTES[uid];
731
+ if (!route) return null;
732
+ const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
733
+ if (!doc) return null;
734
+ // substitui :campo pelos valores do documento (ex.: :slug)
735
+ const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
736
+ encodeURIComponent(String((doc as any)[f] ?? ''))
737
+ );
738
+ const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
739
+ const secret = env('PREVIEW_SECRET', '');
740
+ const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
741
+ return \`\${clientUrl}/api/preview?\${qs.toString()}\`;
742
+ },
743
+ },
744
+ },
745
+ });
746
+ `;
747
+ }
748
+ function ensureInside(base, target) {
749
+ const n = import_node_path2.default.normalize(target);
750
+ return n === base || n.startsWith(base + import_node_path2.default.sep);
751
+ }
752
+ function linkFrontend(manifest, opts) {
753
+ const adapter = adapterForManifest(manifest);
754
+ const result = {
755
+ ok: false,
756
+ envFile: adapter.envFileName,
757
+ envAdded: [],
758
+ envPreserved: [],
759
+ typesFile: "strapi-types.ts",
760
+ previewFile: "config/admin.ts",
761
+ previewAction: "skipped",
762
+ errors: []
763
+ };
764
+ if (!import_node_path2.default.isAbsolute(opts.frontendDir) || !import_node_path2.default.isAbsolute(opts.strapiAppDir)) {
765
+ result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
766
+ return result;
767
+ }
768
+ try {
769
+ const envPath = import_node_path2.default.join(opts.frontendDir, adapter.envFileName);
770
+ if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
771
+ const existing = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : "";
772
+ const vars = adapter.buildEnv(opts.context);
773
+ const { content, added, preserved } = mergeEnv(existing, vars);
774
+ result.envAdded = added;
775
+ result.envPreserved = preserved;
776
+ if (!opts.dryRun && added.length) import_node_fs2.default.writeFileSync(envPath, content, "utf8");
777
+ } catch (e) {
778
+ result.errors.push(`env: ${e?.message ?? e}`);
779
+ }
780
+ try {
781
+ const typesPath = import_node_path2.default.join(opts.frontendDir, result.typesFile);
782
+ if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
783
+ if (!opts.dryRun) import_node_fs2.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
784
+ } catch (e) {
785
+ result.errors.push(`types: ${e?.message ?? e}`);
786
+ }
787
+ try {
788
+ const adminPath = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.ts");
789
+ const content = buildPreviewConfig(manifest);
790
+ if (import_node_fs2.default.existsSync(adminPath)) {
791
+ result.previewFile = "config/admin.mcp-chat-preview.ts";
792
+ const sidecar = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.mcp-chat-preview.ts");
793
+ if (!opts.dryRun) import_node_fs2.default.writeFileSync(sidecar, content, "utf8");
794
+ result.previewAction = "sidecar";
795
+ } else {
796
+ if (!opts.dryRun) {
797
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(adminPath), { recursive: true });
798
+ import_node_fs2.default.writeFileSync(adminPath, content, "utf8");
799
+ }
800
+ result.previewAction = "created";
801
+ }
802
+ } catch (e) {
803
+ result.errors.push(`preview: ${e?.message ?? e}`);
804
+ }
805
+ result.ok = result.errors.length === 0;
806
+ return result;
807
+ }
808
+
809
+ // server/src/provision/permissions.ts
810
+ async function grantPublicRead(strapi, manifest) {
811
+ const result = { granted: [], errors: [] };
812
+ let publicRole;
813
+ try {
814
+ publicRole = await strapi.query("plugin::users-permissions.role").findOne({ where: { type: "public" } });
815
+ } catch (e) {
816
+ result.errors.push(`papel public: ${e?.message ?? e}`);
817
+ return result;
818
+ }
819
+ if (!publicRole) {
820
+ result.errors.push('papel "public" n\xE3o encontrado (users-permissions ativo?)');
821
+ return result;
822
+ }
823
+ for (const ct of manifest.contentTypes) {
824
+ const uid = apiUid(ct.singularName);
825
+ const actions = ct.kind === "singleType" ? ["find"] : ["find", "findOne"];
826
+ for (const action of actions) {
827
+ const actionId = `${uid}.${action}`;
828
+ try {
829
+ const existing = await strapi.query("plugin::users-permissions.permission").findOne({ where: { action: actionId, role: publicRole.id } });
830
+ if (!existing) {
831
+ await strapi.query("plugin::users-permissions.permission").create({ data: { action: actionId, role: publicRole.id } });
832
+ result.granted.push(actionId);
833
+ }
834
+ } catch (e) {
835
+ result.errors.push(`${actionId}: ${e?.message ?? e}`);
836
+ }
837
+ }
838
+ }
839
+ return result;
840
+ }
841
+
842
+ // server/src/provision/orchestrate.ts
843
+ var MARKER_DIR = ".mcp-chat";
844
+ var MARKER_FILE = "pending-provision.json";
845
+ var DONE_FILE = "last-provision.json";
846
+ function markerPath(strapiAppDir) {
847
+ return import_node_path3.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
848
+ }
849
+ function donePath(strapiAppDir) {
850
+ return import_node_path3.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
851
+ }
852
+ function getProvisionStatus(strapiAppDir) {
853
+ const pending = import_node_fs3.default.existsSync(markerPath(strapiAppDir));
854
+ let done = null;
855
+ try {
856
+ const dp = donePath(strapiAppDir);
857
+ if (import_node_fs3.default.existsSync(dp)) done = JSON.parse(import_node_fs3.default.readFileSync(dp, "utf8"));
858
+ } catch {
859
+ }
860
+ return { pending, done };
861
+ }
862
+ function stageProvision(strapi, input) {
863
+ const result = {
864
+ ok: false,
865
+ validation: { ok: false },
866
+ staged: false,
867
+ willReload: false,
868
+ errors: []
869
+ };
870
+ const v = validateManifest(input.rawManifest);
871
+ if (!v.ok) {
872
+ result.validation = { ok: false, errors: v.errors };
873
+ result.errors.push("manifest inv\xE1lido");
874
+ return result;
875
+ }
876
+ result.validation = { ok: true };
877
+ const apis = generateAll(v.data);
878
+ const write = writeApis(apis, { apiRoot: input.apiRoot, dryRun: input.dryRun });
879
+ result.write = write;
880
+ if (!write.ok) {
881
+ result.errors.push(...write.errors);
882
+ return result;
883
+ }
884
+ const marker = {
885
+ manifest: v.data,
886
+ frontendDir: input.frontendDir,
887
+ strapiAppDir: input.strapiAppDir,
888
+ context: input.context
889
+ };
890
+ if (!input.dryRun) {
891
+ try {
892
+ const mp = markerPath(input.strapiAppDir);
893
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(mp), { recursive: true });
894
+ import_node_fs3.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
895
+ try {
896
+ import_node_fs3.default.unlinkSync(donePath(input.strapiAppDir));
897
+ } catch {
898
+ }
899
+ result.staged = true;
900
+ } catch (e) {
901
+ result.errors.push(`marcador: ${e?.message ?? e}`);
902
+ return result;
903
+ }
904
+ }
905
+ result.ok = true;
906
+ result.willReload = !input.dryRun && write.written.length > 0;
907
+ return result;
908
+ }
909
+ async function runPendingProvision(strapi, strapiAppDir) {
910
+ const result = { ran: false, errors: [] };
911
+ const mp = markerPath(strapiAppDir);
912
+ if (!import_node_fs3.default.existsSync(mp)) return result;
913
+ let marker;
914
+ try {
915
+ marker = JSON.parse(import_node_fs3.default.readFileSync(mp, "utf8"));
916
+ } catch (e) {
917
+ result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
918
+ return result;
919
+ }
920
+ result.ran = true;
921
+ try {
922
+ result.seed = await seedContent(strapi, marker.manifest);
923
+ } catch (e) {
924
+ result.errors.push(`seed: ${e?.message ?? e}`);
925
+ }
926
+ try {
927
+ result.permissions = await grantPublicRead(strapi, marker.manifest);
928
+ if (result.permissions.errors.length) {
929
+ result.errors.push(...result.permissions.errors.map((e) => `perm: ${e}`));
930
+ }
931
+ } catch (e) {
932
+ result.errors.push(`permiss\xF5es: ${e?.message ?? e}`);
933
+ }
934
+ try {
935
+ result.link = linkFrontend(marker.manifest, {
936
+ frontendDir: marker.frontendDir,
937
+ strapiAppDir: marker.strapiAppDir,
938
+ context: marker.context
939
+ });
940
+ } catch (e) {
941
+ result.errors.push(`link: ${e?.message ?? e}`);
942
+ }
943
+ try {
944
+ const adapter = adapterForManifest(marker.manifest);
945
+ const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
946
+ const done = {
947
+ name: marker.manifest.name,
948
+ framework: marker.manifest.framework,
949
+ frontendDir: marker.frontendDir,
950
+ contentTypes: marker.manifest.contentTypes.map((c) => apiUid(c.singularName)),
951
+ previewUrl,
952
+ seedCreated: result.seed?.created ?? [],
953
+ linkErrors: result.link?.errors ?? [],
954
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
955
+ };
956
+ const dp = donePath(strapiAppDir);
957
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(dp), { recursive: true });
958
+ import_node_fs3.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
959
+ } catch (e) {
960
+ result.errors.push(`resumo: ${e?.message ?? e}`);
961
+ }
962
+ try {
963
+ import_node_fs3.default.unlinkSync(mp);
964
+ } catch {
965
+ }
966
+ return result;
967
+ }
968
+
969
+ // server/src/provision/infer.ts
970
+ var import_node_fs4 = __toESM(require("node:fs"));
971
+ var import_node_path4 = __toESM(require("node:path"));
972
+ var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
973
+ var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
974
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
975
+ "node_modules",
976
+ ".git",
977
+ "dist",
978
+ ".next",
979
+ ".output",
980
+ ".vinxi",
981
+ ".tanstack",
982
+ "build",
983
+ "coverage",
984
+ ".turbo",
985
+ ".cache",
986
+ "public"
987
+ ]);
988
+ var CODE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
989
+ var MAX_FILES = 18;
990
+ var MAX_TOTAL_CHARS = 6e4;
991
+ var MAX_FILE_CHARS = 12e3;
992
+ function score(rel) {
993
+ const p = rel.toLowerCase();
994
+ let s = 0;
995
+ if (/(^|\/)(data|content|mocks?|seeds?|fixtures?)(\/|\.)/.test(p)) s += 10;
996
+ if (/(site|config|constants|catalog|products?|services?|posts?|items?)/.test(p)) s += 4;
997
+ if (p.startsWith("src/")) s += 2;
998
+ if (p.endsWith(".tsx") || p.endsWith(".jsx")) s -= 1;
999
+ return s;
1000
+ }
1001
+ function walk(dir, base, out) {
1002
+ let entries;
1003
+ try {
1004
+ entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
1005
+ } catch {
1006
+ return;
1007
+ }
1008
+ for (const e of entries) {
1009
+ if (e.name.startsWith(".") && e.name !== ".") continue;
1010
+ const full = import_node_path4.default.join(dir, e.name);
1011
+ if (e.isDirectory()) {
1012
+ if (SKIP_DIRS.has(e.name)) continue;
1013
+ walk(full, base, out);
1014
+ } else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
1015
+ out.push(import_node_path4.default.relative(base, full));
1016
+ }
1017
+ }
1018
+ }
1019
+ function collectFiles(frontendDir) {
1020
+ const all = [];
1021
+ walk(frontendDir, frontendDir, all);
1022
+ const tree = all.slice().sort();
1023
+ const ranked = all.map((rel) => ({ rel, s: score(rel) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s);
1024
+ const files = [];
1025
+ let total = 0;
1026
+ for (const { rel } of ranked) {
1027
+ if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1028
+ try {
1029
+ let content = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8");
1030
+ if (!/export\s+(const|default|type|interface)/.test(content)) continue;
1031
+ if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1032
+ files.push({ rel, content });
1033
+ total += content.length;
1034
+ } catch {
1035
+ }
1036
+ }
1037
+ return { files, tree };
1038
+ }
1039
+ var MAX_TEXT_FILES = 25;
1040
+ var MAX_TEXTS_PER_FILE = 60;
1041
+ function extractTexts(content) {
1042
+ const found = /* @__PURE__ */ new Set();
1043
+ const add = (s) => {
1044
+ const t = s.replace(/\s+/g, " ").trim();
1045
+ if (t.length < 2 || t.length > 140) return;
1046
+ if (!/[A-Za-zÀ-ÿ]/.test(t)) return;
1047
+ if (/[{}<>()[\];=`|]|\$\{|=>|&&|\|\||https?:|@\//.test(t)) return;
1048
+ if (/\b(return|const|let|var|function|map|filter|import|export|className|props)\b/.test(t)) return;
1049
+ if (/\w\.\w/.test(t)) return;
1050
+ if (/^[a-z]+([A-Z][a-z]+)+$/.test(t)) return;
1051
+ if (!/[A-Za-zÀ-ÿ]{3,}/.test(t)) return;
1052
+ found.add(t);
1053
+ };
1054
+ for (const m of content.matchAll(/>\s*([^<>{}\n][^<>{}]*?)\s*</g)) add(m[1]);
1055
+ for (const m of content.matchAll(/\b(?:placeholder|title|alt|label|aria-label)\s*=\s*"([^"]+)"/g)) add(m[1]);
1056
+ return [...found].slice(0, MAX_TEXTS_PER_FILE);
1057
+ }
1058
+ function collectPageTexts(frontendDir) {
1059
+ const all = [];
1060
+ walk(frontendDir, frontendDir, all);
1061
+ const out = [];
1062
+ const ranked = all.filter((rel) => /\.(tsx|jsx)$/.test(rel)).filter((rel) => !/\/(ui)\//.test(rel)).sort((a, b) => {
1063
+ const pa = /routes?\/|pages?\//.test(a) ? 0 : 1;
1064
+ const pb = /routes?\/|pages?\//.test(b) ? 0 : 1;
1065
+ return pa - pb;
1066
+ });
1067
+ for (const rel of ranked) {
1068
+ if (out.length >= MAX_TEXT_FILES) break;
1069
+ try {
1070
+ const texts = extractTexts(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8"));
1071
+ if (texts.length) out.push({ rel, texts });
1072
+ } catch {
1073
+ }
1074
+ }
1075
+ return out;
1076
+ }
1077
+ var RESERVED = /* @__PURE__ */ new Set([
1078
+ "id",
1079
+ "documentId",
1080
+ "createdAt",
1081
+ "updatedAt",
1082
+ "publishedAt",
1083
+ "createdBy",
1084
+ "updatedBy",
1085
+ "locale",
1086
+ "localizations"
1087
+ ]);
1088
+ var MAX_PAGE_TYPES = 45;
1089
+ function toFieldKey(text) {
1090
+ const words = text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean).slice(0, 6);
1091
+ if (!words.length) return "text";
1092
+ let key = words[0] + words.slice(1).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
1093
+ key = key.slice(0, 44).replace(/^[^a-zA-Z]+/, "");
1094
+ if (!key) key = "text";
1095
+ if (RESERVED.has(key)) key = key + "Field";
1096
+ return key;
1097
+ }
1098
+ function toPageName(rel) {
1099
+ let base = rel.replace(/\\/g, "/").split("/").pop() || "page";
1100
+ base = base.replace(/\.(tsx|jsx|ts|js)$/, "");
1101
+ base = base.replace(/^\$+/, "").replace(/\$/g, "");
1102
+ if (/^index$/.test(base)) base = "home";
1103
+ else if (/^__?root$/.test(base)) base = "layout";
1104
+ const kebab2 = base.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase().replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
1105
+ return ((kebab2 || "page") + "-content").slice(0, 48);
1106
+ }
1107
+ function buildPageContentTypes(pageTexts, budget) {
1108
+ const contentTypes = [];
1109
+ const seed = [];
1110
+ const usedNames = /* @__PURE__ */ new Set();
1111
+ const overflow = [];
1112
+ const allowed = Math.max(0, Math.min(budget, MAX_PAGE_TYPES));
1113
+ pageTexts.forEach((p, idx) => {
1114
+ if (idx >= allowed) {
1115
+ for (const t of p.texts) overflow.push({ page: toPageName(p.rel), value: t });
1116
+ return;
1117
+ }
1118
+ let name = toPageName(p.rel);
1119
+ while (usedNames.has(name)) name = name.replace(/(-\d+)?-content$/, "") + `-${idx}-content`;
1120
+ usedNames.add(name);
1121
+ const attributes = {};
1122
+ const entry = {};
1123
+ const usedKeys = /* @__PURE__ */ new Set();
1124
+ for (const text of p.texts) {
1125
+ let key = toFieldKey(text);
1126
+ let k = key;
1127
+ let n = 2;
1128
+ while (usedKeys.has(k)) k = `${key}${n++}`.slice(0, 46);
1129
+ usedKeys.add(k);
1130
+ attributes[k] = { type: text.length > 80 ? "text" : "string" };
1131
+ entry[k] = text;
1132
+ }
1133
+ if (!Object.keys(attributes).length) return;
1134
+ contentTypes.push({
1135
+ singularName: name,
1136
+ displayName: name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
1137
+ kind: "singleType",
1138
+ draftAndPublish: true,
1139
+ attributes
1140
+ });
1141
+ seed.push({ singularName: name, entries: [entry] });
1142
+ });
1143
+ if (overflow.length) {
1144
+ contentTypes.push({
1145
+ singularName: "page-text",
1146
+ displayName: "Page Text",
1147
+ kind: "collectionType",
1148
+ draftAndPublish: true,
1149
+ attributes: {
1150
+ page: { type: "string" },
1151
+ value: { type: "text" }
1152
+ }
1153
+ });
1154
+ seed.push({ singularName: "page-text", entries: overflow });
1155
+ }
1156
+ return { contentTypes, seed };
1157
+ }
1158
+ function detectFramework(frontendDir) {
1159
+ try {
1160
+ const pkg = JSON.parse(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, "package.json"), "utf8"));
1161
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1162
+ if (deps.next) return "next";
1163
+ if (deps["@tanstack/react-start"]) return "tanstack";
1164
+ if (deps.vite) return "tanstack";
1165
+ } catch {
1166
+ }
1167
+ return "tanstack";
1168
+ }
1169
+ function buildPrompt(framework, c, name) {
1170
+ const filesBlock = c.files.map((f) => `--- ARQUIVO: ${f.rel} ---
1171
+ ${f.content}`).join("\n\n");
1172
+ return `Voc\xEA \xE9 um arquiteto de conte\xFAdo Strapi 5. Analise o c\xF3digo de um frontend e projete o modelo de conte\xFAdo.
1173
+
1174
+ Gere um JSON "strapi.manifest.json" com ESTE formato:
1175
+ {
1176
+ "manifestVersion": 1,
1177
+ "name": "${name}",
1178
+ "framework": "${framework}",
1179
+ "strapiVersion": "^5.47",
1180
+ "contentTypes": [
1181
+ {
1182
+ "singularName": "kebab-case", // ex.: "produto", "service", "blog-post"
1183
+ "displayName": "Nome leg\xEDvel",
1184
+ "kind": "collectionType" | "singleType", // listas = collectionType; config/\xFAnico = singleType
1185
+ "draftAndPublish": true,
1186
+ "attributes": {
1187
+ "campo": { "type": "string|text|richtext|integer|decimal|boolean|date|datetime|email|json|uid|enumeration|media|relation", ... }
1188
+ // uid: { "type":"uid", "targetField":"umCampoString" }
1189
+ // enumeration: { "type":"enumeration", "enum":["a","b"] }
1190
+ // media: { "type":"media", "multiple":false, "allowedTypes":["images"] }
1191
+ // relation: { "type":"relation", "relation":"manyToOne|oneToMany|manyToMany|oneToOne", "target":"singularName-de-outro-type" }
1192
+ },
1193
+ "preview": { "route": "/rota/:slug" } // opcional; s\xF3 se houver p\xE1gina de detalhe
1194
+ }
1195
+ ],
1196
+ "seed": [
1197
+ { "singularName": "...", "entries": [ { ...dados-extra\xEDdos-do-c\xF3digo... } ] }
1198
+ ],
1199
+ "env": ["${framework === "next" ? "NEXT_PUBLIC_STRAPI_URL" : "VITE_STRAPI_URL"}", "STRAPI_API_TOKEN", "PREVIEW_SECRET"]
1200
+ }
1201
+
1202
+ REGRAS:
1203
+ - Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
1204
+ - Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
1205
+ - Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
1206
+ - Use "date"/"datetime" SOMENTE para datas ISO completas (YYYY-MM-DD). Datas parciais como "2025-04" ou textos livres \u2192 use "string" (sen\xE3o o seed falha).
1207
+ - Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
1208
+ - Em "seed", extraia o conte\xFAdo REAL hardcoded no c\xF3digo, omitindo campos de m\xEDdia e rela\xE7\xF5es.
1209
+ - Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
1210
+ - N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1211
+ - Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
1212
+ - Responda APENAS com o JSON, nada de markdown.
1213
+
1214
+ \xC1rvore de arquivos do projeto:
1215
+ ${c.tree.slice(0, 200).join("\n")}
1216
+
1217
+ Arquivos de dados (cole\xE7\xF5es):
1218
+ ${filesBlock}`;
1219
+ }
1220
+ async function inferManifest(strapi, frontendDir, opts) {
1221
+ const framework = detectFramework(frontendDir);
1222
+ const result = {
1223
+ ok: false,
1224
+ inferred: true,
1225
+ filesAnalyzed: [],
1226
+ framework,
1227
+ warnings: [],
1228
+ errors: []
1229
+ };
1230
+ const existing = import_node_path4.default.join(frontendDir, "strapi.manifest.json");
1231
+ if (import_node_fs4.default.existsSync(existing)) {
1232
+ try {
1233
+ const raw = JSON.parse(import_node_fs4.default.readFileSync(existing, "utf8"));
1234
+ const v2 = validateManifest(raw);
1235
+ result.inferred = false;
1236
+ result.rawManifest = raw;
1237
+ if (v2.ok) {
1238
+ result.ok = true;
1239
+ result.manifest = v2.data;
1240
+ } else {
1241
+ result.errors.push(...v2.errors ?? []);
1242
+ }
1243
+ return result;
1244
+ } catch (e) {
1245
+ result.errors.push(`manifest existente ileg\xEDvel: ${e?.message ?? e}`);
1246
+ return result;
1247
+ }
1248
+ }
1249
+ const collected = collectFiles(frontendDir);
1250
+ const pageTexts = collectPageTexts(frontendDir);
1251
+ result.filesAnalyzed = [
1252
+ ...collected.files.map((f) => f.rel),
1253
+ ...pageTexts.map((p) => p.rel)
1254
+ ];
1255
+ if (collected.files.length === 0 && pageTexts.length === 0) {
1256
+ result.errors.push(
1257
+ "N\xE3o encontrei dados nem textos para analisar. Adicione um strapi.manifest.json manualmente."
1258
+ );
1259
+ return result;
1260
+ }
1261
+ const envList = [
1262
+ framework === "next" ? "NEXT_PUBLIC_STRAPI_URL" : "VITE_STRAPI_URL",
1263
+ "STRAPI_API_TOKEN",
1264
+ "PREVIEW_SECRET"
1265
+ ];
1266
+ let dataCts = [];
1267
+ let dataSeed = [];
1268
+ const apiKey = process.env.OPENAI_API_KEY;
1269
+ if (apiKey && collected.files.length) {
1270
+ const callOpenAI = async (messages2) => {
1271
+ const res = await fetch(OPENAI_URL, {
1272
+ method: "POST",
1273
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1274
+ body: JSON.stringify({ model: MODEL, temperature: 0, response_format: { type: "json_object" }, messages: messages2 })
1275
+ });
1276
+ if (!res.ok) throw new Error(`OpenAI: ${await res.text()}`);
1277
+ return res.json();
1278
+ };
1279
+ const messages = [
1280
+ { role: "system", content: "Voc\xEA projeta modelos de conte\xFAdo Strapi 5 e responde s\xF3 com JSON v\xE1lido." },
1281
+ { role: "user", content: buildPrompt(framework, collected, opts.name) }
1282
+ ];
1283
+ try {
1284
+ for (let attempt = 0; attempt < 2; attempt++) {
1285
+ const data = await callOpenAI(messages);
1286
+ const raw = JSON.parse(data.choices?.[0]?.message?.content ?? "{}");
1287
+ const candidate = {
1288
+ manifestVersion: 1,
1289
+ name: opts.name,
1290
+ framework,
1291
+ strapiVersion: "^5.47",
1292
+ contentTypes: Array.isArray(raw.contentTypes) ? raw.contentTypes : [],
1293
+ seed: Array.isArray(raw.seed) ? raw.seed : [],
1294
+ env: envList
1295
+ };
1296
+ if (!candidate.contentTypes.length) break;
1297
+ const v2 = validateManifest(candidate);
1298
+ if (v2.ok) {
1299
+ dataCts = v2.data.contentTypes;
1300
+ dataSeed = v2.data.seed ?? [];
1301
+ break;
1302
+ }
1303
+ if (attempt === 0) {
1304
+ messages.push({ role: "assistant", content: JSON.stringify(raw) });
1305
+ messages.push({ role: "user", content: "JSON REJEITADO pela valida\xE7\xE3o:\n" + (v2.errors ?? []).join("\n") + "\nCorrija e responda s\xF3 com o JSON." });
1306
+ } else {
1307
+ result.warnings.push("Modelo de dados (IA) inv\xE1lido \u2014 seguindo s\xF3 com os textos.");
1308
+ }
1309
+ }
1310
+ } catch (e) {
1311
+ result.warnings.push(`IA de dados indispon\xEDvel (seguindo s\xF3 com os textos): ${e?.message ?? e}`);
1312
+ }
1313
+ } else if (!apiKey) {
1314
+ result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
1315
+ }
1316
+ const budget = 60 - dataCts.length - 1;
1317
+ const page = buildPageContentTypes(pageTexts, budget);
1318
+ const finalManifest = {
1319
+ manifestVersion: 1,
1320
+ name: opts.name,
1321
+ framework,
1322
+ strapiVersion: "^5.47",
1323
+ contentTypes: [...dataCts, ...page.contentTypes],
1324
+ seed: [...dataSeed, ...page.seed],
1325
+ env: envList
1326
+ };
1327
+ result.rawManifest = finalManifest;
1328
+ let v = validateManifest(finalManifest);
1329
+ if (!v.ok) {
1330
+ result.warnings.push("Manifest combinado inv\xE1lido; caindo para s\xF3-textos. " + (v.errors ?? []).join("; "));
1331
+ const pageOnly = { ...finalManifest, contentTypes: page.contentTypes, seed: page.seed };
1332
+ result.rawManifest = pageOnly;
1333
+ v = validateManifest(pageOnly);
1334
+ }
1335
+ if (v.ok) {
1336
+ result.ok = true;
1337
+ result.manifest = v.data;
1338
+ } else {
1339
+ result.errors.push(...v.errors ?? []);
1340
+ }
1341
+ return result;
1342
+ }
1343
+
1344
+ // server/src/provision/runner.ts
1345
+ var import_node_child_process = require("node:child_process");
1346
+ var import_node_net = __toESM(require("node:net"));
1347
+ var import_node_fs5 = __toESM(require("node:fs"));
1348
+ var import_node_path5 = __toESM(require("node:path"));
1349
+ var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
1350
+ var child = null;
1351
+ var pollTimer = null;
1352
+ function detectPM(dir) {
1353
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lockb")) || import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lock"))) return "bun";
1354
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
1355
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
1356
+ return "npm";
1357
+ }
1358
+ var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
1359
+ function detectFramework2(dir) {
1360
+ if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
1361
+ if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
1362
+ return "other";
1363
+ }
1364
+ var FRONTEND_BASE_PORT = 4321;
1365
+ function findFreePort(start) {
1366
+ return new Promise((resolve) => {
1367
+ const tryPort = (p) => {
1368
+ if (p > start + 200) return resolve(start);
1369
+ const srv = import_node_net.default.createServer();
1370
+ srv.once("error", () => tryPort(p + 1));
1371
+ srv.once("listening", () => srv.close(() => resolve(p)));
1372
+ srv.listen(p, "0.0.0.0");
1373
+ };
1374
+ tryPort(start);
1375
+ });
1376
+ }
1377
+ function pushLog(s) {
1378
+ for (const line of String(s).split("\n")) {
1379
+ const t = line.trim();
1380
+ if (t) info.log.push(t);
1381
+ }
1382
+ if (info.log.length > 60) info.log = info.log.slice(-60);
1383
+ }
1384
+ async function urlUp(url) {
1385
+ try {
1386
+ const res = await fetch(url, { method: "GET" });
1387
+ return res.status >= 200 && res.status < 400;
1388
+ } catch {
1389
+ return false;
1390
+ }
1391
+ }
1392
+ function getRunStatus() {
1393
+ return { ...info, log: info.log.slice(-15) };
1394
+ }
1395
+ function stopFrontend() {
1396
+ if (pollTimer) {
1397
+ clearInterval(pollTimer);
1398
+ pollTimer = null;
1399
+ }
1400
+ if (child) {
1401
+ try {
1402
+ child.kill("SIGTERM");
1403
+ } catch {
1404
+ }
1405
+ child = null;
1406
+ }
1407
+ if (info.state !== "error") info.state = "idle";
1408
+ }
1409
+ async function startFrontend(_strapi, opts) {
1410
+ const { dir } = opts;
1411
+ if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
1412
+ return getRunStatus();
1413
+ }
1414
+ stopFrontend();
1415
+ const pm = detectPM(dir);
1416
+ const framework = detectFramework2(dir);
1417
+ const port = await findFreePort(FRONTEND_BASE_PORT);
1418
+ const url = `http://127.0.0.1:${port}`;
1419
+ info = { state: "installing", dir, url, pm, error: null, log: [] };
1420
+ const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
1421
+ const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
1422
+ const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
1423
+ const startDev = () => {
1424
+ info.state = "starting";
1425
+ child = spawnIn(pm, devArgs);
1426
+ child.stdout?.on("data", (d) => pushLog(d));
1427
+ child.stderr?.on("data", (d) => pushLog(d));
1428
+ child.on("exit", (code) => {
1429
+ child = null;
1430
+ if (pollTimer) {
1431
+ clearInterval(pollTimer);
1432
+ pollTimer = null;
1433
+ }
1434
+ if (info.state === "running") info.state = "idle";
1435
+ else {
1436
+ info.state = "error";
1437
+ info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
1438
+ }
1439
+ });
1440
+ pollTimer = setInterval(async () => {
1441
+ if (await urlUp(url)) {
1442
+ info.state = "running";
1443
+ if (pollTimer) {
1444
+ clearInterval(pollTimer);
1445
+ pollTimer = null;
1446
+ }
1447
+ }
1448
+ }, 1500);
1449
+ };
1450
+ const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
1451
+ if (needInstall) {
1452
+ pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
1453
+ const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
1454
+ const inst = spawnIn(pm, installArgs);
1455
+ inst.stdout?.on("data", (d) => pushLog(d));
1456
+ inst.stderr?.on("data", (d) => pushLog(d));
1457
+ inst.on("exit", (code) => {
1458
+ if (code === 0) startDev();
1459
+ else {
1460
+ info.state = "error";
1461
+ info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
1462
+ }
1463
+ });
1464
+ } else {
1465
+ startDev();
1466
+ }
1467
+ return getRunStatus();
1468
+ }
1469
+
1470
+ // server/src/provision/integrate.ts
1471
+ var import_node_fs6 = __toESM(require("node:fs"));
1472
+ var import_node_path6 = __toESM(require("node:path"));
1473
+ var import_node_child_process2 = require("node:child_process");
1474
+ var OPENAI_URL2 = "https://api.openai.com/v1/chat/completions";
1475
+ var MODEL2 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1476
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
1477
+ "node_modules",
1478
+ ".git",
1479
+ "dist",
1480
+ ".next",
1481
+ ".output",
1482
+ ".vinxi",
1483
+ ".tanstack",
1484
+ "build",
1485
+ "coverage",
1486
+ ".turbo",
1487
+ ".cache",
1488
+ "public"
1489
+ ]);
1490
+ var CODE_EXT2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs"]);
1491
+ var MAX_DATA_FILES = 3;
1492
+ var MAX_FILE_CHARS2 = 16e3;
1493
+ function score2(rel) {
1494
+ const p = rel.toLowerCase();
1495
+ let s = 0;
1496
+ if (/(^|\/)(data|content|mocks?|seeds?|fixtures?)(\/|\.)/.test(p)) s += 10;
1497
+ if (/(site|config|constants|catalog|products?|services?|posts?|items?)/.test(p)) s += 4;
1498
+ if (p.startsWith("src/")) s += 2;
1499
+ if (p.endsWith(".tsx") || p.endsWith(".jsx")) s -= 3;
1500
+ return s;
1501
+ }
1502
+ function walk2(dir, base, out) {
1503
+ let entries;
1504
+ try {
1505
+ entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
1506
+ } catch {
1507
+ return;
1508
+ }
1509
+ for (const e of entries) {
1510
+ if (e.name.startsWith(".")) continue;
1511
+ const full = import_node_path6.default.join(dir, e.name);
1512
+ if (e.isDirectory()) {
1513
+ if (SKIP_DIRS2.has(e.name)) continue;
1514
+ walk2(full, base, out);
1515
+ } else if (CODE_EXT2.has(import_node_path6.default.extname(e.name))) {
1516
+ out.push(import_node_path6.default.relative(base, full));
1517
+ }
1518
+ }
1519
+ }
1520
+ function findDataFiles(frontendDir) {
1521
+ const all = [];
1522
+ walk2(frontendDir, frontendDir, all);
1523
+ return all.map((rel) => ({ rel, s: score2(rel) })).filter((x) => x.s > 0).filter(({ rel }) => {
1524
+ try {
1525
+ const c = import_node_fs6.default.readFileSync(import_node_path6.default.join(frontendDir, rel), "utf8");
1526
+ return /export\s+const\s+\w+\s*[:=]\s*(\[|\{)/.test(c);
1527
+ } catch {
1528
+ return false;
1529
+ }
1530
+ }).sort((a, b) => b.s - a.s).slice(0, MAX_DATA_FILES).map((x) => x.rel);
1531
+ }
1532
+ async function getLocales(strapi) {
1533
+ try {
1534
+ const svc = strapi.plugin("i18n").service("locales");
1535
+ const all = await svc.find() || [];
1536
+ const def = await svc.getDefaultLocale() || "en";
1537
+ const codes = all.map((l) => l.code);
1538
+ return { codes, def };
1539
+ } catch {
1540
+ return { codes: [], def: "en" };
1541
+ }
1542
+ }
1543
+ function extractImports(src) {
1544
+ return src.split("\n").filter((l) => /^\s*import\s.+from\s+['"].+['"];?\s*$/.test(l)).join("\n");
1545
+ }
1546
+ function exportNames(src) {
1547
+ const names = [];
1548
+ const re = /export\s+const\s+(\w+)\b/g;
1549
+ let m;
1550
+ while (m = re.exec(src)) names.push(m[1]);
1551
+ return names;
1552
+ }
1553
+ function stripFence(s) {
1554
+ const m = s.match(/```[a-zA-Z]*[ \t]*\r?\n([\s\S]*?)```/);
1555
+ let out = (m ? m[1] : s).trim();
1556
+ out = out.replace(/^\s*(?:javascript|typescript|tsx?|jsx?)\s*\n/i, "");
1557
+ return out.trim();
1558
+ }
1559
+ var STRAPI_CLIENT_TS = `// Gerado pelo mcp-chat \u2014 client oficial @strapi/client.
1560
+ import { strapi } from "@strapi/client";
1561
+
1562
+ function baseUrl(): string {
1563
+ // Vite (TanStack) exp\xF5e via import.meta.env; Next via process.env.
1564
+ try {
1565
+ // @ts-ignore
1566
+ const v = import.meta?.env?.VITE_STRAPI_URL;
1567
+ if (v) return v;
1568
+ } catch {}
1569
+ if (typeof process !== "undefined") {
1570
+ const p = process.env.NEXT_PUBLIC_STRAPI_URL || process.env.STRAPI_URL;
1571
+ if (p) return p;
1572
+ }
1573
+ return "http://localhost:1337";
1574
+ }
1575
+ const token = typeof process !== "undefined" ? process.env.STRAPI_API_TOKEN : undefined;
1576
+
1577
+ const client = strapi({ baseURL: baseUrl().replace(/\\/$/, "") + "/api", ...(token ? { auth: token } : {}) });
1578
+
1579
+ export type FetchOpts = { locale?: string; status?: "draft" | "published" };
1580
+ const params = (o: FetchOpts) => ({ populate: "*" as const, ...(o.locale ? { locale: o.locale } : {}), ...(o.status ? { status: o.status } : {}) });
1581
+
1582
+ /** Cole\xE7\xE3o (usa o pluralName). Retorna o array de documentos (shape flat v5). */
1583
+ export async function fetchCollection(plural: string, o: FetchOpts = {}): Promise<any[]> {
1584
+ const r = await client.collection(plural).find(params(o));
1585
+ return (r as any).data ?? [];
1586
+ }
1587
+ /** Single type (usa o singularName). Retorna o documento. */
1588
+ export async function fetchSingle(singular: string, o: FetchOpts = {}): Promise<any> {
1589
+ const r = await client.single(singular).find(params(o));
1590
+ return (r as any).data ?? null;
1591
+ }
1592
+ `;
1593
+ async function ensureClientDep(frontendDir, warnings) {
1594
+ if (import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "node_modules", "@strapi", "client"))) return;
1595
+ try {
1596
+ const pkgPath = import_node_path6.default.join(frontendDir, "package.json");
1597
+ const pkg = JSON.parse(import_node_fs6.default.readFileSync(pkgPath, "utf8"));
1598
+ pkg.dependencies = pkg.dependencies || {};
1599
+ if (!pkg.dependencies["@strapi/client"]) {
1600
+ pkg.dependencies["@strapi/client"] = "^1.6.2";
1601
+ import_node_fs6.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
1602
+ }
1603
+ } catch (e) {
1604
+ warnings.push(`package.json do frontend: ${e?.message ?? e}`);
1605
+ }
1606
+ const pm = import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "bun.lock")) || import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "bun.lockb")) ? "bun" : import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "pnpm-lock.yaml")) ? "pnpm" : import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, "yarn.lock")) ? "yarn" : "npm";
1607
+ const args = pm === "npm" ? ["install", "@strapi/client", "--no-audit", "--no-fund"] : ["add", "@strapi/client"];
1608
+ await new Promise((resolve) => {
1609
+ try {
1610
+ const c = (0, import_node_child_process2.spawn)(pm, args, { cwd: frontendDir, stdio: "ignore" });
1611
+ c.on("exit", () => resolve());
1612
+ c.on("error", () => {
1613
+ warnings.push(`n\xE3o consegui instalar @strapi/client (${pm}); instale manualmente.`);
1614
+ resolve();
1615
+ });
1616
+ } catch {
1617
+ warnings.push("falha ao instalar @strapi/client; instale manualmente.");
1618
+ resolve();
1619
+ }
1620
+ });
1621
+ }
1622
+ async function fetchSample(strapi, cts) {
1623
+ const sample = {};
1624
+ for (const ct of cts) {
1625
+ const uid = apiUid(ct.singularName);
1626
+ try {
1627
+ if (ct.kind === "singleType") {
1628
+ sample[ct.singularName] = await strapi.documents(uid).findFirst({ status: "published", populate: "*" });
1629
+ } else {
1630
+ const docs = await strapi.documents(uid).findMany({ status: "published", populate: "*", limit: 2 });
1631
+ sample[ct.singularName] = Array.isArray(docs) ? docs : [];
1632
+ }
1633
+ } catch {
1634
+ sample[ct.singularName] = ct.kind === "singleType" ? null : [];
1635
+ }
1636
+ }
1637
+ return sample;
1638
+ }
1639
+ async function generateMapper(apiKey, baseSrc, sample, assetIds) {
1640
+ const prompt = `Gere uma FUN\xC7\xC3O TypeScript pura chamada exatamente \`mapStrapiToData(raw: Record<string, any>)\` que recebe os dados da Content API do Strapi e retorna um objeto com EXATAMENTE os mesmos exports (mesmas chaves e MESMO shape) do arquivo de dados abaixo.
1641
+
1642
+ REGRAS:
1643
+ - A fun\xE7\xE3o retorna um objeto cujas chaves s\xE3o os nomes dos \`export const\` do arquivo (ex.: site, services, projects\u2026), cada um no MESMO formato que o arquivo original.
1644
+ - \`raw\` tem uma chave por content-type (singularName): cole\xE7\xF5es s\xE3o arrays de documentos; single types s\xE3o um objeto. Documentos v\xEAm no shape FLAT do Strapi 5 (campos no topo: documentId, e os atributos diretamente).
1645
+ - Mapeie os campos do Strapi para os campos esperados pelo arquivo (casando por nome/significado). Para listas, use \`(raw.x ?? []).map(...)\`.
1646
+ - IMAGENS: o campo pode ser objeto de m\xEDdia com \`.url\`, uma string, ou null. Use \`(doc?.campo?.url ?? doc?.campo)\` quando houver; SEN\xC3O use o asset importado original como fallback. Assets importados dispon\xEDveis (use como vari\xE1veis): ${assetIds.join(", ") || "(nenhum)"}.
1647
+ - DEFENSIVO (OBRIGAT\xD3RIO \u2014 o SSR n\xE3o pode quebrar): use SEMPRE optional chaining \`?.\` e defaults \`??\`. NUNCA chame m\xE9todos (\`.replace\`, \`.map\`, \`.split\`, etc.) em valores que possam ser null/undefined \u2014 guarde antes: \`(x ?? "").replace(...)\`, \`(arr ?? []).map(...)\`. Todo acesso a sub-campo deve tolerar aus\xEAncia.
1648
+ - N\xE3o invente conte\xFAdo; campo ausente \u2192 default sensato (string vazia, array vazio, ou o fallback de imagem).
1649
+ - ISOLAMENTO POR EXPORT (OBRIGAT\xD3RIO): construa o resultado com CADA export no seu pr\xF3prio try/catch, para que uma falha num export N\xC3O derrube os outros. Padr\xE3o EXATO:
1650
+ \`function mapStrapiToData(raw) { const out = {}; try { out.site = (/* ... */); } catch { out.site = {}; } try { out.services = (raw.service ?? []).map((s) => (/* ... */)); } catch { out.services = []; } /* ...um try/catch por export... */ return out; }\`
1651
+ - Responda APENAS com o c\xF3digo da fun\xE7\xE3o (sem imports, sem markdown, sem exports \u2014 s\xF3 \`function mapStrapiToData(raw) { ... }\`).
1652
+
1653
+ ARQUIVO DE DADOS ORIGINAL (shape alvo):
1654
+ ${baseSrc.length > MAX_FILE_CHARS2 ? baseSrc.slice(0, MAX_FILE_CHARS2) : baseSrc}
1655
+
1656
+ AMOSTRA REAL DA RESPOSTA DO STRAPI (JSON, por singularName):
1657
+ ${JSON.stringify(sample, null, 1).slice(0, 18e3)}`;
1658
+ const res = await fetch(OPENAI_URL2, {
1659
+ method: "POST",
1660
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1661
+ body: JSON.stringify({
1662
+ model: MODEL2,
1663
+ temperature: 0,
1664
+ messages: [
1665
+ { role: "system", content: "Voc\xEA gera uma fun\xE7\xE3o pura de mapeamento Strapi\u2192shape do frontend. Responde s\xF3 com a fun\xE7\xE3o." },
1666
+ { role: "user", content: prompt }
1667
+ ]
1668
+ })
1669
+ });
1670
+ if (!res.ok) throw new Error(await res.text());
1671
+ const json = await res.json();
1672
+ return stripFence(json.choices?.[0]?.message?.content ?? "");
1673
+ }
1674
+ function assetImportIds(src) {
1675
+ const ids = [];
1676
+ const re = /import\s+(\w+)\s+from\s+['"][^'"]+['"]/g;
1677
+ let m;
1678
+ while (m = re.exec(src)) ids.push(m[1]);
1679
+ return ids;
1680
+ }
1681
+ function buildLiveDataModule(baseSrc, mapperCode, cts, locales, defLocale) {
1682
+ const imports = extractImports(baseSrc).split("\n").filter((l) => /@\/assets|\.(png|jpe?g|svg|webp|gif)/i.test(l)).join("\n");
1683
+ const names = exportNames(baseSrc);
1684
+ const ctMeta = JSON.stringify(cts.map((c) => ({ s: c.singularName, p: c.pluralName, k: c.kind })));
1685
+ const camel = (s) => s.replace(/-+([a-zA-Z0-9])/g, (_m, c) => c.toUpperCase());
1686
+ const singleMap = {};
1687
+ for (const c of cts) {
1688
+ if (c.kind !== "singleType") continue;
1689
+ const e = camel(c.singularName);
1690
+ if (names.includes(e)) continue;
1691
+ singleMap[c.singularName] = e;
1692
+ }
1693
+ const pageExports = Object.values(singleMap).map((e) => `export const ${e}: any = __live(${JSON.stringify(e)});`).join("\n");
1694
+ const liveExports = names.map((n) => `export const ${n}: any = __live(${JSON.stringify(n)});`).join("\n") + (pageExports ? "\n" + pageExports : "");
1695
+ return `// Live data from Strapi Content API (gerado pelo mcp-chat)
1696
+ ${imports}
1697
+ import { fetchCollection, fetchSingle } from "./strapi-client";
1698
+
1699
+ const __cts = ${ctMeta};
1700
+ const __single: Record<string, string> = ${JSON.stringify(singleMap)};
1701
+ export const __availableLocales = ${JSON.stringify(locales)};
1702
+ export const __defaultLocale = ${JSON.stringify(defLocale)};
1703
+ /** Locale ativo a partir de ?locale/cookie (cliente) \u2014 p/ o seletor. */
1704
+ export function __getLocale(): string {
1705
+ try {
1706
+ if (typeof window !== "undefined") {
1707
+ const u = new URL(window.location.href).searchParams.get("locale");
1708
+ if (u) return u;
1709
+ const m = document.cookie.match(/(?:^|;\\s*)site-locale=([^;]+)/);
1710
+ if (m) return decodeURIComponent(m[1]);
1711
+ }
1712
+ } catch {}
1713
+ return __defaultLocale;
1714
+ }
1715
+
1716
+ ${mapperCode}
1717
+
1718
+ const __store: Record<string, any> = {};
1719
+ export function hydrate(d: any) { if (d) for (const k of Object.keys(d)) __store[k] = d[k]; }
1720
+
1721
+ export async function loadAllData(opts: { locale?: string; status?: "draft" | "published" } = {}) {
1722
+ const raw: Record<string, any> = {};
1723
+ await Promise.all(
1724
+ __cts.map(async (c: any) => {
1725
+ try {
1726
+ raw[c.s] = c.k === "singleType" ? await fetchSingle(c.s, opts) : await fetchCollection(c.p, opts);
1727
+ } catch {
1728
+ raw[c.s] = c.k === "singleType" ? null : [];
1729
+ }
1730
+ })
1731
+ );
1732
+ let data: any = {};
1733
+ try { data = mapStrapiToData(raw) || {}; }
1734
+ catch (e) { if (typeof console !== "undefined") console.error("[mcp-chat] mapStrapiToData falhou:", e); }
1735
+ hydrate(data);
1736
+ // exp\xF5e o conte\xFAdo de p\xE1gina (single types) bruto/traduzido p/ os componentes.
1737
+ for (const s of Object.keys(__single)) __store[__single[s]] = raw[s] || {};
1738
+ return data;
1739
+ }
1740
+
1741
+ // Exports "vivos": leem o store hidratado pelo loader (sem mudar os componentes).
1742
+ function __live(key: string): any {
1743
+ return new Proxy(function () {} as any, {
1744
+ get(_t, p) {
1745
+ const v = __store[key];
1746
+ const r = v == null ? (Array.isArray(__fallback(key)) ? [] : undefined) : (v as any)[p as any];
1747
+ return typeof r === "function" ? r.bind(v) : r;
1748
+ },
1749
+ has(_t, p) { const v = __store[key]; return v != null && (p in (v as any)); },
1750
+ ownKeys() { const v = __store[key]; return v ? Reflect.ownKeys(v as any) : []; },
1751
+ getOwnPropertyDescriptor(_t, p) {
1752
+ const v = __store[key]; if (v == null) return undefined;
1753
+ const d = Object.getOwnPropertyDescriptor(v as any, p);
1754
+ if (d) (d as any).configurable = true;
1755
+ return d;
1756
+ },
1757
+ });
1758
+ }
1759
+ function __fallback(_k: string): any { return []; }
1760
+
1761
+ ${liveExports}
1762
+ `;
1763
+ }
1764
+ var switcherTsx = (dataImport) => `// Gerado pelo mcp-chat \u2014 seletor de idioma.
1765
+ import { __availableLocales, __getLocale } from "${dataImport}";
1766
+
1767
+ const LABELS: Record<string, string> = {
1768
+ en: "EN", "pt-BR": "PT", pt: "PT", es: "ES", fr: "FR", de: "DE", it: "IT",
1769
+ nl: "NL", ja: "JA", ko: "KO", ru: "RU", ar: "AR", "zh-Hans": "ZH", zh: "ZH",
1770
+ };
1771
+
1772
+ export function LanguageSwitcher() {
1773
+ if (!__availableLocales || __availableLocales.length < 2) return null;
1774
+ const current = __getLocale();
1775
+ const onChange = (e: any) => {
1776
+ const loc = e.target.value;
1777
+ try { document.cookie = "site-locale=" + loc + ";path=/;max-age=31536000"; } catch {}
1778
+ const u = new URL(window.location.href);
1779
+ u.searchParams.set("locale", loc);
1780
+ window.location.href = u.toString();
1781
+ };
1782
+ return (
1783
+ <select
1784
+ aria-label="Language"
1785
+ value={current}
1786
+ onChange={onChange}
1787
+ style={{
1788
+ border: "1px solid rgba(0,0,0,.15)", borderRadius: 9999, padding: "4px 10px",
1789
+ fontSize: 13, fontWeight: 600, background: "transparent", cursor: "pointer",
1790
+ }}
1791
+ >
1792
+ {__availableLocales.map((l: string) => (
1793
+ <option key={l} value={l}>{LABELS[l] || l.toUpperCase()}</option>
1794
+ ))}
1795
+ </select>
1796
+ );
1797
+ }
1798
+
1799
+ export default LanguageSwitcher;
1800
+ `;
1801
+ function injectSwitcher(frontendDir, warnings, dataImport = "@/data/site") {
1802
+ const compDir = import_node_path6.default.join(frontendDir, "src", "components");
1803
+ import_node_fs6.default.mkdirSync(compDir, { recursive: true });
1804
+ import_node_fs6.default.writeFileSync(import_node_path6.default.join(compDir, "LanguageSwitcher.tsx"), switcherTsx(dataImport), "utf8");
1805
+ const rootRel = ["src/routes/__root.tsx", "src/routes/__root.jsx"].find(
1806
+ (r) => import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, r))
1807
+ );
1808
+ if (rootRel) {
1809
+ const abs = import_node_path6.default.join(frontendDir, rootRel);
1810
+ let src = import_node_fs6.default.readFileSync(abs, "utf8");
1811
+ if (!src.includes("loadAllData")) {
1812
+ src = `import { loadAllData } from "${dataImport}";
1813
+ ` + src;
1814
+ const m = src.match(/createRootRoute\w*\s*(?:<[\s\S]*?>)?\s*\([\s\S]*?\)\s*\(\s*\{/);
1815
+ if (m) {
1816
+ const at = m.index + m[0].length;
1817
+ src = src.slice(0, at) + `
1818
+ validateSearch: (s: Record<string, unknown>) => ({ locale: typeof s.locale === "string" ? s.locale : undefined }),
1819
+ loaderDeps: ({ search }: any) => ({ locale: search.locale }),
1820
+ loader: async ({ deps }: any) => { const data = await loadAllData({ locale: deps?.locale }); return { data }; },` + src.slice(at);
1821
+ } else {
1822
+ warnings.push("n\xE3o consegui injetar o loader no __root (padr\xE3o n\xE3o encontrado).");
1823
+ }
1824
+ import_node_fs6.default.writeFileSync(abs, src, "utf8");
1825
+ }
1826
+ } else {
1827
+ warnings.push("__root n\xE3o encontrado \u2014 dados ao vivo n\xE3o ligados ao SSR.");
1828
+ }
1829
+ const headerRel = ["src/components/Header.tsx", "src/components/Header.jsx"].find(
1830
+ (r) => import_node_fs6.default.existsSync(import_node_path6.default.join(frontendDir, r))
1831
+ );
1832
+ if (headerRel) {
1833
+ const abs = import_node_path6.default.join(frontendDir, headerRel);
1834
+ let src = import_node_fs6.default.readFileSync(abs, "utf8");
1835
+ if (!src.includes("LanguageSwitcher")) {
1836
+ src = `import { LanguageSwitcher } from "@/components/LanguageSwitcher";
1837
+ ` + src;
1838
+ if (/<button[^>]*lg:hidden/.test(src)) {
1839
+ src = src.replace(/(\s*)(<button[^>]*lg:hidden)/, `$1<LanguageSwitcher />$1$2`);
1840
+ } else {
1841
+ src = src.replace(/<\/header>/, ` <LanguageSwitcher />
1842
+ </header>`);
1843
+ }
1844
+ import_node_fs6.default.writeFileSync(abs, src, "utf8");
1845
+ }
1846
+ } else {
1847
+ warnings.push("Header n\xE3o encontrado \u2014 adicione <LanguageSwitcher/> manualmente.");
1848
+ }
1849
+ }
1850
+ var SYS_FIELDS = /* @__PURE__ */ new Set(["id", "documentId", "createdAt", "updatedAt", "publishedAt", "locale", "slug", "url"]);
1851
+ function buildValueMap(sample, cts) {
1852
+ const camel = (s) => s.replace(/-+([a-zA-Z0-9])/g, (_m, c) => c.toUpperCase());
1853
+ const map = {};
1854
+ for (const ct of cts) {
1855
+ if (ct.kind !== "singleType") continue;
1856
+ const doc = sample[ct.singularName];
1857
+ if (!doc || typeof doc !== "object") continue;
1858
+ const exp = camel(ct.singularName);
1859
+ for (const [field, val] of Object.entries(doc)) {
1860
+ if (SYS_FIELDS.has(field)) continue;
1861
+ if (typeof val !== "string") continue;
1862
+ const v = val.trim();
1863
+ if (v.length < 4) continue;
1864
+ if (/^https?:\/\//.test(v)) continue;
1865
+ if (map[v]) continue;
1866
+ map[v] = `${exp}?.${field} ?? ${JSON.stringify(v)}`;
1867
+ }
1868
+ }
1869
+ return map;
1870
+ }
1871
+ async function generateRewire(apiKey, src, subset, dataImport) {
1872
+ const pairs = Object.entries(subset).map(([v, expr]) => `- ${JSON.stringify(v)} \u2192 {${expr}}`).join("\n");
1873
+ const prompt = `Religue este componente React/JSX ao CMS, trocando textos hardcoded por express\xF5es que leem do Strapi (j\xE1 localizadas).
1874
+
1875
+ MAPA (texto exato no arquivo \u2192 express\xE3o a usar):
1876
+ ${pairs}
1877
+
1878
+ REGRAS ESTRITAS:
1879
+ - Para CADA ocorr\xEAncia EXATA de um texto do mapa, substitua pela express\xE3o, no formato correto do contexto:
1880
+ \u2022 n\xF3 de texto JSX: Texto \u2192 {EXPR}
1881
+ \u2022 atributo string: attr="Texto" \u2192 attr={EXPR}
1882
+ \u2022 string JS usada como conte\xFAdo: "Texto" \u2192 (EXPR)
1883
+ - A EXPR j\xE1 tem fallback (?? "texto original"); use-a como veio (entre {} no JSX).
1884
+ - Adicione os imports necess\xE1rios no topo: importe os s\xEDmbolos usados (ex.: ${dataImport.includes("~") ? "~" : "@"}/data/site). Os exports de conte\xFAdo s\xE3o camelCase (ex.: homeContent).
1885
+ - N\xC3O altere mais NADA: estrutura, classes, l\xF3gica, imports existentes, nada fora do mapa.
1886
+ - Responda APENAS com o arquivo final (sem markdown).
1887
+
1888
+ ARQUIVO:
1889
+ ${src.length > MAX_FILE_CHARS2 ? src.slice(0, MAX_FILE_CHARS2) : src}`;
1890
+ const res = await fetch(OPENAI_URL2, {
1891
+ method: "POST",
1892
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1893
+ body: JSON.stringify({
1894
+ model: MODEL2,
1895
+ temperature: 0,
1896
+ messages: [
1897
+ { role: "system", content: "Voc\xEA religa componentes ao CMS trocando s\xF3 os textos do mapa por express\xF5es. Responde s\xF3 com o c\xF3digo." },
1898
+ { role: "user", content: prompt }
1899
+ ]
1900
+ })
1901
+ });
1902
+ if (!res.ok) throw new Error(await res.text());
1903
+ const json = await res.json();
1904
+ return stripFence(json.choices?.[0]?.message?.content ?? "");
1905
+ }
1906
+ async function syntaxOk(code) {
1907
+ let esbuild;
1908
+ try {
1909
+ esbuild = require("esbuild");
1910
+ } catch {
1911
+ esbuild = null;
1912
+ }
1913
+ if (esbuild?.transform) {
1914
+ try {
1915
+ await esbuild.transform(code, { loader: "tsx" });
1916
+ return true;
1917
+ } catch {
1918
+ return false;
1919
+ }
1920
+ }
1921
+ let depthC = 0, depthB = 0, depthP = 0, inStr = null, esc = false;
1922
+ for (let i = 0; i < code.length; i++) {
1923
+ const c = code[i];
1924
+ if (inStr) {
1925
+ if (esc) esc = false;
1926
+ else if (c === "\\") esc = true;
1927
+ else if (c === inStr) inStr = null;
1928
+ continue;
1929
+ }
1930
+ if (c === '"' || c === "'" || c === "`") inStr = c;
1931
+ else if (c === "{") depthC++;
1932
+ else if (c === "}") depthC--;
1933
+ else if (c === "[") depthB++;
1934
+ else if (c === "]") depthB--;
1935
+ else if (c === "(") depthP++;
1936
+ else if (c === ")") depthP--;
1937
+ if (depthC < 0 || depthB < 0 || depthP < 0) return false;
1938
+ }
1939
+ return depthC === 0 && depthB === 0 && depthP === 0 && /export\s/.test(code);
1940
+ }
1941
+ function normalizeDataImports(code, dataImport) {
1942
+ const base = dataImport.replace(/\/[^/]+$/, "");
1943
+ const esc = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1944
+ const re = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*["']${esc}/[^"']*["'];?[ \\t]*\\n?`, "g");
1945
+ const specs = /* @__PURE__ */ new Set();
1946
+ let m;
1947
+ while (m = re.exec(code)) {
1948
+ for (const s of m[1].split(",")) {
1949
+ const t = s.trim();
1950
+ if (t) specs.add(t);
1951
+ }
1952
+ }
1953
+ if (!specs.size) return code;
1954
+ const out = code.replace(re, "");
1955
+ return `import { ${[...specs].join(", ")} } from "${dataImport}";
1956
+ ` + out;
1957
+ }
1958
+ async function rewireComponents(strapi, opts, warnings) {
1959
+ const map = buildValueMap(opts.sample, opts.cts);
1960
+ if (!Object.keys(map).length) return [];
1961
+ const rewired = [];
1962
+ const files = [];
1963
+ walk2(opts.frontendDir, opts.frontendDir, files);
1964
+ const targets = files.filter(
1965
+ (rel) => /\.(tsx|jsx)$/.test(rel) && !/__root|routeTree\.gen|\/api\/|LanguageSwitcher|PreviewBridge|\/ui\//.test(rel)
1966
+ );
1967
+ for (const rel of targets) {
1968
+ const abs = import_node_path6.default.join(opts.frontendDir, rel);
1969
+ let src;
1970
+ try {
1971
+ src = import_node_fs6.default.readFileSync(abs, "utf8");
1972
+ } catch {
1973
+ continue;
1974
+ }
1975
+ const subset = {};
1976
+ for (const [v, expr] of Object.entries(map)) if (src.includes(v)) subset[v] = expr;
1977
+ if (!Object.keys(subset).length) continue;
1978
+ try {
1979
+ let out = await generateRewire(opts.apiKey, src, subset, opts.dataImport);
1980
+ if (!out || out.length < 30) {
1981
+ warnings.push(`${rel}: rewire vazio, pulado.`);
1982
+ continue;
1983
+ }
1984
+ out = normalizeDataImports(out, opts.dataImport);
1985
+ if (!await syntaxOk(out)) {
1986
+ warnings.push(`${rel}: rewire com erro de sintaxe, mantido original.`);
1987
+ continue;
1988
+ }
1989
+ const bak = abs + ".bak";
1990
+ if (!import_node_fs6.default.existsSync(bak)) import_node_fs6.default.writeFileSync(bak, src, "utf8");
1991
+ import_node_fs6.default.writeFileSync(abs, out, "utf8");
1992
+ rewired.push(rel);
1993
+ } catch (e) {
1994
+ warnings.push(`${rel}: rewire falhou (${e?.message ?? e}).`);
1995
+ }
1996
+ }
1997
+ return rewired;
1998
+ }
1999
+ async function integrateFrontend(strapi, opts) {
2000
+ const result = {
2001
+ ok: false,
2002
+ filesRewritten: [],
2003
+ contentTypesFetched: [],
2004
+ warnings: [],
2005
+ errors: []
2006
+ };
2007
+ const apiKey = process.env.OPENAI_API_KEY;
2008
+ if (!apiKey) {
2009
+ result.errors.push("OPENAI_API_KEY n\xE3o configurada no .env do Strapi.");
2010
+ return result;
2011
+ }
2012
+ const dataFiles = findDataFiles(opts.frontendDir);
2013
+ if (dataFiles.length === 0) {
2014
+ result.errors.push("N\xE3o encontrei um arquivo de dados para sincronizar (ex.: src/data/site.ts).");
2015
+ return result;
2016
+ }
2017
+ const { codes, def } = await getLocales(strapi);
2018
+ const locales = codes.length ? codes : [def];
2019
+ const ctMeta = opts.manifest.contentTypes.map((ct) => ({
2020
+ singularName: ct.singularName,
2021
+ pluralName: strapi?.contentTypes?.[apiUid(ct.singularName)]?.info?.pluralName || `${ct.singularName}s`,
2022
+ kind: ct.kind
2023
+ }));
2024
+ const sample = await fetchSample(strapi, ctMeta);
2025
+ result.contentTypesFetched = ctMeta.map((c) => ({
2026
+ uid: apiUid(c.singularName),
2027
+ count: Array.isArray(sample[c.singularName]) ? sample[c.singularName].length : sample[c.singularName] ? 1 : 0
2028
+ }));
2029
+ for (const rel of dataFiles) {
2030
+ const abs = import_node_path6.default.join(opts.frontendDir, rel);
2031
+ try {
2032
+ const original = import_node_fs6.default.readFileSync(abs, "utf8");
2033
+ const bak = abs + ".bak";
2034
+ if (!import_node_fs6.default.existsSync(bak)) import_node_fs6.default.writeFileSync(bak, original, "utf8");
2035
+ const baseSrc = import_node_fs6.default.existsSync(bak) ? import_node_fs6.default.readFileSync(bak, "utf8") : original;
2036
+ const mapper = await generateMapper(apiKey, baseSrc, sample, assetImportIds(baseSrc));
2037
+ if (!mapper || !/mapStrapiToData/.test(mapper)) {
2038
+ result.warnings.push(`${rel}: mapeador inv\xE1lido, pulado.`);
2039
+ continue;
2040
+ }
2041
+ const moduleSrc = buildLiveDataModule(baseSrc, mapper, ctMeta, locales, def);
2042
+ import_node_fs6.default.writeFileSync(abs, moduleSrc, "utf8");
2043
+ result.filesRewritten.push(rel);
2044
+ } catch (e) {
2045
+ result.errors.push(`${rel}: ${e?.message ?? e}`);
2046
+ }
2047
+ }
2048
+ if (result.filesRewritten.length > 0) {
2049
+ try {
2050
+ const rel0 = result.filesRewritten[0];
2051
+ const dataDir = import_node_path6.default.dirname(import_node_path6.default.join(opts.frontendDir, rel0));
2052
+ import_node_fs6.default.writeFileSync(import_node_path6.default.join(dataDir, "strapi-client.ts"), STRAPI_CLIENT_TS, "utf8");
2053
+ const dataImport = "@/" + rel0.replace(/^src\//, "").replace(/\.(tsx?|jsx?)$/, "");
2054
+ injectSwitcher(opts.frontendDir, result.warnings, dataImport);
2055
+ await ensureClientDep(opts.frontendDir, result.warnings);
2056
+ try {
2057
+ const rewired = await rewireComponents(
2058
+ strapi,
2059
+ { frontendDir: opts.frontendDir, sample, cts: ctMeta, apiKey, dataImport },
2060
+ result.warnings
2061
+ );
2062
+ if (rewired.length) result.filesRewritten.push(...rewired);
2063
+ } catch (e) {
2064
+ result.warnings.push(`rewire: ${e?.message ?? e}`);
2065
+ }
2066
+ } catch (e) {
2067
+ result.warnings.push(`wiring: ${e?.message ?? e}`);
2068
+ }
2069
+ }
2070
+ result.ok = result.filesRewritten.length > 0;
2071
+ return result;
2072
+ }
2073
+
2074
+ // server/src/controllers/frontend.ts
2075
+ var MANIFEST_NAME = "strapi.manifest.json";
2076
+ function ensureInside2(base, target) {
2077
+ const n = import_node_path7.default.normalize(target);
2078
+ return n === base || n.startsWith(base + import_node_path7.default.sep);
2079
+ }
2080
+ function toKebab(input) {
2081
+ const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
2082
+ return s || "frontend";
2083
+ }
2084
+ function devOnly(ctx) {
2085
+ if (process.env.NODE_ENV !== "development") {
2086
+ ctx.badRequest(
2087
+ "A provis\xE3o de frontend s\xF3 funciona em desenvolvimento (gera\xE7\xE3o de content-types \xE9 dev-only)."
2088
+ );
2089
+ return false;
2090
+ }
2091
+ return true;
2092
+ }
2093
+ var frontend_default = {
2094
+ /** Etapa 1: extrai e descobre/infere o manifest (sem criar nada). */
2095
+ async analyze(ctx) {
2096
+ const strapi = ctx.strapi ?? global.strapi;
2097
+ if (!devOnly(ctx)) return;
2098
+ const files = ctx.request.files || {};
2099
+ const file = files.frontend || files.file || Object.values(files)[0];
2100
+ if (!file) return ctx.badRequest('Envie o .zip do frontend no campo "frontend".');
2101
+ const filepath = file.filepath || file.path;
2102
+ if (!filepath) return ctx.badRequest("Arquivo inv\xE1lido.");
2103
+ const originalName = file.originalFilename || file.name || "frontend";
2104
+ let zip;
2105
+ try {
2106
+ zip = await import_jszip.default.loadAsync(import_node_fs7.default.readFileSync(filepath));
2107
+ } catch (e) {
2108
+ return ctx.badRequest(`Zip inv\xE1lido: ${e?.message ?? e}`);
2109
+ }
2110
+ const entryNames = Object.keys(zip.files);
2111
+ const manifestEntry = entryNames.find(
2112
+ (p) => import_node_path7.default.basename(p) === MANIFEST_NAME && !zip.files[p].dir
2113
+ );
2114
+ let rootPrefix = "";
2115
+ if (manifestEntry) {
2116
+ rootPrefix = manifestEntry.slice(0, manifestEntry.length - MANIFEST_NAME.length);
2117
+ } else {
2118
+ const tops = new Set(
2119
+ entryNames.filter((n) => n.includes("/")).map((n) => n.slice(0, n.indexOf("/") + 1))
2120
+ );
2121
+ if (tops.size === 1) rootPrefix = [...tops][0];
2122
+ }
2123
+ const name = toKebab(originalName);
2124
+ const strapiAppDir = strapi.dirs.app.root;
2125
+ const frontendDir = import_node_path7.default.resolve(strapiAppDir, "..", name);
2126
+ if (import_node_fs7.default.existsSync(frontendDir) && import_node_fs7.default.readdirSync(frontendDir).length > 0) {
2127
+ return ctx.badRequest(
2128
+ `A pasta de destino j\xE1 existe e n\xE3o est\xE1 vazia: ${frontendDir}. Renomeie o .zip ou remova a pasta.`
2129
+ );
2130
+ }
2131
+ try {
2132
+ import_node_fs7.default.mkdirSync(frontendDir, { recursive: true });
2133
+ for (const entryName of entryNames) {
2134
+ const entry = zip.files[entryName];
2135
+ const rel = rootPrefix && entryName.startsWith(rootPrefix) ? entryName.slice(rootPrefix.length) : entryName;
2136
+ if (!rel) continue;
2137
+ const dest = import_node_path7.default.join(frontendDir, rel);
2138
+ if (!ensureInside2(frontendDir, dest)) {
2139
+ throw new Error(`entrada perigosa no zip bloqueada: ${entryName}`);
2140
+ }
2141
+ if (entry.dir) {
2142
+ import_node_fs7.default.mkdirSync(dest, { recursive: true });
2143
+ } else {
2144
+ import_node_fs7.default.mkdirSync(import_node_path7.default.dirname(dest), { recursive: true });
2145
+ import_node_fs7.default.writeFileSync(dest, await entry.async("nodebuffer"));
2146
+ }
2147
+ }
2148
+ } catch (e) {
2149
+ return ctx.internalServerError(`Falha ao extrair: ${e?.message ?? e}`);
2150
+ }
2151
+ const inf = await inferManifest(strapi, frontendDir, { name });
2152
+ ctx.body = {
2153
+ ok: inf.ok,
2154
+ frontendDir,
2155
+ inferred: inf.inferred,
2156
+ framework: inf.framework,
2157
+ filesAnalyzed: inf.filesAnalyzed,
2158
+ // manifest cru (mesmo se inválido, para o usuário revisar/editar)
2159
+ manifest: inf.rawManifest ?? inf.manifest ?? null,
2160
+ warnings: inf.warnings,
2161
+ errors: inf.errors,
2162
+ message: inf.ok ? inf.inferred ? "Manifest inferido a partir do c\xF3digo. Revise e confirme para provisionar." : "Manifest encontrado no projeto. Revise e confirme para provisionar." : "N\xE3o foi poss\xEDvel obter um manifest v\xE1lido. Edite-o abaixo e tente provisionar."
2163
+ };
2164
+ },
2165
+ /** Etapa 2: provisiona a partir do manifest confirmado. */
2166
+ async provision(ctx) {
2167
+ const strapi = ctx.strapi ?? global.strapi;
2168
+ if (!devOnly(ctx)) return;
2169
+ const body = ctx.request.body || {};
2170
+ const rawManifest = body.manifest;
2171
+ const frontendDir = body.frontendDir;
2172
+ if (!rawManifest) return ctx.badRequest('Envie o "manifest".');
2173
+ if (!frontendDir || !import_node_path7.default.isAbsolute(frontendDir)) {
2174
+ return ctx.badRequest("frontendDir ausente ou inv\xE1lido.");
2175
+ }
2176
+ const strapiAppDir = strapi.dirs.app.root;
2177
+ const apiRoot = strapi.dirs.app.api;
2178
+ const parent = import_node_path7.default.resolve(strapiAppDir, "..");
2179
+ if (!ensureInside2(parent, frontendDir) || frontendDir === parent) {
2180
+ return ctx.badRequest("frontendDir fora da pasta permitida.");
2181
+ }
2182
+ if (!import_node_fs7.default.existsSync(frontendDir)) {
2183
+ return ctx.badRequest("frontendDir n\xE3o existe (rode a an\xE1lise primeiro).");
2184
+ }
2185
+ const v = validateManifest(rawManifest);
2186
+ if (!v.ok) {
2187
+ return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
2188
+ }
2189
+ try {
2190
+ import_node_fs7.default.writeFileSync(
2191
+ import_node_path7.default.join(frontendDir, MANIFEST_NAME),
2192
+ JSON.stringify(v.data, null, 2),
2193
+ "utf8"
2194
+ );
2195
+ } catch (e) {
2196
+ return ctx.internalServerError(`N\xE3o foi poss\xEDvel gravar o manifest: ${e?.message ?? e}`);
2197
+ }
2198
+ const port = strapi.config.get("server.port", 1337);
2199
+ let locales = [];
2200
+ try {
2201
+ locales = (await strapi.plugin("i18n").service("locales").find() || []).map((l) => l.code);
2202
+ } catch {
2203
+ }
2204
+ const context = { strapiUrl: `http://localhost:${port}`, locales };
2205
+ const staged = stageProvision(strapi, {
2206
+ rawManifest: v.data,
2207
+ apiRoot,
2208
+ frontendDir,
2209
+ strapiAppDir,
2210
+ context
2211
+ });
2212
+ if (!staged.ok) {
2213
+ return ctx.badRequest({
2214
+ message: "Provis\xE3o falhou",
2215
+ validation: staged.validation,
2216
+ errors: staged.errors
2217
+ });
2218
+ }
2219
+ ctx.body = {
2220
+ ok: true,
2221
+ frontendDir,
2222
+ contentTypes: staged.write?.written.filter((f) => f.endsWith("schema.json")).length ?? 0,
2223
+ skipped: staged.write?.skipped ?? [],
2224
+ willReload: staged.willReload,
2225
+ message: staged.willReload ? "Provis\xE3o agendada. A Strapi vai reiniciar e ent\xE3o criar o conte\xFAdo e ligar o preview." : "Nenhuma content-type nova (j\xE1 existiam)."
2226
+ };
2227
+ },
2228
+ /** Status da provisão — a UI faz polling após confirmar. */
2229
+ status(ctx) {
2230
+ const strapi = ctx.strapi ?? global.strapi;
2231
+ ctx.body = getProvisionStatus(strapi.dirs.app.root);
2232
+ },
2233
+ /**
2234
+ * Roda o dev server do frontend provisionado. A UI chama isto ao ligar o
2235
+ * preview pela 1ª vez após o upload. Sem body, usa o último provisionado.
2236
+ */
2237
+ async run(ctx) {
2238
+ const strapi = ctx.strapi ?? global.strapi;
2239
+ if (!devOnly(ctx)) return;
2240
+ const body = ctx.request.body || {};
2241
+ let dir = body.dir;
2242
+ let url = body.url;
2243
+ if (!dir || !url) {
2244
+ const st = getProvisionStatus(strapi.dirs.app.root);
2245
+ if (st.done) {
2246
+ dir = dir || st.done.frontendDir;
2247
+ url = url || st.done.previewUrl;
2248
+ }
2249
+ }
2250
+ if (!dir || !url) {
2251
+ return ctx.badRequest("Nenhum frontend provisionado para rodar.");
2252
+ }
2253
+ const parent = import_node_path7.default.resolve(strapi.dirs.app.root, "..");
2254
+ if (!ensureInside2(parent, dir) || !import_node_fs7.default.existsSync(dir)) {
2255
+ return ctx.badRequest("Pasta do frontend inv\xE1lida.");
2256
+ }
2257
+ ctx.body = await startFrontend(strapi, { dir, url });
2258
+ },
2259
+ /** Status do dev server do frontend (polling da UI). */
2260
+ runStatus(ctx) {
2261
+ ctx.body = getRunStatus();
2262
+ },
2263
+ /**
2264
+ * Religa o frontend ao Strapi por SNAPSHOT: regenera o(s) arquivo(s) de dados
2265
+ * com o conteúdo do Strapi, mantendo nomes de export e imagens originais.
2266
+ * Usa o último provisionado por padrão.
2267
+ */
2268
+ async integrate(ctx) {
2269
+ const strapi = ctx.strapi ?? global.strapi;
2270
+ if (!devOnly(ctx)) return;
2271
+ const body = ctx.request.body || {};
2272
+ let frontendDir = body.frontendDir;
2273
+ if (!frontendDir) {
2274
+ const st = getProvisionStatus(strapi.dirs.app.root);
2275
+ frontendDir = st.done?.frontendDir || "";
2276
+ }
2277
+ if (!frontendDir) return ctx.badRequest("Nenhum frontend provisionado.");
2278
+ const parent = import_node_path7.default.resolve(strapi.dirs.app.root, "..");
2279
+ if (!ensureInside2(parent, frontendDir) || !import_node_fs7.default.existsSync(frontendDir)) {
2280
+ return ctx.badRequest("Pasta do frontend inv\xE1lida.");
2281
+ }
2282
+ let manifest;
2283
+ try {
2284
+ manifest = JSON.parse(import_node_fs7.default.readFileSync(import_node_path7.default.join(frontendDir, MANIFEST_NAME), "utf8"));
2285
+ } catch {
2286
+ return ctx.badRequest("Manifest do projeto n\xE3o encontrado (rode a provis\xE3o primeiro).");
2287
+ }
2288
+ const v = validateManifest(manifest);
2289
+ if (!v.ok) return ctx.badRequest({ message: "Manifest inv\xE1lido", errors: v.errors });
2290
+ ctx.body = await integrateFrontend(strapi, { frontendDir, manifest: v.data });
2291
+ }
2292
+ };
2293
+
2294
+ // server/src/mcp-client.ts
2295
+ var MCP_URL = process.env.MCP_URL || "http://localhost:1337/mcp";
2296
+ var baseHeaders = {
2297
+ "Content-Type": "application/json",
2298
+ Accept: "application/json, text/event-stream"
2299
+ };
2300
+ var parseSse = (text) => {
2301
+ const dataLines = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
2302
+ const last = dataLines[dataLines.length - 1];
2303
+ return last ? JSON.parse(last) : null;
2304
+ };
2305
+ var McpClient = class {
2306
+ /**
2307
+ * @param url endpoint MCP streamable. Default: o /mcp nativo da Strapi.
2308
+ * @param name rótulo p/ logs (ex.: 'strapi', 'playwright').
2309
+ * @param token Bearer token (admin token, exigido pelo /mcp nativo).
2310
+ */
2311
+ constructor(url = MCP_URL, name = "strapi", token) {
2312
+ this.url = url;
2313
+ this.name = name;
2314
+ this.token = token;
2315
+ }
2316
+ headers() {
2317
+ const h = { ...baseHeaders };
2318
+ if (this.token) h["Authorization"] = `Bearer ${this.token}`;
2319
+ if (this.sessionId) h["mcp-session-id"] = this.sessionId;
2320
+ return h;
2321
+ }
2322
+ async init() {
2323
+ const res = await fetch(this.url, {
2324
+ method: "POST",
2325
+ headers: this.headers(),
2326
+ body: JSON.stringify({
2327
+ jsonrpc: "2.0",
2328
+ id: 1,
2329
+ method: "initialize",
2330
+ params: {
2331
+ protocolVersion: "2024-11-05",
2332
+ capabilities: {},
2333
+ clientInfo: { name: "mcp-chat-plugin", version: "0.1.0" }
2334
+ }
2335
+ })
2336
+ });
2337
+ this.sessionId = res.headers.get("mcp-session-id") || void 0;
2338
+ await res.text();
2339
+ await fetch(this.url, {
2340
+ method: "POST",
2341
+ headers: this.headers(),
2342
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
2343
+ });
2344
+ }
2345
+ async rpc(method, params, id) {
2346
+ const res = await fetch(this.url, {
2347
+ method: "POST",
2348
+ headers: this.headers(),
2349
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
2350
+ });
2351
+ const json = parseSse(await res.text());
2352
+ if (json?.error) throw new Error(json.error.message || "Erro MCP");
2353
+ return json?.result;
2354
+ }
2355
+ async listTools() {
2356
+ const result = await this.rpc("tools/list", {}, 2);
2357
+ return result?.tools || [];
2358
+ }
2359
+ async callTool(name, args) {
2360
+ return this.rpc("tools/call", { name, arguments: args || {} }, 3);
2361
+ }
2362
+ };
2363
+
2364
+ // server/src/provision/translate.ts
2365
+ var OPENAI_URL3 = "https://api.openai.com/v1/chat/completions";
2366
+ var MODEL3 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
2367
+ var approxTokens = (s) => Math.ceil((s || "").length / 4);
2368
+ var MAX_CHUNK_TOKENS = 1200;
2369
+ var splitParagraphs = (text) => text.split(/\n{2,}/);
2370
+ var splitSentences = (text) => text.match(/[^.!?]+(?:[.!?]+|$)/g) || [text];
2371
+ function splitForTranslation(value, _type) {
2372
+ const text = String(value ?? "");
2373
+ if (approxTokens(text) <= MAX_CHUNK_TOKENS) {
2374
+ return { chunks: [text], join: (t) => t[0] ?? "" };
2375
+ }
2376
+ const segs = [];
2377
+ for (const para of splitParagraphs(text)) {
2378
+ if (approxTokens(para) <= MAX_CHUNK_TOKENS) {
2379
+ segs.push(para);
2380
+ continue;
2381
+ }
2382
+ let buf2 = "";
2383
+ for (const sent of splitSentences(para)) {
2384
+ if (buf2 && approxTokens(buf2 + sent) > MAX_CHUNK_TOKENS) {
2385
+ segs.push(buf2);
2386
+ buf2 = "";
2387
+ }
2388
+ buf2 += sent;
2389
+ }
2390
+ if (buf2) segs.push(buf2);
2391
+ }
2392
+ const chunks = [];
2393
+ let buf = [];
2394
+ let bufTok = 0;
2395
+ for (const seg of segs) {
2396
+ const t = approxTokens(seg);
2397
+ if (buf.length && bufTok + t > MAX_CHUNK_TOKENS) {
2398
+ chunks.push(buf.join("\n\n"));
2399
+ buf = [];
2400
+ bufTok = 0;
2401
+ }
2402
+ buf.push(seg);
2403
+ bufTok += t;
2404
+ }
2405
+ if (buf.length) chunks.push(buf.join("\n\n"));
2406
+ return { chunks, join: (t) => t.join("\n\n") };
2407
+ }
2408
+ async function translateChunk(apiKey, text, sourceLang, targetLang) {
2409
+ if (!text || !text.trim()) return text;
2410
+ const body = {
2411
+ model: MODEL3,
2412
+ temperature: 0,
2413
+ max_tokens: Math.min(4e3, approxTokens(text) * 3 + 256),
2414
+ messages: [
2415
+ {
2416
+ role: "system",
2417
+ content: `You are a professional translator. Translate the user's text from ${sourceLang} to ${targetLang}. Preserve EXACTLY all markdown, HTML tags, URLs, and placeholders such as {name}, :slug, %s, {{var}}. Keep line breaks. Do NOT add quotes, notes, or explanations. Return ONLY the translated text.`
2418
+ },
2419
+ { role: "user", content: text }
2420
+ ]
2421
+ };
2422
+ const res = await fetch(OPENAI_URL3, {
2423
+ method: "POST",
2424
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
2425
+ body: JSON.stringify(body)
2426
+ });
2427
+ if (!res.ok) throw new Error(`OpenAI translate: ${await res.text()}`);
2428
+ const data = await res.json();
2429
+ return (data.choices?.[0]?.message?.content ?? "").trim();
2430
+ }
2431
+ async function mapPool(items, limit, fn) {
2432
+ const out = new Array(items.length);
2433
+ let i = 0;
2434
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
2435
+ while (i < items.length) {
2436
+ const idx = i++;
2437
+ out[idx] = await fn(items[idx], idx);
2438
+ }
2439
+ });
2440
+ await Promise.all(workers);
2441
+ return out;
2442
+ }
2443
+ async function translateText(apiKey, value, sourceLang, targetLang, type) {
2444
+ if (typeof value !== "string" || !value.trim()) return { text: value, chunks: 0 };
2445
+ const { chunks, join } = splitForTranslation(value, type);
2446
+ const translated = await mapPool(
2447
+ chunks,
2448
+ 4,
2449
+ (c) => c.trim() ? translateChunk(apiKey, c, sourceLang, targetLang) : Promise.resolve(c)
2450
+ );
2451
+ return { text: join(translated), chunks: chunks.length };
2452
+ }
2453
+
2454
+ // server/src/content-tools.ts
2455
+ var TEXTUAL = ["string", "text", "richtext"];
2456
+ function createContentTools(strapi) {
2457
+ const apiContentTypes = () => Object.values(strapi.contentTypes).filter(
2458
+ (ct) => ct.uid?.startsWith("api::")
2459
+ );
2460
+ const attrsOf = (uid) => strapi.contentTypes?.[uid]?.attributes || strapi.components?.[uid]?.attributes || {};
2461
+ const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set()) => {
2462
+ const populate = {};
2463
+ for (const [name, a] of Object.entries(attributes)) {
2464
+ if (a.type === "component" && a.component) {
2465
+ const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
2466
+ populate[name] = Object.keys(sub).length ? { populate: sub } : true;
2467
+ } else if (a.type === "dynamiczone") {
2468
+ const on = {};
2469
+ for (const comp of a.components || []) {
2470
+ const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp));
2471
+ on[comp] = Object.keys(sub).length ? { populate: sub } : true;
2472
+ }
2473
+ populate[name] = { on };
2474
+ } else if (a.type === "media" || a.type === "relation") {
2475
+ populate[name] = true;
2476
+ }
2477
+ }
2478
+ return populate;
2479
+ };
2480
+ const walkFind = (node, attributes, basePath, needle, collect) => {
2481
+ if (!node || typeof node !== "object") return;
2482
+ for (const [name, a] of Object.entries(attributes)) {
2483
+ const v = node[name];
2484
+ if (v == null) continue;
2485
+ const path9 = [...basePath, name];
2486
+ if (TEXTUAL.includes(a.type)) {
2487
+ if (typeof v === "string" && v.toLowerCase().includes(needle)) {
2488
+ collect(path9, name, v);
2489
+ }
2490
+ } else if (a.type === "component" && a.component) {
2491
+ const sub = attrsOf(a.component);
2492
+ if (a.repeatable && Array.isArray(v)) {
2493
+ v.forEach((item, i) => walkFind(item, sub, [...path9, i], needle, collect));
2494
+ } else {
2495
+ walkFind(v, sub, path9, needle, collect);
2496
+ }
2497
+ } else if (a.type === "dynamiczone" && Array.isArray(v)) {
2498
+ v.forEach((item, i) => {
2499
+ if (item?.__component) {
2500
+ walkFind(item, attrsOf(item.__component), [...path9, i], needle, collect);
2501
+ }
2502
+ });
2503
+ }
2504
+ }
2505
+ };
2506
+ const buscarTexto = async (termo) => {
2507
+ const needle = String(termo || "").toLowerCase().trim();
2508
+ if (!needle) return { erro: "termo vazio" };
2509
+ const matches = [];
2510
+ for (const ct of apiContentTypes()) {
2511
+ const attributes = ct.attributes || {};
2512
+ const populate = buildPopulate(attributes);
2513
+ let entries = [];
2514
+ try {
2515
+ const res = await strapi.documents(ct.uid).findMany({ status: "draft", populate, limit: 200 });
2516
+ entries = Array.isArray(res) ? res : res ? [res] : [];
2517
+ } catch {
2518
+ continue;
2519
+ }
2520
+ for (const e of entries) {
2521
+ walkFind(e, attributes, [], needle, (path9, campo, valor) => {
2522
+ matches.push({
2523
+ uid: ct.uid,
2524
+ tipo: ct.info?.displayName || ct.uid,
2525
+ documentId: e.documentId,
2526
+ path: path9,
2527
+ campo,
2528
+ valor_atual: valor.length > 300 ? valor.slice(0, 300) + "\u2026" : valor
2529
+ });
2530
+ });
2531
+ }
2532
+ }
2533
+ return { total: matches.length, resultados: matches };
2534
+ };
2535
+ const sanitizeNode = (node, attributes) => {
2536
+ if (node == null) return node;
2537
+ const out = {};
2538
+ if (node.id != null) out.id = node.id;
2539
+ for (const [name, a] of Object.entries(attributes)) {
2540
+ const v = node[name];
2541
+ if (v === void 0) continue;
2542
+ out[name] = sanitizeAttr(v, a);
2543
+ }
2544
+ return out;
2545
+ };
2546
+ const sanitizeAttr = (value, a) => {
2547
+ if (value == null) return value;
2548
+ if (a.type === "component" && a.component) {
2549
+ const sub = attrsOf(a.component);
2550
+ return a.repeatable && Array.isArray(value) ? value.map((it) => sanitizeNode(it, sub)) : sanitizeNode(value, sub);
2551
+ }
2552
+ if (a.type === "dynamiczone" && Array.isArray(value)) {
2553
+ return value.map((it) => ({
2554
+ __component: it.__component,
2555
+ ...sanitizeNode(it, attrsOf(it.__component))
2556
+ }));
2557
+ }
2558
+ if (a.type === "media") {
2559
+ return Array.isArray(value) ? value.map((m) => m?.id).filter(Boolean) : value?.id ?? null;
2560
+ }
2561
+ if (a.type === "relation") {
2562
+ return Array.isArray(value) ? value.map((r) => r?.id).filter(Boolean) : value?.id ?? null;
2563
+ }
2564
+ return value;
2565
+ };
2566
+ const editarCampo = async ({ uid, documentId, path: path9, campo, novo_valor, locale }) => {
2567
+ const p = Array.isArray(path9) && path9.length ? path9 : campo ? [campo] : null;
2568
+ if (!p) return { erro: 'informe "path" (array) ou "campo"' };
2569
+ const attributes = strapi.contentTypes?.[uid]?.attributes || {};
2570
+ const topAttr = p[0];
2571
+ const ad = attributes[topAttr];
2572
+ const loc = locale ? { locale } : {};
2573
+ if (p.length === 1 && ad && TEXTUAL.includes(ad.type)) {
2574
+ const updated2 = await strapi.documents(uid).update({ documentId, ...loc, data: { [topAttr]: novo_valor } });
2575
+ return { ok: true, uid, documentId: updated2?.documentId || documentId, path: p, novo_valor, locale };
2576
+ }
2577
+ const populate = buildPopulate(attributes);
2578
+ const entry = await strapi.documents(uid).findOne({ documentId, status: "draft", ...loc, populate });
2579
+ if (!entry) return { erro: "entrada n\xE3o encontrada" };
2580
+ let cur = entry;
2581
+ for (let i = 0; i < p.length - 1; i++) {
2582
+ if (cur == null) break;
2583
+ cur = cur[p[i]];
2584
+ }
2585
+ if (cur == null) return { erro: `caminho inv\xE1lido: ${p.join(".")}` };
2586
+ cur[p[p.length - 1]] = novo_valor;
2587
+ const data = { [topAttr]: sanitizeAttr(entry[topAttr], ad) };
2588
+ const updated = await strapi.documents(uid).update({ documentId, ...loc, data });
2589
+ return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale };
2590
+ };
2591
+ const publicar = async ({
2592
+ uid,
2593
+ documentId,
2594
+ locale
2595
+ }) => {
2596
+ await strapi.documents(uid).publish({ documentId, ...locale ? { locale } : {} });
2597
+ return { ok: true, uid, documentId, status: "published", locale };
2598
+ };
2599
+ const i18nLocalesSvc = () => strapi.plugin?.("i18n")?.service?.("locales");
2600
+ const isoLocalesSvc = () => strapi.plugin?.("i18n")?.service?.("iso-locales");
2601
+ const listarLocales = async () => {
2602
+ const svc = i18nLocalesSvc();
2603
+ if (!svc) return { erro: "plugin i18n indispon\xEDvel" };
2604
+ const def = await svc.getDefaultLocale();
2605
+ const all = await svc.find() || [];
2606
+ return {
2607
+ default: def,
2608
+ locales: all.map((l) => ({ code: l.code, name: l.name, isDefault: l.code === def }))
2609
+ };
2610
+ };
2611
+ const criarLocale = async ({ code, name }) => {
2612
+ const svc = i18nLocalesSvc();
2613
+ const iso = isoLocalesSvc();
2614
+ if (!svc || !iso) return { erro: "plugin i18n indispon\xEDvel" };
2615
+ const wanted = String(code || "").trim();
2616
+ if (!wanted) return { erro: 'informe o "code" do locale (ex.: "pt-BR")' };
2617
+ const list = iso.getIsoLocales();
2618
+ const match = list.find((l) => l.code.toLowerCase() === wanted.toLowerCase());
2619
+ if (!match) return { erro: `c\xF3digo de locale inv\xE1lido: "${wanted}" (n\xE3o est\xE1 na lista ISO do Strapi)` };
2620
+ const existing = await svc.findByCode(match.code);
2621
+ if (existing) return { ok: true, code: match.code, name: existing.name, existed: true };
2622
+ const created = await svc.create({ code: match.code, name: name || match.name });
2623
+ return { ok: true, code: created.code, name: created.name, existed: false };
2624
+ };
2625
+ const translateAttrValue = async (value, a, ctx) => {
2626
+ if (value == null) return value;
2627
+ if (TEXTUAL.includes(a.type)) {
2628
+ const { text, chunks } = await translateText(ctx.apiKey, value, ctx.src, ctx.tgt, a.type);
2629
+ ctx.bump(chunks);
2630
+ return text;
2631
+ }
2632
+ if (a.type === "component" && a.component) {
2633
+ const sub = attrsOf(a.component);
2634
+ return a.repeatable && Array.isArray(value) ? Promise.all(value.map((it) => translateNodeSanitized(it, sub, ctx))) : translateNodeSanitized(value, sub, ctx);
2635
+ }
2636
+ if (a.type === "dynamiczone" && Array.isArray(value)) {
2637
+ return Promise.all(
2638
+ value.map(async (it) => ({
2639
+ __component: it.__component,
2640
+ ...await translateNodeSanitized(it, attrsOf(it.__component), ctx)
2641
+ }))
2642
+ );
2643
+ }
2644
+ return sanitizeAttr(value, a);
2645
+ };
2646
+ const translateNodeSanitized = async (node, attributes, ctx) => {
2647
+ if (node == null) return node;
2648
+ const out = {};
2649
+ if (node.id != null) out.id = node.id;
2650
+ for (const [name, a] of Object.entries(attributes)) {
2651
+ if (node[name] === void 0) continue;
2652
+ out[name] = await translateAttrValue(node[name], a, ctx);
2653
+ }
2654
+ return out;
2655
+ };
2656
+ const isLocalizedCT = (ct) => ct?.pluginOptions?.i18n?.localized === true;
2657
+ const isLocalizedAttr = (a) => a?.pluginOptions?.i18n?.localized === true;
2658
+ const traduzir = async ({
2659
+ target_locales,
2660
+ source_locale,
2661
+ uid,
2662
+ documentId,
2663
+ publish = true
2664
+ }) => {
2665
+ const apiKey = process.env.OPENAI_API_KEY;
2666
+ if (!apiKey) return { erro: "OPENAI_API_KEY n\xE3o configurada no .env do Strapi." };
2667
+ const svc = i18nLocalesSvc();
2668
+ const iso = isoLocalesSvc();
2669
+ if (!svc || !iso) return { erro: "plugin i18n indispon\xEDvel" };
2670
+ const targets = (Array.isArray(target_locales) ? target_locales : [target_locales]).filter(Boolean);
2671
+ if (!targets.length) return { erro: 'informe target_locales (ex.: ["pt-BR","es"])' };
2672
+ const src = source_locale || await svc.getDefaultLocale();
2673
+ const cts = apiContentTypes().filter(
2674
+ (ct) => isLocalizedCT(ct) && (!uid || ct.uid === uid)
2675
+ );
2676
+ if (!cts.length) {
2677
+ return {
2678
+ erro: uid ? `content-type "${uid}" n\xE3o \xE9 localizada (i18n desligado). Habilite i18n nos campos antes de traduzir.` : "nenhuma content-type localizada encontrada. Habilite i18n (pluginOptions.i18n.localized) nos campos a traduzir."
2679
+ };
2680
+ }
2681
+ const isoList = iso.getIsoLocales();
2682
+ const nameOf = (code) => isoList.find((l) => l.code.toLowerCase() === code.toLowerCase())?.name || code;
2683
+ const por_locale = [];
2684
+ for (const rawTgt of targets) {
2685
+ const created = await criarLocale({ code: rawTgt });
2686
+ if (created.erro) {
2687
+ por_locale.push({ locale: rawTgt, erro: created.erro });
2688
+ continue;
2689
+ }
2690
+ const tgt = created.code;
2691
+ let documentos = 0;
2692
+ let campos = 0;
2693
+ let chunks = 0;
2694
+ let publicados = 0;
2695
+ for (const ct of cts) {
2696
+ const attributes = ct.attributes || {};
2697
+ const populate = buildPopulate(attributes);
2698
+ let res;
2699
+ try {
2700
+ res = await strapi.documents(ct.uid).findMany({ status: "draft", locale: src, populate, limit: 1e3 });
2701
+ } catch {
2702
+ continue;
2703
+ }
2704
+ let entries = Array.isArray(res) ? res : res ? [res] : [];
2705
+ if (documentId) entries = entries.filter((e) => e.documentId === documentId);
2706
+ for (const e of entries) {
2707
+ const ctx = {
2708
+ apiKey,
2709
+ src: nameOf(src),
2710
+ tgt: nameOf(tgt),
2711
+ bump: (c) => {
2712
+ campos += 1;
2713
+ chunks += c;
2714
+ }
2715
+ };
2716
+ const data = {};
2717
+ for (const [name, a] of Object.entries(attributes)) {
2718
+ if (!isLocalizedAttr(a)) continue;
2719
+ if (e[name] == null) continue;
2720
+ data[name] = await translateAttrValue(e[name], a, ctx);
2721
+ }
2722
+ if (!Object.keys(data).length) continue;
2723
+ await strapi.documents(ct.uid).update({ documentId: e.documentId, locale: tgt, data });
2724
+ documentos += 1;
2725
+ if (publish) {
2726
+ await strapi.documents(ct.uid).publish({ documentId: e.documentId, locale: tgt });
2727
+ publicados += 1;
2728
+ }
2729
+ }
2730
+ }
2731
+ por_locale.push({ locale: tgt, documentos, campos, chunks, publicados });
2732
+ }
2733
+ return { ok: true, source: src, por_locale };
2734
+ };
2735
+ return { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir };
2736
+ }
2737
+ var openAiToolSpecs = [
2738
+ {
2739
+ type: "function",
2740
+ function: {
2741
+ name: "buscar_texto",
2742
+ description: 'Procura uma palavra/frase em TODOS os content-types, single types, COMPONENTES e DYNAMIC ZONES do Strapi (substring, recursivo). Cada resultado traz uid, documentId, "path" (ex.: ["dynamic_zone",2,"heading"]), campo e valor_atual. Passe esse "path" para editar_campo.',
2743
+ parameters: {
2744
+ type: "object",
2745
+ properties: {
2746
+ termo: {
2747
+ type: "string",
2748
+ description: 'trecho distintivo do texto a localizar; N\xC3O inclua r\xF3tulos de status do preview, como "(Draft)"/"(Rascunho)"'
2749
+ }
2750
+ },
2751
+ required: ["termo"]
2752
+ }
2753
+ }
2754
+ },
2755
+ {
2756
+ type: "function",
2757
+ function: {
2758
+ name: "editar_campo",
2759
+ description: 'Altera o valor de um campo (salva como rascunho). Use o "path" retornado por buscar_texto para campos aninhados; para campo simples no topo, pode usar "campo". Passe "locale" para gravar numa vers\xE3o de idioma espec\xEDfica.',
2760
+ parameters: {
2761
+ type: "object",
2762
+ properties: {
2763
+ uid: { type: "string" },
2764
+ documentId: { type: "string" },
2765
+ path: {
2766
+ type: "array",
2767
+ description: "caminho at\xE9 o campo, exatamente como veio de buscar_texto",
2768
+ items: { type: ["string", "number"] }
2769
+ },
2770
+ campo: { type: "string", description: "alternativa ao path (campo simples no topo)" },
2771
+ novo_valor: { type: "string" },
2772
+ locale: { type: "string", description: 'opcional; c\xF3digo do locale (ex.: "pt-BR")' }
2773
+ },
2774
+ required: ["uid", "documentId", "novo_valor"]
2775
+ }
2776
+ }
2777
+ },
2778
+ {
2779
+ type: "function",
2780
+ function: {
2781
+ name: "publicar",
2782
+ description: 'Publica a entrada (torna a altera\xE7\xE3o vis\xEDvel no site p\xFAblico). Passe "locale" para publicar um idioma espec\xEDfico, ou "*" para todos.',
2783
+ parameters: {
2784
+ type: "object",
2785
+ properties: {
2786
+ uid: { type: "string" },
2787
+ documentId: { type: "string" },
2788
+ locale: { type: "string", description: 'opcional; c\xF3digo do locale ou "*" para todos' }
2789
+ },
2790
+ required: ["uid", "documentId"]
2791
+ }
2792
+ }
2793
+ },
2794
+ {
2795
+ type: "function",
2796
+ function: {
2797
+ name: "listar_locales",
2798
+ description: "Lista os locales (idiomas) configurados no Strapi e qual \xE9 o default.",
2799
+ parameters: { type: "object", properties: {} }
2800
+ }
2801
+ },
2802
+ {
2803
+ type: "function",
2804
+ function: {
2805
+ name: "criar_locale",
2806
+ description: 'Cria um locale (idioma) no Strapi. O "code" precisa ser um c\xF3digo ISO v\xE1lido (ex.: "pt-BR", "es", "fr"). Idempotente: se j\xE1 existir, n\xE3o recria.',
2807
+ parameters: {
2808
+ type: "object",
2809
+ properties: {
2810
+ code: { type: "string", description: 'c\xF3digo ISO do locale, ex.: "pt-BR"' },
2811
+ name: { type: "string", description: "opcional; nome exibido (default = nome ISO)" }
2812
+ },
2813
+ required: ["code"]
2814
+ }
2815
+ }
2816
+ },
2817
+ {
2818
+ type: "function",
2819
+ function: {
2820
+ name: "traduzir",
2821
+ description: "Traduz o conte\xFAdo localizado para um ou mais idiomas. Cria os locales se preciso, traduz campo a campo (textos longos s\xE3o divididos e remontados \u2014 nunca estoura) e publica. Sem uid/documentId, traduz TODAS as content-types localizadas (todas as p\xE1ginas). Funciona para muitos locales de uma vez.",
2822
+ parameters: {
2823
+ type: "object",
2824
+ properties: {
2825
+ target_locales: {
2826
+ type: "array",
2827
+ items: { type: "string" },
2828
+ description: 'lista de c\xF3digos de destino, ex.: ["pt-BR","es","fr"]'
2829
+ },
2830
+ source_locale: { type: "string", description: "idioma de origem; default = locale default do Strapi" },
2831
+ uid: { type: "string", description: "opcional; restringe a uma content-type (ex.: api::home.home)" },
2832
+ documentId: { type: "string", description: "opcional; restringe a um documento" },
2833
+ publish: { type: "boolean", description: "publicar ap\xF3s traduzir (default true)" }
2834
+ },
2835
+ required: ["target_locales"]
2836
+ }
2837
+ }
2838
+ },
2839
+ {
2840
+ type: "function",
2841
+ function: {
2842
+ name: "habilitar_i18n",
2843
+ description: 'Habilita i18n (tradu\xE7\xE3o) em content-types que ainda n\xE3o s\xE3o localizadas: marca a CT e seus campos textuais/componentes como localizados. Necess\xE1rio antes de traduzir conte\xFAdo provisionado sem i18n. OMITA "uid" (ou passe "*") para habilitar em TODAS as content-types de uma vez (recomendado para "traduzir o site inteiro") \u2014 um \xFAnico restart. Edita o schema e a Strapi reinicia (em desenvolvimento). N\xC3O adivinhe uids.',
2844
+ parameters: {
2845
+ type: "object",
2846
+ properties: {
2847
+ uid: { type: "string", description: "opcional; ex.: api::home-content.home-content. Omita p/ TODAS." },
2848
+ campos: {
2849
+ type: "array",
2850
+ items: { type: "string" },
2851
+ description: "opcional; campos a localizar (default = todos os textuais/componentes)"
2852
+ }
2853
+ }
2854
+ }
2855
+ }
2856
+ }
2857
+ ];
2858
+
2859
+ // server/src/provision/enable-i18n.ts
2860
+ var import_node_fs8 = __toESM(require("node:fs"));
2861
+ var import_node_path8 = __toESM(require("node:path"));
2862
+ var LOCALIZABLE = ["string", "text", "richtext", "component", "dynamiczone"];
2863
+ var isDev2 = () => process.env.NODE_ENV === "development";
2864
+ function schemaPathFor(apiRoot, uid) {
2865
+ const m = /^api::([^.]+)\.([^.]+)$/.exec(uid);
2866
+ if (!m) return null;
2867
+ const [, api, ct] = m;
2868
+ return import_node_path8.default.join(apiRoot, api, "content-types", ct, "schema.json");
2869
+ }
2870
+ function listAllUids(apiRoot) {
2871
+ const out = [];
2872
+ let apis = [];
2873
+ try {
2874
+ apis = import_node_fs8.default.readdirSync(apiRoot);
2875
+ } catch {
2876
+ return out;
2877
+ }
2878
+ for (const api of apis) {
2879
+ const ctDir = import_node_path8.default.join(apiRoot, api, "content-types");
2880
+ if (!import_node_fs8.default.existsSync(ctDir)) continue;
2881
+ for (const ct of import_node_fs8.default.readdirSync(ctDir)) {
2882
+ if (import_node_fs8.default.existsSync(import_node_path8.default.join(ctDir, ct, "schema.json"))) out.push(`api::${api}.${ct}`);
2883
+ }
2884
+ }
2885
+ return out;
2886
+ }
2887
+ var withLocalized = (obj) => ({
2888
+ ...obj || {},
2889
+ i18n: { ...(obj || {}).i18n || {}, localized: true }
2890
+ });
2891
+ function patchOne(file, campos) {
2892
+ if (!import_node_fs8.default.existsSync(file)) return { erro: `schema.json n\xE3o encontrado (${file})` };
2893
+ let schema;
2894
+ try {
2895
+ schema = JSON.parse(import_node_fs8.default.readFileSync(file, "utf8"));
2896
+ } catch (e) {
2897
+ return { erro: `schema.json ileg\xEDvel: ${e?.message ?? e}` };
2898
+ }
2899
+ schema.pluginOptions = withLocalized(schema.pluginOptions);
2900
+ const attrs = schema.attributes || {};
2901
+ const alvos = campos && campos.length ? campos : Object.keys(attrs).filter((k) => LOCALIZABLE.includes(attrs[k]?.type));
2902
+ const changed = [];
2903
+ for (const name of alvos) {
2904
+ if (!attrs[name]) continue;
2905
+ attrs[name].pluginOptions = withLocalized(attrs[name].pluginOptions);
2906
+ changed.push(name);
2907
+ }
2908
+ try {
2909
+ import_node_fs8.default.writeFileSync(file, JSON.stringify(schema, null, 2) + "\n", "utf8");
2910
+ } catch (e) {
2911
+ return { erro: `falha ao gravar schema.json: ${e?.message ?? e}` };
2912
+ }
2913
+ return { campos: changed };
2914
+ }
2915
+ function enableI18n(opts) {
2916
+ const { strapi, uid, campos, allowOutsideDev } = opts;
2917
+ if (!allowOutsideDev && !isDev2()) {
2918
+ return { erro: "habilitar i18n s\xF3 \xE9 permitido em desenvolvimento (NODE_ENV=development)." };
2919
+ }
2920
+ const srcDir = strapi?.dirs?.app?.src || import_node_path8.default.join(process.cwd(), "src");
2921
+ const apiRoot = import_node_path8.default.join(srcDir, "api");
2922
+ if (!uid || uid === "*") {
2923
+ const uids = listAllUids(apiRoot);
2924
+ if (!uids.length) return { erro: `nenhuma content-type encontrada em ${apiRoot}` };
2925
+ const done = [];
2926
+ const errors = [];
2927
+ for (const u of uids) {
2928
+ const file2 = schemaPathFor(apiRoot, u);
2929
+ const r2 = patchOne(file2, campos);
2930
+ if ("erro" in r2) errors.push(`${u}: ${r2.erro}`);
2931
+ else done.push({ uid: u, campos: r2.campos });
2932
+ }
2933
+ if (!done.length) return { erro: `nada habilitado. ${errors.join("; ")}` };
2934
+ return { ok: true, contentTypes: done, total: done.length, restart: true };
2935
+ }
2936
+ const file = schemaPathFor(apiRoot, uid);
2937
+ if (!file) return { erro: `uid inv\xE1lido: "${uid}" (esperado api::x.x)` };
2938
+ const r = patchOne(file, campos);
2939
+ if ("erro" in r) return { erro: r.erro };
2940
+ return { ok: true, uid, campos: r.campos, restart: true };
2941
+ }
2942
+
2943
+ // server/src/services/chat.ts
2944
+ var MODEL4 = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
2945
+ var MAX_TURNS = 10;
2946
+ var OPENAI_URL4 = "https://api.openai.com/v1/chat/completions";
2947
+ var SYSTEM = {
2948
+ pt: `Voc\xEA \xE9 um assistente embutido no admin do Strapi 5 deste projeto. Voc\xEA N\xC3O \xE9 s\xF3 um guia: voc\xEA consegue EDITAR e PUBLICAR conte\xFAdo de verdade atrav\xE9s das ferramentas.
2949
+
2950
+ Ferramentas de conte\xFAdo:
2951
+ - buscar_texto({termo}): procura uma palavra ou frase em TODOS os content-types, single types, COMPONENTES e DYNAMIC ZONES (recursivo, por substring). Retorna uma lista; cada item tem uid, documentId, "path" (caminho at\xE9 o campo, ex.: ["dynamic_zone",2,"heading"]), campo e valor_atual. Use SEMPRE isto primeiro \u2014 N\xC3O pe\xE7a ao usu\xE1rio onde est\xE1, ache sozinho. Busque um trecho distintivo e N\xC3O inclua r\xF3tulos que o preview adiciona, como "(Draft)"/"(Rascunho)".
2952
+ - editar_campo({uid, documentId, path, novo_valor}): troca o valor de um campo (salva como rascunho). Passe o "path" EXATAMENTE como veio de buscar_texto.
2953
+ - publicar({uid, documentId}): publica a entrada, deixando a mudan\xE7a vis\xEDvel no site.
2954
+
2955
+ Fluxo padr\xE3o quando o usu\xE1rio pede uma mudan\xE7a no site (por texto, voz ou mostrando a tela):
2956
+ 1. Use buscar_texto com um trecho distintivo do texto a alterar (sem r\xF3tulos de status).
2957
+ 2. Se houver mais de um resultado, escolha o mais prov\xE1vel pelo contexto (e diga qual escolheu); se amb\xEDguo de verdade, pergunte.
2958
+ 3. editar_campo passando o mesmo uid, documentId e path do resultado, com o novo valor.
2959
+ 4. publicar a entrada.
2960
+ 5. Confirme em 1 frase o que foi alterado e publicado (content-type, campo, antes \u2192 depois).
2961
+
2962
+ Ferramentas de tradu\xE7\xE3o / idiomas (i18n):
2963
+ - listar_locales(): mostra os idiomas configurados e o default.
2964
+ - criar_locale({code}): cria um idioma (code ISO, ex.: "pt-BR"). Idempotente.
2965
+ - traduzir({target_locales, source_locale?, uid?, documentId?, publish?}): traduz o conte\xFAdo localizado para um ou MAIS idiomas. Cria os locales se faltarem, traduz campo a campo (textos longos s\xE3o divididos e remontados \u2014 n\xE3o estoura) e publica. Sem uid/documentId, traduz TODAS as p\xE1ginas.
2966
+ - habilitar_i18n({uid?, campos?}): liga a tradu\xE7\xE3o em content-types ainda n\xE3o localizadas (a Strapi reinicia). OMITA uid para habilitar em TODAS de uma vez. NUNCA adivinhe uids \u2014 para o site inteiro, sempre sem uid.
2967
+
2968
+ Fluxo quando o usu\xE1rio pede tradu\xE7\xE3o (ex.: "quero o site todo em pt-BR"):
2969
+ 1. Chame traduzir com target_locales (lista de c\xF3digos). N\xE3o precisa criar o locale antes \u2014 traduzir j\xE1 cria.
2970
+ 2. Se traduzir disser que NENHUMA content-type \xE9 localizada (i18n desligado), chame habilitar_i18n SEM uid (habilita todas de uma vez), avise que a Strapi vai reiniciar e que \xE9 s\xF3 repetir o pedido ap\xF3s o restart. N\xC3O chame habilitar_i18n uid por uid nem invente nomes.
2971
+ 3. Ap\xF3s o restart, ao repetir, traduzir funciona e localiza tudo.
2972
+ 4. Confirme em 1 frase: idiomas, quantos documentos e campos foram traduzidos/publicados (use o resumo retornado, n\xE3o despeje o conte\xFAdo).
2973
+
2974
+ Se o usu\xE1rio compartilhar a tela, uma imagem \xE9 anexada \xE0 \xFAltima mensagem \u2014 use-a para entender exatamente o que ele est\xE1 vendo e qual texto quer trocar.
2975
+
2976
+ Seja objetivo e acion\xE1vel. Responda SEMPRE em portugu\xEAs.`,
2977
+ en: `You are an assistant embedded in this project's Strapi 5 admin. You are NOT just a guide: you can actually EDIT and PUBLISH content through your tools.
2978
+
2979
+ Content tools:
2980
+ - buscar_texto({termo}): searches a word or phrase across ALL content-types, single types, COMPONENTS and DYNAMIC ZONES (recursive, substring). Returns a list; each item has uid, documentId, "path" (the path to the field, e.g. ["dynamic_zone",2,"heading"]), field and current value. ALWAYS use this first \u2014 do NOT ask the user where it is, find it yourself. Search a distinctive snippet and do NOT include labels the preview adds, like "(Draft)".
2981
+ - editar_campo({uid, documentId, path, novo_valor}): replaces a field value (saved as draft). Pass the "path" EXACTLY as returned by buscar_texto.
2982
+ - publicar({uid, documentId}): publishes the entry, making the change visible on the site.
2983
+
2984
+ Default flow when the user asks for a site change (by text, voice or by showing their screen):
2985
+ 1. Use buscar_texto with a distinctive snippet of the text to change (no status labels).
2986
+ 2. If there is more than one result, pick the most likely from context (and say which); if truly ambiguous, ask.
2987
+ 3. editar_campo passing the same uid, documentId and path from the result, with the new value.
2988
+ 4. publicar the entry.
2989
+ 5. Confirm in one sentence what was changed and published (content-type, field, before \u2192 after).
2990
+
2991
+ Translation / language tools (i18n):
2992
+ - listar_locales(): shows configured languages and the default.
2993
+ - criar_locale({code}): creates a language (ISO code, e.g. "pt-BR"). Idempotent.
2994
+ - traduzir({target_locales, source_locale?, uid?, documentId?, publish?}): translates localized content into one or MORE languages. It creates missing locales, translates field by field (long text is split and reassembled \u2014 never overflows) and publishes. Without uid/documentId it translates ALL pages.
2995
+ - habilitar_i18n({uid?, campos?}): enables translation on content-types not localized yet (Strapi restarts). OMIT uid to enable ALL at once. NEVER guess uids \u2014 for the whole site, always call it without uid.
2996
+
2997
+ Flow when the user asks for translation (e.g. "I want the whole site in pt-BR"):
2998
+ 1. Call traduzir with target_locales (list of codes). No need to create the locale first \u2014 traduzir creates it.
2999
+ 2. If traduzir says NO content-type is localized (i18n off), call habilitar_i18n WITHOUT uid (enables all at once), warn that Strapi will restart and that they just need to repeat the request after the restart. Do NOT call habilitar_i18n per-uid or invent names.
3000
+ 3. After the restart, repeating the request makes traduzir localize everything.
3001
+ 4. Confirm in one sentence: languages, how many documents and fields were translated/published (use the returned summary, don't dump the content).
3002
+
3003
+ If the user shares their screen, an image is attached to the last message \u2014 use it to understand exactly what they see and which text they want to change.
3004
+
3005
+ Be concise and actionable. ALWAYS answer in English.`
3006
+ };
3007
+ var chat_default2 = ({ strapi }) => ({
3008
+ async chat({ messages, image, lang = "pt", previewUrl }) {
3009
+ const apiKey = process.env.OPENAI_API_KEY;
3010
+ if (!apiKey) {
3011
+ throw new Error(
3012
+ "OPENAI_API_KEY n\xE3o configurada no .env do Strapi. Adicione e reinicie."
3013
+ );
3014
+ }
3015
+ const language = lang === "en" ? "en" : "pt";
3016
+ const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
3017
+ const LOCAL_TOOLS = {
3018
+ buscar_texto: (a) => buscarTexto(a?.termo),
3019
+ editar_campo: (a) => editarCampo(a),
3020
+ publicar: (a) => publicar(a),
3021
+ listar_locales: () => listarLocales(),
3022
+ criar_locale: (a) => criarLocale(a),
3023
+ traduzir: (a) => traduzir(a),
3024
+ habilitar_i18n: async (a) => enableI18n({ strapi, uid: a?.uid, campos: a?.campos })
3025
+ };
3026
+ const localToolSpecs = openAiToolSpecs;
3027
+ const mcpByTool = {};
3028
+ const mcpTools = [];
3029
+ if (process.env.PLAYWRIGHT_MCP_URL) {
3030
+ try {
3031
+ const client = new McpClient(process.env.PLAYWRIGHT_MCP_URL, "playwright");
3032
+ await client.init();
3033
+ const list = await client.listTools();
3034
+ for (const t of list) {
3035
+ if (mcpByTool[t.name]) continue;
3036
+ mcpByTool[t.name] = client;
3037
+ mcpTools.push(t);
3038
+ }
3039
+ strapi.log.info(`[mcp-chat] MCP "playwright" ok: ${list.length} tools`);
3040
+ } catch (e) {
3041
+ strapi.log.warn(`[mcp-chat] MCP "playwright" indispon\xEDvel: ${e?.message || e}`);
3042
+ }
3043
+ }
3044
+ const tools2 = [
3045
+ ...localToolSpecs,
3046
+ ...mcpTools.map((t) => ({
3047
+ type: "function",
3048
+ function: {
3049
+ name: t.name,
3050
+ description: t.description || t.name,
3051
+ parameters: t.inputSchema || { type: "object", properties: {} }
3052
+ }
3053
+ }))
3054
+ ];
3055
+ const hasBrowser = mcpTools.some((t) => String(t.name).startsWith("browser_"));
3056
+ const adminBase = process.env.STRAPI_ADMIN_URL || "http://localhost:1337/admin";
3057
+ const BROWSER_NOTE = {
3058
+ pt: `
3059
+
3060
+ Voc\xEA tamb\xE9m controla um navegador real via ferramentas browser_* (Playwright), apontado para o ADMIN DA STRAPI em ${adminBase} (o backend \u2014 \xE9 aqui que o conte\xFAdo muda de verdade, N\xC3O no site p\xFAblico). Pode navegar (browser_navigate), clicar, digitar, rolar, tirar seus pr\xF3prios screenshots (browser_take_screenshot) e inspecionar console/erros. Prefira sempre suas ferramentas diretas (buscar_texto/editar_campo/publicar) para alterar conte\xFAdo; use o navegador para VERIFICAR no admin que a edi\xE7\xE3o/publica\xE7\xE3o ficou correta, ou para fluxos da UI que as ferramentas diretas n\xE3o cobrem.`,
3061
+ en: `
3062
+
3063
+ You also control a real browser via browser_* tools (Playwright), pointed at the STRAPI ADMIN at ${adminBase} (the backend \u2014 this is where content actually changes, NOT the public site). You can navigate (browser_navigate), click, type, scroll, take your own screenshots (browser_take_screenshot) and inspect console/errors. Always prefer your direct tools (buscar_texto/editar_campo/publicar) to change content; use the browser to VERIFY in the admin that the edit/publish landed, or for admin UI flows the direct tools don't cover.`
3064
+ };
3065
+ const systemContent = SYSTEM[language] + (hasBrowser ? BROWSER_NOTE[language] : "");
3066
+ const convo = [{ role: "system", content: systemContent }];
3067
+ const pageNote = previewUrl ? language === "en" ? `
3068
+
3069
+ [context: the user is viewing the page ${previewUrl} in the preview right now \u2014 assume "this/here" refers to what's on that page]` : `
3070
+
3071
+ [contexto: o usu\xE1rio est\xE1 vendo a p\xE1gina ${previewUrl} no preview agora \u2014 assuma que "isso/aqui" se refere ao que est\xE1 nessa p\xE1gina]` : "";
3072
+ messages.forEach((m, i) => {
3073
+ const isLastUser = i === messages.length - 1 && m.role === "user";
3074
+ if (isLastUser) {
3075
+ const text = (m.content || "") + pageNote;
3076
+ if (image) {
3077
+ convo.push({
3078
+ role: "user",
3079
+ content: [
3080
+ { type: "text", text: text || "(veja minha tela)" },
3081
+ { type: "image_url", image_url: { url: image } }
3082
+ ]
3083
+ });
3084
+ } else {
3085
+ convo.push({ role: "user", content: text || m.content });
3086
+ }
3087
+ } else {
3088
+ convo.push({ role: m.role, content: m.content });
3089
+ }
3090
+ });
3091
+ const callOpenAI = async (body) => {
3092
+ const res = await fetch(OPENAI_URL4, {
3093
+ method: "POST",
3094
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3095
+ body: JSON.stringify(body)
3096
+ });
3097
+ if (!res.ok) throw new Error(`OpenAI chat: ${await res.text()}`);
3098
+ return res.json();
3099
+ };
3100
+ let didWrite = false;
3101
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
3102
+ const data = await callOpenAI({
3103
+ model: MODEL4,
3104
+ max_tokens: 2048,
3105
+ messages: convo,
3106
+ ...tools2.length > 0 ? { tools: tools2, tool_choice: "auto" } : {}
3107
+ });
3108
+ const msg = data.choices?.[0]?.message;
3109
+ if (!msg) throw new Error("OpenAI: resposta sem message.");
3110
+ convo.push(msg);
3111
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
3112
+ for (const call of msg.tool_calls) {
3113
+ const name = call.function?.name;
3114
+ let content;
3115
+ try {
3116
+ const argsStr = call.function?.arguments || "{}";
3117
+ const args = argsStr ? JSON.parse(argsStr) : {};
3118
+ const local = LOCAL_TOOLS[name];
3119
+ const owner = mcpByTool[name];
3120
+ let r;
3121
+ if (local) {
3122
+ r = await local(args);
3123
+ if (["editar_campo", "publicar", "criar_locale", "traduzir", "habilitar_i18n"].includes(name))
3124
+ didWrite = true;
3125
+ strapi.log.info(`[mcp-chat] tool ${name} -> ${JSON.stringify(r).slice(0, 200)}`);
3126
+ } else if (owner) {
3127
+ r = await owner.callTool(name, args);
3128
+ } else {
3129
+ r = { erro: `tool ${name} indispon\xEDvel` };
3130
+ }
3131
+ content = typeof r === "string" ? r : JSON.stringify(r);
3132
+ } catch (e) {
3133
+ content = `Erro ao chamar a tool ${name}: ${e?.message || e}`;
3134
+ }
3135
+ convo.push({ role: "tool", tool_call_id: call.id, content });
3136
+ }
3137
+ continue;
3138
+ }
3139
+ const text = (typeof msg.content === "string" ? msg.content : "").trim();
3140
+ return {
3141
+ reply: text || "(sem resposta)",
3142
+ model: MODEL4,
3143
+ lang: language,
3144
+ didWrite,
3145
+ toolsAvailable: tools2.length
3146
+ };
3147
+ }
3148
+ return {
3149
+ reply: language === "en" ? "(agent turn limit reached)" : "(limite de turnos do agente atingido)",
3150
+ model: MODEL4,
3151
+ lang: language,
3152
+ didWrite,
3153
+ toolsAvailable: tools2.length
3154
+ };
3155
+ }
3156
+ });
3157
+
3158
+ // server/src/services/audio.ts
3159
+ var audio_default2 = ({ strapi }) => ({
3160
+ async transcribe(buffer, mimetype, language) {
3161
+ const key = process.env.OPENAI_API_KEY;
3162
+ if (!key) throw new Error("OPENAI_API_KEY n\xE3o configurada no .env (necess\xE1ria p/ \xE1udio).");
3163
+ const ext = mimetype.includes("mp4") ? "mp4" : mimetype.includes("ogg") ? "ogg" : mimetype.includes("wav") ? "wav" : "webm";
3164
+ const form = new FormData();
3165
+ form.append("file", new Blob([buffer], { type: mimetype }), `audio.${ext}`);
3166
+ form.append("model", "whisper-1");
3167
+ const raw = Array.isArray(language) ? language[0] : language;
3168
+ const lang = raw === "en" || raw === "pt" ? raw : void 0;
3169
+ if (lang) form.append("language", lang);
3170
+ const res = await fetch("https://api.openai.com/v1/audio/transcriptions", {
3171
+ method: "POST",
3172
+ headers: { Authorization: `Bearer ${key}` },
3173
+ body: form
3174
+ });
3175
+ if (!res.ok) throw new Error(`OpenAI STT: ${await res.text()}`);
3176
+ const data = await res.json();
3177
+ return { text: data.text };
3178
+ },
3179
+ async synthesize(text, voice = "echo") {
3180
+ const key = process.env.OPENAI_API_KEY;
3181
+ if (!key) throw new Error("OPENAI_API_KEY n\xE3o configurada no .env (necess\xE1ria p/ \xE1udio).");
3182
+ const res = await fetch("https://api.openai.com/v1/audio/speech", {
3183
+ method: "POST",
3184
+ headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
3185
+ body: JSON.stringify({ model: "tts-1", input: text, voice, response_format: "mp3" })
3186
+ });
3187
+ if (!res.ok) throw new Error(`OpenAI TTS: ${await res.text()}`);
3188
+ const buffer = Buffer.from(await res.arrayBuffer());
3189
+ return { audio_base64: buffer.toString("base64"), content_type: "audio/mpeg" };
3190
+ }
3191
+ });
3192
+
3193
+ // server/src/routes/index.ts
3194
+ var routes_default = {
3195
+ admin: {
3196
+ type: "admin",
3197
+ routes: [
3198
+ {
3199
+ method: "POST",
3200
+ path: "/message",
3201
+ handler: "chat.message",
3202
+ config: { policies: [] }
3203
+ },
3204
+ {
3205
+ method: "POST",
3206
+ path: "/stt",
3207
+ handler: "audio.stt",
3208
+ config: { policies: [] }
3209
+ },
3210
+ {
3211
+ method: "POST",
3212
+ path: "/tts",
3213
+ handler: "audio.tts",
3214
+ config: { policies: [] }
3215
+ },
3216
+ {
3217
+ method: "POST",
3218
+ path: "/frontend/analyze",
3219
+ handler: "frontend.analyze",
3220
+ config: { policies: [] }
3221
+ },
3222
+ {
3223
+ method: "POST",
3224
+ path: "/frontend/provision",
3225
+ handler: "frontend.provision",
3226
+ config: { policies: [] }
3227
+ },
3228
+ {
3229
+ method: "GET",
3230
+ path: "/frontend/status",
3231
+ handler: "frontend.status",
3232
+ config: { policies: [] }
3233
+ },
3234
+ {
3235
+ method: "POST",
3236
+ path: "/frontend/run",
3237
+ handler: "frontend.run",
3238
+ config: { policies: [] }
3239
+ },
3240
+ {
3241
+ method: "GET",
3242
+ path: "/frontend/run-status",
3243
+ handler: "frontend.runStatus",
3244
+ config: { policies: [] }
3245
+ },
3246
+ {
3247
+ method: "POST",
3248
+ path: "/frontend/integrate",
3249
+ handler: "frontend.integrate",
3250
+ config: { policies: [] }
3251
+ }
3252
+ ]
3253
+ }
3254
+ };
3255
+
3256
+ // server/src/mcp/tools/buscar-texto.ts
3257
+ var import_utils2 = require("@strapi/utils");
3258
+ var tool = {
3259
+ register(registerTool) {
3260
+ registerTool({
3261
+ name: "mcp_chat_buscar_texto",
3262
+ title: "Search text across content (deep)",
3263
+ description: 'Search a phrase across ALL content-types, single types, components and dynamic zones (recursive, substring). Returns matches with a `path` (e.g. ["dynamic_zone",2,"heading"]) to pass to mcp_chat_editar_campo.',
3264
+ resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
3265
+ resolveOutputSchema: () => import_utils2.z.object({
3266
+ total: import_utils2.z.number().optional(),
3267
+ resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
3268
+ erro: import_utils2.z.string().optional()
3269
+ }),
3270
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3271
+ createHandler: (strapi) => async ({ args }) => {
3272
+ const r = await createContentTools(strapi).buscarTexto(args?.termo);
3273
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3274
+ }
3275
+ });
3276
+ }
3277
+ };
3278
+ var buscar_texto_default = tool;
3279
+
3280
+ // server/src/mcp/tools/editar-campo.ts
3281
+ var import_utils3 = require("@strapi/utils");
3282
+ var tool2 = {
3283
+ register(registerTool) {
3284
+ registerTool({
3285
+ name: "mcp_chat_editar_campo",
3286
+ title: "Edit a (possibly nested) field",
3287
+ description: "Edit a field value (saved as draft), including text nested in components/dynamic zones. Pass the `path` exactly as returned by mcp_chat_buscar_texto; for a simple top-level field you may use `campo`.",
3288
+ resolveInputSchema: () => import_utils3.z.object({
3289
+ uid: import_utils3.z.string(),
3290
+ documentId: import_utils3.z.string(),
3291
+ path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
3292
+ campo: import_utils3.z.string().optional(),
3293
+ novo_valor: import_utils3.z.string(),
3294
+ locale: import_utils3.z.string().optional()
3295
+ }),
3296
+ resolveOutputSchema: () => import_utils3.z.object({
3297
+ ok: import_utils3.z.boolean().optional(),
3298
+ uid: import_utils3.z.string().optional(),
3299
+ documentId: import_utils3.z.string().optional(),
3300
+ path: import_utils3.z.array(import_utils3.z.any()).optional(),
3301
+ novo_valor: import_utils3.z.string().optional(),
3302
+ erro: import_utils3.z.string().optional()
3303
+ }),
3304
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3305
+ createHandler: (strapi) => async ({ args }) => {
3306
+ const r = await createContentTools(strapi).editarCampo(args);
3307
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3308
+ }
3309
+ });
3310
+ }
3311
+ };
3312
+ var editar_campo_default = tool2;
3313
+
3314
+ // server/src/mcp/tools/publicar.ts
3315
+ var import_utils4 = require("@strapi/utils");
3316
+ var tool3 = {
3317
+ register(registerTool) {
3318
+ registerTool({
3319
+ name: "mcp_chat_publicar",
3320
+ title: "Publish an entry",
3321
+ description: 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all.',
3322
+ resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
3323
+ resolveOutputSchema: () => import_utils4.z.object({
3324
+ ok: import_utils4.z.boolean().optional(),
3325
+ uid: import_utils4.z.string().optional(),
3326
+ documentId: import_utils4.z.string().optional(),
3327
+ status: import_utils4.z.string().optional(),
3328
+ locale: import_utils4.z.string().optional()
3329
+ }),
3330
+ auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
3331
+ createHandler: (strapi) => async ({ args }) => {
3332
+ const r = await createContentTools(strapi).publicar(args);
3333
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3334
+ }
3335
+ });
3336
+ }
3337
+ };
3338
+ var publicar_default = tool3;
3339
+
3340
+ // server/src/mcp/tools/listar-locales.ts
3341
+ var import_utils5 = require("@strapi/utils");
3342
+ var tool4 = {
3343
+ register(registerTool) {
3344
+ registerTool({
3345
+ name: "mcp_chat_listar_locales",
3346
+ title: "List i18n locales",
3347
+ description: "List the configured locales (languages) and which one is the default.",
3348
+ resolveInputSchema: () => import_utils5.z.object({}),
3349
+ resolveOutputSchema: () => import_utils5.z.object({
3350
+ default: import_utils5.z.string().optional(),
3351
+ locales: import_utils5.z.array(import_utils5.z.any()).optional(),
3352
+ erro: import_utils5.z.string().optional()
3353
+ }),
3354
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3355
+ createHandler: (strapi) => async () => {
3356
+ const r = await createContentTools(strapi).listarLocales();
3357
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3358
+ }
3359
+ });
3360
+ }
3361
+ };
3362
+ var listar_locales_default = tool4;
3363
+
3364
+ // server/src/mcp/tools/criar-locale.ts
3365
+ var import_utils6 = require("@strapi/utils");
3366
+ var tool5 = {
3367
+ register(registerTool) {
3368
+ registerTool({
3369
+ name: "mcp_chat_criar_locale",
3370
+ title: "Create an i18n locale",
3371
+ description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
3372
+ resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
3373
+ resolveOutputSchema: () => import_utils6.z.object({
3374
+ ok: import_utils6.z.boolean().optional(),
3375
+ code: import_utils6.z.string().optional(),
3376
+ name: import_utils6.z.string().optional(),
3377
+ existed: import_utils6.z.boolean().optional(),
3378
+ erro: import_utils6.z.string().optional()
3379
+ }),
3380
+ auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
3381
+ createHandler: (strapi) => async ({ args }) => {
3382
+ const r = await createContentTools(strapi).criarLocale(args);
3383
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3384
+ }
3385
+ });
3386
+ }
3387
+ };
3388
+ var criar_locale_default = tool5;
3389
+
3390
+ // server/src/mcp/tools/traduzir.ts
3391
+ var import_utils7 = require("@strapi/utils");
3392
+ var tool6 = {
3393
+ register(registerTool) {
3394
+ registerTool({
3395
+ name: "mcp_chat_traduzir",
3396
+ title: "Translate localized content",
3397
+ description: "Translate localized content into one or more languages. Creates missing locales, translates field by field (long text is split and reassembled, never overflows) and publishes. Without uid/documentId, translates ALL localized content-types. Handles many locales at once.",
3398
+ resolveInputSchema: () => import_utils7.z.object({
3399
+ target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
3400
+ source_locale: import_utils7.z.string().optional(),
3401
+ uid: import_utils7.z.string().optional(),
3402
+ documentId: import_utils7.z.string().optional(),
3403
+ publish: import_utils7.z.boolean().optional()
3404
+ }),
3405
+ resolveOutputSchema: () => import_utils7.z.object({
3406
+ ok: import_utils7.z.boolean().optional(),
3407
+ source: import_utils7.z.string().optional(),
3408
+ por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
3409
+ erro: import_utils7.z.string().optional()
3410
+ }),
3411
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3412
+ createHandler: (strapi) => async ({ args }) => {
3413
+ const r = await createContentTools(strapi).traduzir(args);
3414
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3415
+ }
3416
+ });
3417
+ }
3418
+ };
3419
+ var traduzir_default = tool6;
3420
+
3421
+ // server/src/mcp/tools/habilitar-i18n.ts
3422
+ var import_utils8 = require("@strapi/utils");
3423
+ var tool7 = {
3424
+ register(registerTool) {
3425
+ registerTool({
3426
+ name: "mcp_chat_habilitar_i18n",
3427
+ title: "Enable i18n on a content-type",
3428
+ description: 'Enable translation on content-types not localized yet: marks the content-type and its textual fields/components as localized. Required before translating content provisioned without i18n. Omit `uid` (or pass "*") to enable ALL content-types at once. Edits the schema (dev-only); Strapi restarts.',
3429
+ resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
3430
+ resolveOutputSchema: () => import_utils8.z.object({
3431
+ ok: import_utils8.z.boolean().optional(),
3432
+ uid: import_utils8.z.string().optional(),
3433
+ campos: import_utils8.z.array(import_utils8.z.string()).optional(),
3434
+ contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
3435
+ total: import_utils8.z.number().optional(),
3436
+ restart: import_utils8.z.boolean().optional(),
3437
+ erro: import_utils8.z.string().optional()
3438
+ }),
3439
+ auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
3440
+ createHandler: (strapi) => async ({ args }) => {
3441
+ const r = enableI18n({ strapi, uid: args?.uid, campos: args?.campos });
3442
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3443
+ }
3444
+ });
3445
+ }
3446
+ };
3447
+ var habilitar_i18n_default = tool7;
3448
+
3449
+ // server/src/mcp/tools/index.ts
3450
+ var tools = [
3451
+ buscar_texto_default,
3452
+ editar_campo_default,
3453
+ publicar_default,
3454
+ listar_locales_default,
3455
+ criar_locale_default,
3456
+ traduzir_default,
3457
+ habilitar_i18n_default
3458
+ ];
3459
+
3460
+ // server/src/mcp/index.ts
3461
+ var registerMcpTools = (strapi) => {
3462
+ const mcp = strapi?.ai?.mcp;
3463
+ const enabled = typeof mcp?.isEnabled === "function" ? mcp.isEnabled() : !!mcp?.registerTool;
3464
+ if (!mcp || typeof mcp.registerTool !== "function" || !enabled) {
3465
+ strapi.log.warn(
3466
+ "[mcp-chat] MCP nativo indispon\xEDvel/desligado \u2014 tools N\xC3O registradas. Requer Strapi >= 5.47.0 com `mcp: { enabled: true }` em config/server."
3467
+ );
3468
+ return;
3469
+ }
3470
+ const { registerTool } = mcp;
3471
+ for (const tool8 of tools) tool8.register(registerTool, strapi);
3472
+ strapi.log.info(`[mcp-chat] ${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
3473
+ };
3474
+
3475
+ // server/src/register.ts
3476
+ var register_default = ({ strapi }) => {
3477
+ try {
3478
+ registerMcpTools(strapi);
3479
+ } catch (e) {
3480
+ strapi.log.warn(`[mcp-chat] registro do MCP falhou (seguindo sem ele): ${e?.message ?? e}`);
3481
+ }
3482
+ };
3483
+
3484
+ // server/src/index.ts
3485
+ var index_default = {
3486
+ register: register_default,
3487
+ async bootstrap({ strapi }) {
3488
+ try {
3489
+ const r = await runPendingProvision(strapi, strapi.dirs.app.root);
3490
+ if (r.ran) {
3491
+ strapi.log.info(
3492
+ `[mcp-chat] provis\xE3o conclu\xEDda: seed=${JSON.stringify(r.seed?.created ?? [])} link=${r.link?.previewAction ?? "n/a"}`
3493
+ );
3494
+ if (r.errors.length) strapi.log.warn(`[mcp-chat] provis\xE3o com avisos: ${r.errors.join("; ")}`);
3495
+ }
3496
+ } catch (e) {
3497
+ strapi.log.error(`[mcp-chat] runPendingProvision falhou: ${e?.message ?? e}`);
3498
+ }
3499
+ },
3500
+ destroy() {
3501
+ stopFrontend();
3502
+ },
3503
+ config: {
3504
+ default: {},
3505
+ validator() {
3506
+ }
3507
+ },
3508
+ controllers: { chat: chat_default, audio: audio_default, frontend: frontend_default },
3509
+ routes: routes_default,
3510
+ services: { chat: chat_default2, audio: audio_default2 }
3511
+ };