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.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/admin/src/components/AdminOverlays.tsx +190 -0
- package/admin/src/components/FloatingChat.tsx +370 -0
- package/admin/src/components/PreviewPanel.tsx +188 -0
- package/admin/src/index.tsx +49 -0
- package/admin/src/pages/App.tsx +14 -0
- package/admin/src/pages/HomePage.tsx +333 -0
- package/admin/src/pages/ProvisionPage.tsx +391 -0
- package/admin/src/pluginId.ts +1 -0
- package/dist/server/index.js +3511 -0
- package/package.json +77 -0
- package/server/src/content-tools.ts +520 -0
- package/server/src/controllers/audio.ts +45 -0
- package/server/src/controllers/chat.ts +22 -0
- package/server/src/controllers/frontend.ts +310 -0
- package/server/src/index.ts +43 -0
- package/server/src/mcp/index.ts +24 -0
- package/server/src/mcp/tools/buscar-texto.ts +28 -0
- package/server/src/mcp/tools/criar-locale.ts +30 -0
- package/server/src/mcp/tools/editar-campo.ts +39 -0
- package/server/src/mcp/tools/habilitar-i18n.ts +33 -0
- package/server/src/mcp/tools/index.ts +17 -0
- package/server/src/mcp/tools/listar-locales.ts +27 -0
- package/server/src/mcp/tools/publicar.ts +31 -0
- package/server/src/mcp/tools/traduzir.ts +36 -0
- package/server/src/mcp/types.ts +11 -0
- package/server/src/mcp-client.ts +96 -0
- package/server/src/provision/adapters.ts +91 -0
- package/server/src/provision/enable-i18n.ts +129 -0
- package/server/src/provision/generate.ts +216 -0
- package/server/src/provision/infer.ts +495 -0
- package/server/src/provision/integrate.ts +963 -0
- package/server/src/provision/link.ts +203 -0
- package/server/src/provision/manifest.ts +281 -0
- package/server/src/provision/orchestrate.ts +236 -0
- package/server/src/provision/permissions.ts +58 -0
- package/server/src/provision/runner.ts +176 -0
- package/server/src/provision/seed.ts +115 -0
- package/server/src/provision/translate.ts +153 -0
- package/server/src/provision/types-gen.ts +117 -0
- package/server/src/provision/write.ts +136 -0
- package/server/src/register.ts +17 -0
- package/server/src/routes/index.ts +66 -0
- package/server/src/services/audio.ts +53 -0
- package/server/src/services/chat.ts +263 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Flex, Typography, Button, Loader, Textarea } from '@strapi/design-system';
|
|
3
|
+
import { useFetchClient } from '@strapi/strapi/admin';
|
|
4
|
+
import { Link } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provisão de frontend em duas etapas (cenário Figma/Lovable, sem manifest):
|
|
8
|
+
* 1. Analisar: sobe o .zip → o plugin extrai e, se não houver manifest, a IA o
|
|
9
|
+
* infere a partir do código. A UI mostra o manifest proposto para revisão.
|
|
10
|
+
* 2. Provisionar: o usuário confirma (podendo editar o JSON) → cria as
|
|
11
|
+
* content-types, semeia, libera leitura e liga o preview (a Strapi reinicia).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Phase = 'idle' | 'analyzing' | 'review' | 'provisioning' | 'ready' | 'done-noreload' | 'error';
|
|
15
|
+
|
|
16
|
+
interface AnalyzeResp {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
frontendDir: string;
|
|
19
|
+
inferred: boolean;
|
|
20
|
+
framework: string;
|
|
21
|
+
filesAnalyzed: string[];
|
|
22
|
+
manifest: any;
|
|
23
|
+
errors: string[];
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProvisionDone {
|
|
28
|
+
name: string;
|
|
29
|
+
framework: string;
|
|
30
|
+
frontendDir: string;
|
|
31
|
+
contentTypes: string[];
|
|
32
|
+
previewUrl: string;
|
|
33
|
+
seedCreated: { uid: string; count: number }[];
|
|
34
|
+
linkErrors: string[];
|
|
35
|
+
finishedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const POLL_MS = 2000;
|
|
39
|
+
const POLL_TIMEOUT_MS = 120000;
|
|
40
|
+
|
|
41
|
+
const ProvisionPage = () => {
|
|
42
|
+
const { post, get } = useFetchClient();
|
|
43
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
44
|
+
|
|
45
|
+
const [file, setFile] = useState<File | null>(null);
|
|
46
|
+
const [phase, setPhase] = useState<Phase>('idle');
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// resultado da análise / revisão
|
|
50
|
+
const [frontendDir, setFrontendDir] = useState('');
|
|
51
|
+
const [manifestText, setManifestText] = useState('');
|
|
52
|
+
const [inferred, setInferred] = useState(false);
|
|
53
|
+
const [framework, setFramework] = useState('');
|
|
54
|
+
const [filesAnalyzed, setFilesAnalyzed] = useState<string[]>([]);
|
|
55
|
+
|
|
56
|
+
const [done, setDone] = useState<ProvisionDone | null>(null);
|
|
57
|
+
const [noReloadMsg, setNoReloadMsg] = useState('');
|
|
58
|
+
|
|
59
|
+
// religamento (snapshot) do frontend ao Strapi
|
|
60
|
+
const [integrating, setIntegrating] = useState(false);
|
|
61
|
+
const [integrateMsg, setIntegrateMsg] = useState('');
|
|
62
|
+
|
|
63
|
+
const stopRef = useRef(false);
|
|
64
|
+
useEffect(() => () => { stopRef.current = true; }, []);
|
|
65
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
66
|
+
|
|
67
|
+
const errDetail = (e: any, fallback: string) => {
|
|
68
|
+
const body = e?.response?.data;
|
|
69
|
+
const base =
|
|
70
|
+
body?.error?.message ||
|
|
71
|
+
body?.message ||
|
|
72
|
+
(Array.isArray(body?.errors) ? body.errors.join('; ') : null) ||
|
|
73
|
+
e?.message ||
|
|
74
|
+
fallback;
|
|
75
|
+
const extra =
|
|
76
|
+
body?.errors && Array.isArray(body.errors) && body?.message
|
|
77
|
+
? `\n• ${body.errors.join('\n• ')}`
|
|
78
|
+
: '';
|
|
79
|
+
return `${base}${extra}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ── Etapa 1: analisar ──────────────────────────────────────────────────────
|
|
83
|
+
const analyze = async () => {
|
|
84
|
+
if (!file || phase === 'analyzing') return;
|
|
85
|
+
setError(null);
|
|
86
|
+
setDone(null);
|
|
87
|
+
setPhase('analyzing');
|
|
88
|
+
try {
|
|
89
|
+
const form = new FormData();
|
|
90
|
+
form.append('frontend', file, file.name);
|
|
91
|
+
const { data } = await post('/mcp-chat/frontend/analyze', form);
|
|
92
|
+
const res = data as AnalyzeResp;
|
|
93
|
+
setFrontendDir(res.frontendDir);
|
|
94
|
+
setInferred(res.inferred);
|
|
95
|
+
setFramework(res.framework);
|
|
96
|
+
setFilesAnalyzed(res.filesAnalyzed || []);
|
|
97
|
+
setManifestText(res.manifest ? JSON.stringify(res.manifest, null, 2) : '');
|
|
98
|
+
if (!res.ok && res.errors?.length) {
|
|
99
|
+
setError(`Aviso da análise:\n• ${res.errors.join('\n• ')}`);
|
|
100
|
+
}
|
|
101
|
+
setPhase('review');
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
setError(errDetail(e, 'Falha ao analisar o projeto.'));
|
|
104
|
+
setPhase('error');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ── Etapa 2: provisionar ───────────────────────────────────────────────────
|
|
109
|
+
const provision = async () => {
|
|
110
|
+
let manifest: any;
|
|
111
|
+
try {
|
|
112
|
+
manifest = JSON.parse(manifestText);
|
|
113
|
+
} catch {
|
|
114
|
+
setError('O manifest não é um JSON válido. Corrija a sintaxe.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setError(null);
|
|
118
|
+
setPhase('provisioning');
|
|
119
|
+
try {
|
|
120
|
+
const { data } = await post('/mcp-chat/frontend/provision', { frontendDir, manifest });
|
|
121
|
+
if (data.willReload) {
|
|
122
|
+
stopRef.current = false;
|
|
123
|
+
void pollUntilReady();
|
|
124
|
+
} else {
|
|
125
|
+
setNoReloadMsg(data.message);
|
|
126
|
+
setPhase('done-noreload');
|
|
127
|
+
}
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
setError(errDetail(e, 'Falha na provisão.'));
|
|
130
|
+
setPhase('error');
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const pollUntilReady = async () => {
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
while (!stopRef.current) {
|
|
137
|
+
if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
|
|
138
|
+
setError('A provisão demorou mais que o esperado. Verifique o terminal da Strapi.');
|
|
139
|
+
setPhase('error');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await sleep(POLL_MS);
|
|
143
|
+
try {
|
|
144
|
+
const { data } = await get('/mcp-chat/frontend/status');
|
|
145
|
+
if (data && data.pending === false && data.done) {
|
|
146
|
+
setDone(data.done as ProvisionDone);
|
|
147
|
+
setPhase('ready');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// servidor reiniciando: continua tentando.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const integrate = async () => {
|
|
157
|
+
if (integrating) return;
|
|
158
|
+
setIntegrating(true);
|
|
159
|
+
setIntegrateMsg('');
|
|
160
|
+
try {
|
|
161
|
+
const { data } = await post('/mcp-chat/frontend/integrate', {});
|
|
162
|
+
if (data.ok) {
|
|
163
|
+
setIntegrateMsg(
|
|
164
|
+
`✅ Religado! Arquivos atualizados: ${data.filesRewritten.join(', ')}. ` +
|
|
165
|
+
`Recarregue o preview para ver os dados do Strapi. (Original salvo como .bak.)`
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
setIntegrateMsg(
|
|
169
|
+
`⚠️ Não consegui religar: ${(data.errors || []).join('; ') || 'sem arquivo de dados'}.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} catch (e: any) {
|
|
173
|
+
setIntegrateMsg(`⚠️ ${errDetail(e, 'Falha ao religar.')}`);
|
|
174
|
+
} finally {
|
|
175
|
+
setIntegrating(false);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const reset = () => {
|
|
180
|
+
setFile(null);
|
|
181
|
+
setError(null);
|
|
182
|
+
setDone(null);
|
|
183
|
+
setManifestText('');
|
|
184
|
+
setFrontendDir('');
|
|
185
|
+
setFilesAnalyzed([]);
|
|
186
|
+
setIntegrateMsg('');
|
|
187
|
+
setPhase('idle');
|
|
188
|
+
if (inputRef.current) inputRef.current.value = '';
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const busy = phase === 'analyzing' || phase === 'provisioning';
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Box padding={6} background="neutral100" style={{ minHeight: '100vh' }}>
|
|
195
|
+
<Flex justifyContent="space-between" alignItems="center" paddingBottom={4}>
|
|
196
|
+
<Box>
|
|
197
|
+
<Typography variant="alpha" tag="h1">Provisionar frontend</Typography>
|
|
198
|
+
<Typography variant="pi" textColor="neutral600">
|
|
199
|
+
Suba o .zip do seu frontend (Figma/Lovable, Next ou TanStack) — a IA infere o
|
|
200
|
+
modelo de conteúdo, você revisa, e o plugin cria tudo no Strapi.
|
|
201
|
+
</Typography>
|
|
202
|
+
</Box>
|
|
203
|
+
<Link to="..">
|
|
204
|
+
<Button variant="tertiary">← Voltar ao chat</Button>
|
|
205
|
+
</Link>
|
|
206
|
+
</Flex>
|
|
207
|
+
|
|
208
|
+
<Box
|
|
209
|
+
background="neutral0"
|
|
210
|
+
hasRadius
|
|
211
|
+
shadow="tableShadow"
|
|
212
|
+
padding={6}
|
|
213
|
+
style={{ maxWidth: 820, margin: '0 auto' }}
|
|
214
|
+
>
|
|
215
|
+
<Flex direction="column" alignItems="stretch" gap={4}>
|
|
216
|
+
{/* Seleção do arquivo */}
|
|
217
|
+
<Typography variant="delta" tag="h2">1. Escolha o .zip do frontend</Typography>
|
|
218
|
+
<Typography textColor="neutral600">
|
|
219
|
+
Não precisa de <code>strapi.manifest.json</code>: se ele não existir, a IA cria um
|
|
220
|
+
analisando os dados do código (ex.: <code>src/data/*.ts</code>).
|
|
221
|
+
</Typography>
|
|
222
|
+
|
|
223
|
+
<input
|
|
224
|
+
ref={inputRef}
|
|
225
|
+
type="file"
|
|
226
|
+
accept=".zip,application/zip"
|
|
227
|
+
style={{ display: 'none' }}
|
|
228
|
+
onChange={(e) => {
|
|
229
|
+
setError(null);
|
|
230
|
+
setDone(null);
|
|
231
|
+
setPhase('idle');
|
|
232
|
+
setManifestText('');
|
|
233
|
+
setFile(e.target.files?.[0] ?? null);
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
<Flex gap={2} alignItems="center">
|
|
238
|
+
<Button variant="secondary" onClick={() => inputRef.current?.click()} disabled={busy}>
|
|
239
|
+
Selecionar arquivo…
|
|
240
|
+
</Button>
|
|
241
|
+
<Typography textColor={file ? 'neutral800' : 'neutral500'}>
|
|
242
|
+
{file ? file.name : 'Nenhum arquivo selecionado'}
|
|
243
|
+
</Typography>
|
|
244
|
+
</Flex>
|
|
245
|
+
|
|
246
|
+
{(phase === 'idle' || phase === 'analyzing') && (
|
|
247
|
+
<Box paddingTop={2}>
|
|
248
|
+
<Button onClick={analyze} loading={phase === 'analyzing'} disabled={!file || busy}>
|
|
249
|
+
Analisar projeto
|
|
250
|
+
</Button>
|
|
251
|
+
</Box>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{phase === 'analyzing' && (
|
|
255
|
+
<Flex gap={3} alignItems="center" background="primary100" padding={4} hasRadius>
|
|
256
|
+
<Loader small>Analisando…</Loader>
|
|
257
|
+
<Typography textColor="primary700">
|
|
258
|
+
Lendo o código e inferindo o modelo de conteúdo (content-types + seed)…
|
|
259
|
+
</Typography>
|
|
260
|
+
</Flex>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Etapa 2: revisão do manifest */}
|
|
264
|
+
{phase === 'review' && (
|
|
265
|
+
<>
|
|
266
|
+
<Box height="1px" background="neutral200" />
|
|
267
|
+
<Typography variant="delta" tag="h2">2. Revise o modelo de conteúdo</Typography>
|
|
268
|
+
<Flex gap={2} alignItems="center" wrap="wrap">
|
|
269
|
+
<Box background={inferred ? 'warning100' : 'success100'} padding={2} hasRadius>
|
|
270
|
+
<Typography variant="pi" textColor={inferred ? 'warning700' : 'success700'}>
|
|
271
|
+
{inferred ? '🤖 Inferido pela IA' : '✓ Manifest do projeto'} • framework: {framework}
|
|
272
|
+
</Typography>
|
|
273
|
+
</Box>
|
|
274
|
+
{filesAnalyzed.length > 0 && (
|
|
275
|
+
<Typography variant="pi" textColor="neutral600">
|
|
276
|
+
Analisou: {filesAnalyzed.slice(0, 6).join(', ')}
|
|
277
|
+
{filesAnalyzed.length > 6 ? ` +${filesAnalyzed.length - 6}` : ''}
|
|
278
|
+
</Typography>
|
|
279
|
+
)}
|
|
280
|
+
</Flex>
|
|
281
|
+
<Typography variant="pi" textColor="neutral600">
|
|
282
|
+
Edite o JSON se quiser ajustar nomes, tipos ou o conteúdo semeado antes de criar.
|
|
283
|
+
</Typography>
|
|
284
|
+
<Textarea
|
|
285
|
+
name="manifest"
|
|
286
|
+
value={manifestText}
|
|
287
|
+
onChange={(e: any) => setManifestText(e.target.value)}
|
|
288
|
+
style={{ fontFamily: 'monospace', fontSize: 12, minHeight: 320 }}
|
|
289
|
+
/>
|
|
290
|
+
<Flex gap={2}>
|
|
291
|
+
<Button onClick={provision} disabled={!manifestText.trim()}>
|
|
292
|
+
Provisionar
|
|
293
|
+
</Button>
|
|
294
|
+
<Button variant="tertiary" onClick={reset}>Recomeçar</Button>
|
|
295
|
+
</Flex>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{/* Provisionando */}
|
|
300
|
+
{phase === 'provisioning' && (
|
|
301
|
+
<Flex direction="column" gap={2} background="primary100" padding={4} hasRadius>
|
|
302
|
+
<Flex gap={3} alignItems="center">
|
|
303
|
+
<Loader small>Provisionando…</Loader>
|
|
304
|
+
<Typography fontWeight="bold" textColor="primary700">
|
|
305
|
+
Configurando tudo — isso leva alguns segundos
|
|
306
|
+
</Typography>
|
|
307
|
+
</Flex>
|
|
308
|
+
<Typography variant="pi" textColor="neutral700">
|
|
309
|
+
A Strapi está reiniciando para reconhecer as content-types, depois semeia o
|
|
310
|
+
conteúdo, libera leitura pública e liga o preview. Não feche esta página.
|
|
311
|
+
</Typography>
|
|
312
|
+
</Flex>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{phase === 'done-noreload' && (
|
|
316
|
+
<Box background="success100" padding={4} hasRadius>
|
|
317
|
+
<Typography textColor="success700">{noReloadMsg}</Typography>
|
|
318
|
+
</Box>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Pronto */}
|
|
322
|
+
{phase === 'ready' && done && (
|
|
323
|
+
<Box background="success100" padding={4} hasRadius>
|
|
324
|
+
<Typography variant="beta" textColor="success700" tag="div">
|
|
325
|
+
✅ Tudo pronto! Você já pode ver o preview.
|
|
326
|
+
</Typography>
|
|
327
|
+
<Box paddingTop={3}>
|
|
328
|
+
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
329
|
+
Content-types criadas: {done.contentTypes.join(', ')}
|
|
330
|
+
</Typography>
|
|
331
|
+
{done.seedCreated.length > 0 && (
|
|
332
|
+
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
333
|
+
Conteúdo semeado: {done.seedCreated.map((s) => `${s.uid} (${s.count})`).join(', ')}
|
|
334
|
+
</Typography>
|
|
335
|
+
)}
|
|
336
|
+
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
337
|
+
Frontend em: <code>{done.frontendDir}</code>
|
|
338
|
+
</Typography>
|
|
339
|
+
</Box>
|
|
340
|
+
<Box paddingTop={3}>
|
|
341
|
+
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
342
|
+
Para ver o preview, rode o frontend (uma vez):
|
|
343
|
+
</Typography>
|
|
344
|
+
<Box background="neutral0" padding={2} hasRadius marginTop={1}
|
|
345
|
+
style={{ fontFamily: 'monospace', fontSize: 12 }}>
|
|
346
|
+
cd {done.frontendDir} && npm install && npm run dev
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
<Box paddingTop={3}>
|
|
350
|
+
<Typography variant="pi" textColor="neutral700" tag="div">
|
|
351
|
+
Religar o frontend ao Strapi (snapshot): troca os dados hardcoded pelos do
|
|
352
|
+
Strapi, mantendo as imagens. Os componentes não mudam.
|
|
353
|
+
</Typography>
|
|
354
|
+
<Box paddingTop={1}>
|
|
355
|
+
<Button onClick={integrate} loading={integrating} variant="default">
|
|
356
|
+
Religar dados ao Strapi
|
|
357
|
+
</Button>
|
|
358
|
+
</Box>
|
|
359
|
+
{integrateMsg && (
|
|
360
|
+
<Box paddingTop={2}>
|
|
361
|
+
<Typography variant="pi" textColor="neutral800" style={{ whiteSpace: 'pre-wrap' }}>
|
|
362
|
+
{integrateMsg}
|
|
363
|
+
</Typography>
|
|
364
|
+
</Box>
|
|
365
|
+
)}
|
|
366
|
+
</Box>
|
|
367
|
+
|
|
368
|
+
<Flex gap={2} paddingTop={3}>
|
|
369
|
+
<a href={done.previewUrl} target="_blank" rel="noreferrer">
|
|
370
|
+
<Button variant="success">Abrir {done.previewUrl} ↗</Button>
|
|
371
|
+
</a>
|
|
372
|
+
<Button variant="tertiary" onClick={reset}>Provisionar outro</Button>
|
|
373
|
+
</Flex>
|
|
374
|
+
</Box>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{error && (
|
|
378
|
+
<Box background={phase === 'error' ? 'danger100' : 'warning100'} padding={3} hasRadius>
|
|
379
|
+
<Typography textColor={phase === 'error' ? 'danger600' : 'warning700'}
|
|
380
|
+
style={{ whiteSpace: 'pre-wrap' }}>
|
|
381
|
+
{error}
|
|
382
|
+
</Typography>
|
|
383
|
+
</Box>
|
|
384
|
+
)}
|
|
385
|
+
</Flex>
|
|
386
|
+
</Box>
|
|
387
|
+
</Box>
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export { ProvisionPage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLUGIN_ID = 'mcp-chat';
|