strapi-plugin-mcp-chat 0.5.0 → 0.6.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.
@@ -36,12 +36,12 @@ module.exports = __toCommonJS(index_exports);
36
36
  // server/src/controllers/chat.ts
37
37
  var chat_default = ({ strapi }) => ({
38
38
  async message(ctx) {
39
- const { messages, image, lang, previewUrl, autoPublish } = ctx.request.body || {};
39
+ const { messages, image, lang, previewUrl, previewStatus, autoPublish } = ctx.request.body || {};
40
40
  if (!Array.isArray(messages) || messages.length === 0) {
41
41
  return ctx.badRequest('Campo "messages" (array) \xE9 obrigat\xF3rio.');
42
42
  }
43
43
  try {
44
- const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, autoPublish });
44
+ const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, previewStatus, autoPublish });
45
45
  ctx.body = result;
46
46
  } catch (e) {
47
47
  strapi.log.error(`[mcp-chat] ${e?.message || e}`);
@@ -88,8 +88,8 @@ var import_node_path7 = __toESM(require("node:path"));
88
88
  var import_jszip = __toESM(require("jszip"));
89
89
 
90
90
  // server/src/provision/orchestrate.ts
91
- var import_node_fs3 = __toESM(require("node:fs"));
92
- var import_node_path3 = __toESM(require("node:path"));
91
+ var import_node_fs4 = __toESM(require("node:fs"));
92
+ var import_node_path4 = __toESM(require("node:path"));
93
93
 
94
94
  // server/src/provision/manifest.ts
95
95
  var import_utils = require("@strapi/utils");
@@ -633,8 +633,8 @@ async function seedContent(strapi, manifest) {
633
633
  }
634
634
 
635
635
  // server/src/provision/link.ts
636
- var import_node_fs2 = __toESM(require("node:fs"));
637
- var import_node_path2 = __toESM(require("node:path"));
636
+ var import_node_fs3 = __toESM(require("node:fs"));
637
+ var import_node_path3 = __toESM(require("node:path"));
638
638
 
639
639
  // server/src/provision/adapters.ts
640
640
  var nextAdapter = {
@@ -770,6 +770,179 @@ ${interfaces}
770
770
  `;
771
771
  }
772
772
 
773
+ // server/src/provision/runner.ts
774
+ var import_node_child_process = require("node:child_process");
775
+ var import_node_net = __toESM(require("node:net"));
776
+ var import_node_fs2 = __toESM(require("node:fs"));
777
+ var import_node_path2 = __toESM(require("node:path"));
778
+ var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
779
+ var child = null;
780
+ var pollTimer = null;
781
+ var appRootForPid = null;
782
+ function pidFilePath(appRoot) {
783
+ return import_node_path2.default.join(appRoot, ".mcp-chat", "frontend.pid");
784
+ }
785
+ function writePid(pid) {
786
+ try {
787
+ if (!appRootForPid) return;
788
+ const pf = pidFilePath(appRootForPid);
789
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(pf), { recursive: true });
790
+ import_node_fs2.default.writeFileSync(pf, String(pid), "utf8");
791
+ } catch {
792
+ }
793
+ }
794
+ function clearPid() {
795
+ try {
796
+ if (appRootForPid) import_node_fs2.default.unlinkSync(pidFilePath(appRootForPid));
797
+ } catch {
798
+ }
799
+ }
800
+ function isAlive(pid) {
801
+ try {
802
+ process.kill(pid, 0);
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ function cleanupStaleFrontend(appRoot) {
809
+ appRootForPid = appRoot;
810
+ try {
811
+ const pf = pidFilePath(appRoot);
812
+ if (!import_node_fs2.default.existsSync(pf)) return;
813
+ const pid = parseInt(import_node_fs2.default.readFileSync(pf, "utf8").trim(), 10);
814
+ if (pid && isAlive(pid)) {
815
+ try {
816
+ process.kill(pid, "SIGTERM");
817
+ } catch {
818
+ }
819
+ }
820
+ import_node_fs2.default.unlinkSync(pf);
821
+ } catch {
822
+ }
823
+ }
824
+ function detectPM(dir) {
825
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lockb")) || import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lock"))) return "bun";
826
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
827
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "yarn.lock"))) return "yarn";
828
+ return "npm";
829
+ }
830
+ var has = (dir, ...names) => names.some((n) => import_node_fs2.default.existsSync(import_node_path2.default.join(dir, n)));
831
+ function detectFramework(dir) {
832
+ if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
833
+ if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
834
+ return "other";
835
+ }
836
+ var FRONTEND_BASE_PORT = 4321;
837
+ function findFreePort(start) {
838
+ return new Promise((resolve) => {
839
+ const tryPort = (p) => {
840
+ if (p > start + 200) return resolve(start);
841
+ const srv = import_node_net.default.createServer();
842
+ srv.once("error", () => tryPort(p + 1));
843
+ srv.once("listening", () => srv.close(() => resolve(p)));
844
+ srv.listen(p, "0.0.0.0");
845
+ };
846
+ tryPort(start);
847
+ });
848
+ }
849
+ function pushLog(s) {
850
+ for (const line of String(s).split("\n")) {
851
+ const t = line.trim();
852
+ if (t) info.log.push(t);
853
+ }
854
+ if (info.log.length > 60) info.log = info.log.slice(-60);
855
+ }
856
+ async function urlUp(url) {
857
+ try {
858
+ const res = await fetch(url, { method: "GET" });
859
+ return res.status >= 200 && res.status < 400;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+ function getRunStatus() {
865
+ return { ...info, log: info.log.slice(-15) };
866
+ }
867
+ function stopFrontend() {
868
+ if (pollTimer) {
869
+ clearInterval(pollTimer);
870
+ pollTimer = null;
871
+ }
872
+ if (child) {
873
+ try {
874
+ child.kill("SIGTERM");
875
+ } catch {
876
+ }
877
+ child = null;
878
+ }
879
+ clearPid();
880
+ if (info.state !== "error") info.state = "idle";
881
+ }
882
+ async function startFrontend(_strapi, opts) {
883
+ const { dir } = opts;
884
+ if (_strapi?.dirs?.app?.root) appRootForPid = _strapi.dirs.app.root;
885
+ if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
886
+ return getRunStatus();
887
+ }
888
+ stopFrontend();
889
+ const pm = detectPM(dir);
890
+ const framework = detectFramework(dir);
891
+ const port = await findFreePort(FRONTEND_BASE_PORT);
892
+ const url = `http://127.0.0.1:${port}`;
893
+ info = { state: "installing", dir, url, pm, error: null, log: [] };
894
+ const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
895
+ 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)];
896
+ const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
897
+ const startDev = () => {
898
+ info.state = "starting";
899
+ child = spawnIn(pm, devArgs);
900
+ if (child.pid) writePid(child.pid);
901
+ child.stdout?.on("data", (d) => pushLog(d));
902
+ child.stderr?.on("data", (d) => pushLog(d));
903
+ child.on("exit", (code) => {
904
+ child = null;
905
+ clearPid();
906
+ if (pollTimer) {
907
+ clearInterval(pollTimer);
908
+ pollTimer = null;
909
+ }
910
+ if (info.state === "running") info.state = "idle";
911
+ else {
912
+ info.state = "error";
913
+ info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
914
+ }
915
+ });
916
+ pollTimer = setInterval(async () => {
917
+ if (await urlUp(url)) {
918
+ info.state = "running";
919
+ if (pollTimer) {
920
+ clearInterval(pollTimer);
921
+ pollTimer = null;
922
+ }
923
+ }
924
+ }, 1500);
925
+ };
926
+ const needInstall = !import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "node_modules"));
927
+ if (needInstall) {
928
+ pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
929
+ const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
930
+ const inst = spawnIn(pm, installArgs);
931
+ inst.stdout?.on("data", (d) => pushLog(d));
932
+ inst.stderr?.on("data", (d) => pushLog(d));
933
+ inst.on("exit", (code) => {
934
+ if (code === 0) startDev();
935
+ else {
936
+ info.state = "error";
937
+ info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
938
+ }
939
+ });
940
+ } else {
941
+ startDev();
942
+ }
943
+ return getRunStatus();
944
+ }
945
+
773
946
  // server/src/provision/link.ts
774
947
  function parseEnv(content) {
775
948
  const out = {};
@@ -799,48 +972,120 @@ function mergeEnv(existing, next) {
799
972
  }
800
973
  return { content, added, preserved };
801
974
  }
802
- function buildPreviewConfig(manifest) {
975
+ var PREVIEW_MARKER = "mcp-chat:preview-merged";
976
+ var PREVIEW_MODULE = "mcp-chat-preview";
977
+ function buildPreviewConfig(manifest, framework = manifest.framework) {
803
978
  const routes = {};
804
979
  for (const ct of manifest.contentTypes) {
805
- if (ct.preview?.route) routes[apiUid(ct.singularName)] = ct.preview.route;
980
+ routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
806
981
  }
807
982
  const routesJson = JSON.stringify(routes, null, 2);
808
- return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json.
983
+ const isNext = framework === "next";
984
+ const urlBranch = isNext ? ` // Next.js: rota de draft mode que seta o cookie e redireciona p/ \`path\`.
985
+ const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
986
+ return \`\${clientUrl}/api/preview?\${qs.toString()}\`;` : ` // SPA (Vite/TanStack): abre a p\xE1gina direta; o front l\xEA ?preview/status.
987
+ const qs = new URLSearchParams({ preview: '1', status: status ?? 'draft', secret });
988
+ return \`\${clientUrl}\${pathname}?\${qs.toString()}\`;`;
989
+ return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json (framework: ${framework}).
809
990
  // Mapa uid -> rota do frontend (placeholders :campo s\xE3o preenchidos pelo doc).
991
+ // Este arquivo \xE9 mesclado em config/admin.ts \u2014 n\xE3o precisa edit\xE1-lo \xE0 m\xE3o.
810
992
  const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
811
993
 
812
- export default ({ env }) => ({
813
- auth: {
814
- secret: env('ADMIN_JWT_SECRET'),
815
- },
816
- apiToken: { salt: env('API_TOKEN_SALT') },
817
- transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
994
+ export default ({ env }: { env: any }) => ({
818
995
  preview: {
819
996
  enabled: true,
820
997
  config: {
821
998
  allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
822
999
  async handler(uid: string, { documentId, locale, status }: any) {
823
- const route = PREVIEW_ROUTES[uid];
824
- if (!route) return null;
825
- const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
826
- if (!doc) return null;
827
- // substitui :campo pelos valores do documento (ex.: :slug)
828
- const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
829
- encodeURIComponent(String((doc as any)[f] ?? ''))
830
- );
1000
+ const route = PREVIEW_ROUTES[uid] ?? '/';
831
1001
  const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
832
1002
  const secret = env('PREVIEW_SECRET', '');
833
- const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
834
- return \`\${clientUrl}/api/preview?\${qs.toString()}\`;
1003
+
1004
+ // s\xF3 busca o doc se a rota tiver placeholders (ex.: :slug) a preencher.
1005
+ let pathname = route;
1006
+ if (pathname.includes(':')) {
1007
+ const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
1008
+ if (!doc) return null;
1009
+ pathname = pathname.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
1010
+ encodeURIComponent(String((doc as any)[f] ?? ''))
1011
+ );
1012
+ }
1013
+
1014
+ ${urlBranch}
835
1015
  },
836
1016
  },
837
1017
  },
838
1018
  });
839
1019
  `;
840
1020
  }
1021
+ function buildStandaloneAdmin() {
1022
+ return `// ${PREVIEW_MARKER} \u2014 admin.ts gerado pelo mcp-chat (preview inclu\xEDdo).
1023
+ import previewConfig from './${PREVIEW_MODULE}';
1024
+
1025
+ export default ({ env }: { env: any }) => ({
1026
+ auth: { secret: env('ADMIN_JWT_SECRET') },
1027
+ apiToken: { salt: env('API_TOKEN_SALT') },
1028
+ transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
1029
+ secrets: { encryptionKey: env('ENCRYPTION_KEY') },
1030
+ flags: {
1031
+ nps: env.bool('FLAG_NPS', true),
1032
+ promoteEE: env.bool('FLAG_PROMOTE_EE', true),
1033
+ },
1034
+ ...previewConfig({ env }),
1035
+ });
1036
+ `;
1037
+ }
1038
+ function buildAdminWrapper() {
1039
+ return `// ${PREVIEW_MARKER} \u2014 preview do mcp-chat mesclado sobre o admin original.
1040
+ // Sua config original est\xE1 preservada em ./admin.base \u2014 edite l\xE1, n\xE3o aqui.
1041
+ import base from './admin.base';
1042
+ import previewConfig from './${PREVIEW_MODULE}';
1043
+
1044
+ export default (ctx: any) => {
1045
+ const b = typeof base === 'function' ? (base as any)(ctx) : (base ?? {});
1046
+ return { ...b, ...previewConfig(ctx) };
1047
+ };
1048
+ `;
1049
+ }
1050
+ var CSP_MARKER = "mcp-chat:csp-frame";
1051
+ function securityBlock() {
1052
+ return ` // ${CSP_MARKER} \u2014 libera o frame-src p/ o admin embutir o preview do frontend
1053
+ // (dev server local em qualquer porta). Sem isto a CSP padr\xE3o (default-src 'self')
1054
+ // bloqueia o iframe e o preview fica em branco.
1055
+ {
1056
+ name: 'strapi::security',
1057
+ config: {
1058
+ contentSecurityPolicy: {
1059
+ useDefaults: true,
1060
+ directives: {
1061
+ 'connect-src': ["'self'", 'https:', 'http:'],
1062
+ 'frame-src': ["'self'", 'http://localhost:*', 'http://127.0.0.1:*'],
1063
+ 'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io'],
1064
+ 'media-src': ["'self'", 'data:', 'blob:'],
1065
+ upgradeInsecureRequests: null,
1066
+ },
1067
+ },
1068
+ },
1069
+ },`;
1070
+ }
1071
+ function patchSecurityMiddleware(strapiAppDir, dryRun) {
1072
+ const configDir = import_node_path3.default.join(strapiAppDir, "config");
1073
+ const file = ["middlewares.ts", "middlewares.js"].find(
1074
+ (f) => import_node_fs3.default.existsSync(import_node_path3.default.join(configDir, f))
1075
+ );
1076
+ if (!file) return "skipped";
1077
+ const p = import_node_path3.default.join(configDir, file);
1078
+ const content = import_node_fs3.default.readFileSync(p, "utf8");
1079
+ if (content.includes(CSP_MARKER) || content.includes("'frame-src'")) return "already";
1080
+ const m = content.match(/^[ \t]*['"]strapi::security['"]\s*,/m);
1081
+ if (!m) return "manual";
1082
+ const next = content.replace(m[0], securityBlock());
1083
+ if (!dryRun) import_node_fs3.default.writeFileSync(p, next, "utf8");
1084
+ return "patched";
1085
+ }
841
1086
  function ensureInside(base, target) {
842
- const n = import_node_path2.default.normalize(target);
843
- return n === base || n.startsWith(base + import_node_path2.default.sep);
1087
+ const n = import_node_path3.default.normalize(target);
1088
+ return n === base || n.startsWith(base + import_node_path3.default.sep);
844
1089
  }
845
1090
  function linkFrontend(manifest, opts) {
846
1091
  const adapter = adapterForManifest(manifest);
@@ -852,49 +1097,90 @@ function linkFrontend(manifest, opts) {
852
1097
  typesFile: "strapi-types.ts",
853
1098
  previewFile: "config/admin.ts",
854
1099
  previewAction: "skipped",
1100
+ backendEnvAdded: [],
1101
+ cspAction: "skipped",
855
1102
  errors: []
856
1103
  };
857
- if (!import_node_path2.default.isAbsolute(opts.frontendDir) || !import_node_path2.default.isAbsolute(opts.strapiAppDir)) {
1104
+ if (!import_node_path3.default.isAbsolute(opts.frontendDir) || !import_node_path3.default.isAbsolute(opts.strapiAppDir)) {
858
1105
  result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
859
1106
  return result;
860
1107
  }
861
1108
  try {
862
- const envPath = import_node_path2.default.join(opts.frontendDir, adapter.envFileName);
1109
+ const envPath = import_node_path3.default.join(opts.frontendDir, adapter.envFileName);
863
1110
  if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
864
- const existing = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : "";
1111
+ const existing = import_node_fs3.default.existsSync(envPath) ? import_node_fs3.default.readFileSync(envPath, "utf8") : "";
865
1112
  const vars = adapter.buildEnv(opts.context);
866
1113
  const { content, added, preserved } = mergeEnv(existing, vars);
867
1114
  result.envAdded = added;
868
1115
  result.envPreserved = preserved;
869
- if (!opts.dryRun && added.length) import_node_fs2.default.writeFileSync(envPath, content, "utf8");
1116
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(envPath, content, "utf8");
870
1117
  } catch (e) {
871
1118
  result.errors.push(`env: ${e?.message ?? e}`);
872
1119
  }
873
1120
  try {
874
- const typesPath = import_node_path2.default.join(opts.frontendDir, result.typesFile);
1121
+ const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
875
1122
  if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
876
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
1123
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
877
1124
  } catch (e) {
878
1125
  result.errors.push(`types: ${e?.message ?? e}`);
879
1126
  }
880
1127
  try {
881
- const adminPath = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.ts");
882
- const content = buildPreviewConfig(manifest);
883
- if (import_node_fs2.default.existsSync(adminPath)) {
884
- result.previewFile = "config/admin.mcp-chat-preview.ts";
885
- const sidecar = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.mcp-chat-preview.ts");
886
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(sidecar, content, "utf8");
887
- result.previewAction = "sidecar";
888
- } else {
889
- if (!opts.dryRun) {
890
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(adminPath), { recursive: true });
891
- import_node_fs2.default.writeFileSync(adminPath, content, "utf8");
1128
+ const configDir = import_node_path3.default.join(opts.strapiAppDir, "config");
1129
+ if (!ensureInside(opts.strapiAppDir, configDir)) throw new Error("config fora do strapiAppDir");
1130
+ const adminPath = import_node_path3.default.join(configDir, "admin.ts");
1131
+ const adminBasePath = import_node_path3.default.join(configDir, "admin.base.ts");
1132
+ const modulePath = import_node_path3.default.join(configDir, `${PREVIEW_MODULE}.ts`);
1133
+ if (!opts.dryRun) {
1134
+ import_node_fs3.default.mkdirSync(configDir, { recursive: true });
1135
+ import_node_fs3.default.writeFileSync(modulePath, buildPreviewConfig(manifest, adapter.framework), "utf8");
1136
+ try {
1137
+ import_node_fs3.default.unlinkSync(import_node_path3.default.join(configDir, "admin.mcp-chat-preview.ts"));
1138
+ } catch {
892
1139
  }
1140
+ }
1141
+ result.previewFile = `config/${PREVIEW_MODULE}.ts`;
1142
+ if (!import_node_fs3.default.existsSync(adminPath)) {
1143
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(adminPath, buildStandaloneAdmin(), "utf8");
893
1144
  result.previewAction = "created";
1145
+ } else {
1146
+ const adminContent = import_node_fs3.default.readFileSync(adminPath, "utf8");
1147
+ if (!adminContent.includes(PREVIEW_MARKER)) {
1148
+ if (!opts.dryRun) {
1149
+ if (!import_node_fs3.default.existsSync(adminBasePath)) {
1150
+ import_node_fs3.default.writeFileSync(adminBasePath, adminContent, "utf8");
1151
+ }
1152
+ import_node_fs3.default.writeFileSync(adminPath, buildAdminWrapper(), "utf8");
1153
+ }
1154
+ }
1155
+ result.previewAction = "merged";
894
1156
  }
895
1157
  } catch (e) {
896
1158
  result.errors.push(`preview: ${e?.message ?? e}`);
897
1159
  }
1160
+ try {
1161
+ const clientUrl = opts.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
1162
+ const backendVars = { CLIENT_URL: clientUrl };
1163
+ if (opts.context.previewSecret) backendVars.PREVIEW_SECRET = opts.context.previewSecret;
1164
+ const backendEnvPath = import_node_path3.default.join(opts.strapiAppDir, ".env");
1165
+ if (ensureInside(opts.strapiAppDir, backendEnvPath)) {
1166
+ const existing = import_node_fs3.default.existsSync(backendEnvPath) ? import_node_fs3.default.readFileSync(backendEnvPath, "utf8") : "";
1167
+ const { content, added } = mergeEnv(existing, backendVars);
1168
+ result.backendEnvAdded = added;
1169
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(backendEnvPath, content, "utf8");
1170
+ }
1171
+ } catch (e) {
1172
+ result.errors.push(`backend env: ${e?.message ?? e}`);
1173
+ }
1174
+ try {
1175
+ result.cspAction = patchSecurityMiddleware(opts.strapiAppDir, opts.dryRun);
1176
+ if (result.cspAction === "manual") {
1177
+ result.errors.push(
1178
+ "CSP: config/middlewares j\xE1 tem strapi::security customizado \u2014 adicione manualmente frame-src 'self' http://localhost:* http://127.0.0.1:* para o preview embutir o frontend."
1179
+ );
1180
+ }
1181
+ } catch (e) {
1182
+ result.errors.push(`csp: ${e?.message ?? e}`);
1183
+ }
898
1184
  result.ok = result.errors.length === 0;
899
1185
  return result;
900
1186
  }
@@ -937,17 +1223,17 @@ var MARKER_DIR = ".mcp-chat";
937
1223
  var MARKER_FILE = "pending-provision.json";
938
1224
  var DONE_FILE = "last-provision.json";
939
1225
  function markerPath(strapiAppDir) {
940
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
1226
+ return import_node_path4.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
941
1227
  }
942
1228
  function donePath(strapiAppDir) {
943
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
1229
+ return import_node_path4.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
944
1230
  }
945
1231
  function getProvisionStatus(strapiAppDir) {
946
- const pending = import_node_fs3.default.existsSync(markerPath(strapiAppDir));
1232
+ const pending = import_node_fs4.default.existsSync(markerPath(strapiAppDir));
947
1233
  let done = null;
948
1234
  try {
949
1235
  const dp = donePath(strapiAppDir);
950
- if (import_node_fs3.default.existsSync(dp)) done = JSON.parse(import_node_fs3.default.readFileSync(dp, "utf8"));
1236
+ if (import_node_fs4.default.existsSync(dp)) done = JSON.parse(import_node_fs4.default.readFileSync(dp, "utf8"));
951
1237
  } catch {
952
1238
  }
953
1239
  return { pending, done };
@@ -983,10 +1269,10 @@ function stageProvision(strapi, input) {
983
1269
  if (!input.dryRun) {
984
1270
  try {
985
1271
  const mp = markerPath(input.strapiAppDir);
986
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(mp), { recursive: true });
987
- import_node_fs3.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
1272
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(mp), { recursive: true });
1273
+ import_node_fs4.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
988
1274
  try {
989
- import_node_fs3.default.unlinkSync(donePath(input.strapiAppDir));
1275
+ import_node_fs4.default.unlinkSync(donePath(input.strapiAppDir));
990
1276
  } catch {
991
1277
  }
992
1278
  result.staged = true;
@@ -1002,10 +1288,10 @@ function stageProvision(strapi, input) {
1002
1288
  async function runPendingProvision(strapi, strapiAppDir) {
1003
1289
  const result = { ran: false, errors: [] };
1004
1290
  const mp = markerPath(strapiAppDir);
1005
- if (!import_node_fs3.default.existsSync(mp)) return result;
1291
+ if (!import_node_fs4.default.existsSync(mp)) return result;
1006
1292
  let marker;
1007
1293
  try {
1008
- marker = JSON.parse(import_node_fs3.default.readFileSync(mp, "utf8"));
1294
+ marker = JSON.parse(import_node_fs4.default.readFileSync(mp, "utf8"));
1009
1295
  } catch (e) {
1010
1296
  result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
1011
1297
  return result;
@@ -1034,8 +1320,7 @@ async function runPendingProvision(strapi, strapiAppDir) {
1034
1320
  result.errors.push(`link: ${e?.message ?? e}`);
1035
1321
  }
1036
1322
  try {
1037
- const adapter = adapterForManifest(marker.manifest);
1038
- const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
1323
+ const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
1039
1324
  const done = {
1040
1325
  name: marker.manifest.name,
1041
1326
  framework: marker.manifest.framework,
@@ -1047,21 +1332,21 @@ async function runPendingProvision(strapi, strapiAppDir) {
1047
1332
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1048
1333
  };
1049
1334
  const dp = donePath(strapiAppDir);
1050
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(dp), { recursive: true });
1051
- import_node_fs3.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
1335
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(dp), { recursive: true });
1336
+ import_node_fs4.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
1052
1337
  } catch (e) {
1053
1338
  result.errors.push(`resumo: ${e?.message ?? e}`);
1054
1339
  }
1055
1340
  try {
1056
- import_node_fs3.default.unlinkSync(mp);
1341
+ import_node_fs4.default.unlinkSync(mp);
1057
1342
  } catch {
1058
1343
  }
1059
1344
  return result;
1060
1345
  }
1061
1346
 
1062
1347
  // server/src/provision/infer.ts
1063
- var import_node_fs4 = __toESM(require("node:fs"));
1064
- var import_node_path4 = __toESM(require("node:path"));
1348
+ var import_node_fs5 = __toESM(require("node:fs"));
1349
+ var import_node_path5 = __toESM(require("node:path"));
1065
1350
  var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
1066
1351
  var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
1067
1352
  var SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -1094,38 +1379,53 @@ function score(rel) {
1094
1379
  function walk(dir, base, out) {
1095
1380
  let entries;
1096
1381
  try {
1097
- entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
1382
+ entries = import_node_fs5.default.readdirSync(dir, { withFileTypes: true });
1098
1383
  } catch {
1099
1384
  return;
1100
1385
  }
1101
1386
  for (const e of entries) {
1102
1387
  if (e.name.startsWith(".") && e.name !== ".") continue;
1103
- const full = import_node_path4.default.join(dir, e.name);
1388
+ const full = import_node_path5.default.join(dir, e.name);
1104
1389
  if (e.isDirectory()) {
1105
1390
  if (SKIP_DIRS.has(e.name)) continue;
1106
1391
  walk(full, base, out);
1107
- } else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
1108
- out.push(import_node_path4.default.relative(base, full));
1392
+ } else if (CODE_EXT.has(import_node_path5.default.extname(e.name))) {
1393
+ out.push(import_node_path5.default.relative(base, full));
1109
1394
  }
1110
1395
  }
1111
1396
  }
1397
+ function hasInlineDataArray(content) {
1398
+ return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
1399
+ }
1112
1400
  function collectFiles(frontendDir) {
1113
1401
  const all = [];
1114
1402
  walk(frontendDir, frontendDir, all);
1115
1403
  const tree = all.slice().sort();
1116
- const ranked = all.map((rel) => ({ rel, s: score(rel) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s);
1117
- const files = [];
1118
- let total = 0;
1119
- for (const { rel } of ranked) {
1120
- if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1404
+ const scored = [];
1405
+ for (const rel of all) {
1406
+ let content;
1121
1407
  try {
1122
- let content = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8");
1123
- if (!/export\s+(const|default|type|interface)/.test(content)) continue;
1124
- if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1125
- files.push({ rel, content });
1126
- total += content.length;
1408
+ content = import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8");
1127
1409
  } catch {
1410
+ continue;
1128
1411
  }
1412
+ const dataArray = hasInlineDataArray(content);
1413
+ const hasExport = /export\s+(const|default|type|interface)/.test(content);
1414
+ if (!hasExport && !dataArray) continue;
1415
+ let s = score(rel);
1416
+ if (dataArray) s += 8;
1417
+ if (s <= 0) continue;
1418
+ scored.push({ rel, content, s });
1419
+ }
1420
+ scored.sort((a, b) => b.s - a.s);
1421
+ const files = [];
1422
+ let total = 0;
1423
+ for (const it of scored) {
1424
+ if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1425
+ let content = it.content;
1426
+ if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1427
+ files.push({ rel: it.rel, content });
1428
+ total += content.length;
1129
1429
  }
1130
1430
  return { files, tree };
1131
1431
  }
@@ -1160,7 +1460,7 @@ function collectPageTexts(frontendDir) {
1160
1460
  for (const rel of ranked) {
1161
1461
  if (out.length >= MAX_TEXT_FILES) break;
1162
1462
  try {
1163
- const texts = extractTexts(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8"));
1463
+ const texts = extractTexts(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8"));
1164
1464
  if (texts.length) out.push({ rel, texts });
1165
1465
  } catch {
1166
1466
  }
@@ -1248,9 +1548,9 @@ function buildPageContentTypes(pageTexts, budget) {
1248
1548
  }
1249
1549
  return { contentTypes, seed };
1250
1550
  }
1251
- function detectFramework(frontendDir) {
1551
+ function detectFramework2(frontendDir) {
1252
1552
  try {
1253
- const pkg = JSON.parse(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, "package.json"), "utf8"));
1553
+ const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, "package.json"), "utf8"));
1254
1554
  const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1255
1555
  if (deps.next) return "next";
1256
1556
  if (deps["@tanstack/react-start"]) return "tanstack";
@@ -1294,13 +1594,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
1294
1594
 
1295
1595
  REGRAS:
1296
1596
  - Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
1597
+ - IMPORTANTE: muitos frontends guardam os dados em arrays declarados INLINE dentro de componentes (.tsx/.jsx), ex.: \`const services = [{ name, price, desc }]\`, \`const reviews = [...]\`, \`const hours = [...]\`. TRATE esses arrays como cole\xE7\xF5es e modele cada um como um collectionType, mesmo que estejam dentro de um componente de UI.
1598
+ - Ao modelar um array desses, inclua SOMENTE os campos que s\xE3o dados/texto (ex.: name, price, desc, label, href, value). IGNORE props que s\xE3o c\xF3digo/apresenta\xE7\xE3o: componentes de \xEDcone (ex.: \`icon: Scissors\`), elementos React, fun\xE7\xF5es, classes CSS, imports de imagem.
1297
1599
  - Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
1298
1600
  - Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
1299
1601
  - 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).
1300
1602
  - Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
1301
- - Em "seed", extraia o conte\xFAdo REAL hardcoded no c\xF3digo, omitindo campos de m\xEDdia e rela\xE7\xF5es.
1603
+ - Em "seed", copie o conte\xFAdo REAL hardcoded no c\xF3digo, VERBATIM (exatamente como est\xE1, sem reescrever, traduzir ou inventar), omitindo campos de m\xEDdia e rela\xE7\xF5es. Todo valor de seed TEM que existir literalmente no c\xF3digo fornecido.
1302
1604
  - Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
1303
- - N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1605
+ - N\xC3O invente NADA. Se n\xE3o tiver certeza de um valor, omita-o. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1304
1606
  - Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
1305
1607
  - Responda APENAS com o JSON, nada de markdown.
1306
1608
 
@@ -1311,7 +1613,7 @@ Arquivos de dados (cole\xE7\xF5es):
1311
1613
  ${filesBlock}`;
1312
1614
  }
1313
1615
  async function inferManifest(strapi, frontendDir, opts) {
1314
- const framework = detectFramework(frontendDir);
1616
+ const framework = detectFramework2(frontendDir);
1315
1617
  const result = {
1316
1618
  ok: false,
1317
1619
  inferred: true,
@@ -1320,10 +1622,10 @@ async function inferManifest(strapi, frontendDir, opts) {
1320
1622
  warnings: [],
1321
1623
  errors: []
1322
1624
  };
1323
- const existing = import_node_path4.default.join(frontendDir, "strapi.manifest.json");
1324
- if (import_node_fs4.default.existsSync(existing)) {
1625
+ const existing = import_node_path5.default.join(frontendDir, "strapi.manifest.json");
1626
+ if (import_node_fs5.default.existsSync(existing)) {
1325
1627
  try {
1326
- const raw = JSON.parse(import_node_fs4.default.readFileSync(existing, "utf8"));
1628
+ const raw = JSON.parse(import_node_fs5.default.readFileSync(existing, "utf8"));
1327
1629
  const v2 = validateManifest(raw);
1328
1630
  result.inferred = false;
1329
1631
  result.rawManifest = raw;
@@ -1406,6 +1708,48 @@ async function inferManifest(strapi, frontendDir, opts) {
1406
1708
  } else if (!apiKey) {
1407
1709
  result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
1408
1710
  }
1711
+ if (dataCts.length && dataSeed.length) {
1712
+ const parts = [];
1713
+ for (const f of collected.files) {
1714
+ try {
1715
+ parts.push(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, f.rel), "utf8"));
1716
+ } catch {
1717
+ parts.push(f.content);
1718
+ }
1719
+ }
1720
+ const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
1721
+ const hay = norm(parts.join("\n"));
1722
+ const present = (v2) => {
1723
+ if (typeof v2 !== "string") return false;
1724
+ const n = norm(v2);
1725
+ return n.length >= 4 && hay.includes(n);
1726
+ };
1727
+ const keep = /* @__PURE__ */ new Set();
1728
+ const verifiedSeed = [];
1729
+ let droppedEntries = 0;
1730
+ for (const grp of dataSeed) {
1731
+ const entries = (grp.entries ?? []).filter((e) => {
1732
+ const ok = Object.values(e).some(present);
1733
+ if (!ok) droppedEntries++;
1734
+ return ok;
1735
+ });
1736
+ if (entries.length) {
1737
+ verifiedSeed.push({ ...grp, entries });
1738
+ keep.add(grp.singularName);
1739
+ }
1740
+ }
1741
+ const verifiedCts = dataCts.filter(
1742
+ (ct) => ct.kind === "singleType" || keep.has(ct.singularName)
1743
+ );
1744
+ const droppedCts = dataCts.length - verifiedCts.length;
1745
+ if (droppedEntries || droppedCts) {
1746
+ result.warnings.push(
1747
+ `Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
1748
+ );
1749
+ }
1750
+ dataCts = verifiedCts;
1751
+ dataSeed = verifiedSeed;
1752
+ }
1409
1753
  const budget = 60 - dataCts.length - 1;
1410
1754
  const page = buildPageContentTypes(pageTexts, budget);
1411
1755
  const finalManifest = {
@@ -1434,132 +1778,6 @@ async function inferManifest(strapi, frontendDir, opts) {
1434
1778
  return result;
1435
1779
  }
1436
1780
 
1437
- // server/src/provision/runner.ts
1438
- var import_node_child_process = require("node:child_process");
1439
- var import_node_net = __toESM(require("node:net"));
1440
- var import_node_fs5 = __toESM(require("node:fs"));
1441
- var import_node_path5 = __toESM(require("node:path"));
1442
- var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
1443
- var child = null;
1444
- var pollTimer = null;
1445
- function detectPM(dir) {
1446
- 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";
1447
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
1448
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
1449
- return "npm";
1450
- }
1451
- var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
1452
- function detectFramework2(dir) {
1453
- if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
1454
- if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
1455
- return "other";
1456
- }
1457
- var FRONTEND_BASE_PORT = 4321;
1458
- function findFreePort(start) {
1459
- return new Promise((resolve) => {
1460
- const tryPort = (p) => {
1461
- if (p > start + 200) return resolve(start);
1462
- const srv = import_node_net.default.createServer();
1463
- srv.once("error", () => tryPort(p + 1));
1464
- srv.once("listening", () => srv.close(() => resolve(p)));
1465
- srv.listen(p, "0.0.0.0");
1466
- };
1467
- tryPort(start);
1468
- });
1469
- }
1470
- function pushLog(s) {
1471
- for (const line of String(s).split("\n")) {
1472
- const t = line.trim();
1473
- if (t) info.log.push(t);
1474
- }
1475
- if (info.log.length > 60) info.log = info.log.slice(-60);
1476
- }
1477
- async function urlUp(url) {
1478
- try {
1479
- const res = await fetch(url, { method: "GET" });
1480
- return res.status >= 200 && res.status < 400;
1481
- } catch {
1482
- return false;
1483
- }
1484
- }
1485
- function getRunStatus() {
1486
- return { ...info, log: info.log.slice(-15) };
1487
- }
1488
- function stopFrontend() {
1489
- if (pollTimer) {
1490
- clearInterval(pollTimer);
1491
- pollTimer = null;
1492
- }
1493
- if (child) {
1494
- try {
1495
- child.kill("SIGTERM");
1496
- } catch {
1497
- }
1498
- child = null;
1499
- }
1500
- if (info.state !== "error") info.state = "idle";
1501
- }
1502
- async function startFrontend(_strapi, opts) {
1503
- const { dir } = opts;
1504
- if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
1505
- return getRunStatus();
1506
- }
1507
- stopFrontend();
1508
- const pm = detectPM(dir);
1509
- const framework = detectFramework2(dir);
1510
- const port = await findFreePort(FRONTEND_BASE_PORT);
1511
- const url = `http://127.0.0.1:${port}`;
1512
- info = { state: "installing", dir, url, pm, error: null, log: [] };
1513
- const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
1514
- 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)];
1515
- const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
1516
- const startDev = () => {
1517
- info.state = "starting";
1518
- child = spawnIn(pm, devArgs);
1519
- child.stdout?.on("data", (d) => pushLog(d));
1520
- child.stderr?.on("data", (d) => pushLog(d));
1521
- child.on("exit", (code) => {
1522
- child = null;
1523
- if (pollTimer) {
1524
- clearInterval(pollTimer);
1525
- pollTimer = null;
1526
- }
1527
- if (info.state === "running") info.state = "idle";
1528
- else {
1529
- info.state = "error";
1530
- info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
1531
- }
1532
- });
1533
- pollTimer = setInterval(async () => {
1534
- if (await urlUp(url)) {
1535
- info.state = "running";
1536
- if (pollTimer) {
1537
- clearInterval(pollTimer);
1538
- pollTimer = null;
1539
- }
1540
- }
1541
- }, 1500);
1542
- };
1543
- const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
1544
- if (needInstall) {
1545
- pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
1546
- const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
1547
- const inst = spawnIn(pm, installArgs);
1548
- inst.stdout?.on("data", (d) => pushLog(d));
1549
- inst.stderr?.on("data", (d) => pushLog(d));
1550
- inst.on("exit", (code) => {
1551
- if (code === 0) startDev();
1552
- else {
1553
- info.state = "error";
1554
- info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
1555
- }
1556
- });
1557
- } else {
1558
- startDev();
1559
- }
1560
- return getRunStatus();
1561
- }
1562
-
1563
1781
  // server/src/provision/integrate.ts
1564
1782
  var import_node_fs6 = __toESM(require("node:fs"));
1565
1783
  var import_node_path6 = __toESM(require("node:path"));
@@ -2627,9 +2845,10 @@ function createContentTools(strapi) {
2627
2845
  }
2628
2846
  };
2629
2847
  const MAX_MATCHES = 100;
2630
- const buscarTexto = async (termo) => {
2848
+ const buscarTexto = async (termo, status = "draft") => {
2631
2849
  const needle = String(termo || "").toLowerCase().trim();
2632
2850
  if (!needle) return { erro: "termo vazio" };
2851
+ const st = status === "published" ? "published" : "draft";
2633
2852
  const matches = [];
2634
2853
  for (const ct of apiContentTypes()) {
2635
2854
  if (matches.length >= MAX_MATCHES) break;
@@ -2637,7 +2856,7 @@ function createContentTools(strapi) {
2637
2856
  const populate = buildPopulate(attributes);
2638
2857
  let entries = [];
2639
2858
  try {
2640
- const res = await strapi.documents(ct.uid).findMany({ status: "draft", populate, limit: 200 });
2859
+ const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
2641
2860
  entries = Array.isArray(res) ? res : res ? [res] : [];
2642
2861
  } catch {
2643
2862
  continue;
@@ -3152,7 +3371,7 @@ If the user shares their screen, an image is attached to the last message \u2014
3152
3371
  Be concise and actionable. ALWAYS answer in English.`
3153
3372
  };
3154
3373
  var chat_default2 = ({ strapi }) => ({
3155
- async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
3374
+ async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
3156
3375
  const apiKey = process.env.OPENAI_API_KEY;
3157
3376
  if (!apiKey) {
3158
3377
  throw new Error(
@@ -3162,7 +3381,7 @@ var chat_default2 = ({ strapi }) => ({
3162
3381
  const language = lang === "en" ? "en" : "pt";
3163
3382
  const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
3164
3383
  const LOCAL_TOOLS = {
3165
- buscar_texto: (a) => buscarTexto(a?.termo),
3384
+ buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
3166
3385
  editar_campo: (a) => editarCampo(a),
3167
3386
  publicar: (a) => publicar(a),
3168
3387
  listar_locales: () => listarLocales(),
@@ -3640,6 +3859,10 @@ var register_default = ({ strapi }) => {
3640
3859
  var index_default = {
3641
3860
  register: register_default,
3642
3861
  async bootstrap({ strapi }) {
3862
+ try {
3863
+ cleanupStaleFrontend(strapi.dirs.app.root);
3864
+ } catch {
3865
+ }
3643
3866
  try {
3644
3867
  const r = await runPendingProvision(strapi, strapi.dirs.app.root);
3645
3868
  if (r.ran) {