@kiubi/mcp-designer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +219 -0
- package/package.json +57 -0
- package/src/app.ts +39 -0
- package/src/index.ts +21 -0
- package/src/lib/stemmer.ts +38 -0
- package/src/lib/theme-checker/block-validator.ts +353 -0
- package/src/lib/theme-checker/checker.ts +897 -0
- package/src/lib/theme-checker/filter-validator.ts +163 -0
- package/src/lib/theme-checker/index.ts +6 -0
- package/src/lib/theme-checker/inspector.ts +193 -0
- package/src/lib/theme-checker/lang.ts +42 -0
- package/src/lib/theme-checker/parsers/component-xml.ts +120 -0
- package/src/lib/theme-checker/parsers/model-xml.ts +82 -0
- package/src/lib/theme-checker/parsers/post-type-xml.ts +92 -0
- package/src/lib/theme-checker/parsers/symbol-xml.ts +162 -0
- package/src/lib/theme-checker/parsers/theme-xml.ts +102 -0
- package/src/lib/theme-checker/types.ts +97 -0
- package/src/logger.ts +28 -0
- package/src/tool.ts +13 -0
- package/src/tools/create_type.ts +535 -0
- package/src/tools/detail_type.ts +208 -0
- package/src/tools/get_doc_section.ts +201 -0
- package/src/tools/list_types.ts +87 -0
- package/src/tools/list_widgets.ts +181 -0
- package/src/tools/ruleset.ts +25 -0
- package/src/tools/search_doc.ts +160 -0
- package/src/tools/validate_theme.ts +63 -0
- package/storage/definitions/blog.json +1444 -0
- package/storage/definitions/catalogue.json +2817 -0
- package/storage/definitions/cms.json +1002 -0
- package/storage/definitions/commandes.json +2409 -0
- package/storage/definitions/comptes.json +780 -0
- package/storage/definitions/modules.json +418 -0
- package/storage/definitions/recherche.json +804 -0
- package/storage/documents/CLAUDE.exemple.md +281 -0
- package/storage/documents/generated/exemples.md +8054 -0
- package/storage/documents/generated/integration/01_theme_perso.md +182 -0
- package/storage/documents/generated/integration/02_modeles.md +220 -0
- package/storage/documents/generated/integration/03_widgets.md +139 -0
- package/storage/documents/generated/integration/04_blocs.md +45 -0
- package/storage/documents/generated/integration/05_balises.md +43 -0
- package/storage/documents/generated/integration/06_balises_globales.md +201 -0
- package/storage/documents/generated/integration/07_balises_widget.md +257 -0
- package/storage/documents/generated/integration/08_types_billets.md +154 -0
- package/storage/documents/generated/integration/09_types_produits.md +77 -0
- package/storage/documents/generated/integration/10_types_composants.md +142 -0
- package/storage/documents/generated/integration/11_symboles.md +156 -0
- package/storage/documents/generated/integration/12_mediatheque.md +66 -0
- package/storage/documents/generated/integration/13_includes.md +25 -0
- package/storage/documents/generated/integration/14_404.md +32 -0
- package/storage/documents/generated/integration/15_arianne.md +62 -0
- package/storage/documents/generated/integration/16_fermeture.md +36 -0
- package/storage/documents/generated/integration/17_recherche.md +36 -0
- package/storage/documents/generated/integration/18_cms.md +582 -0
- package/storage/documents/generated/integration/19_modules.md +479 -0
- package/storage/documents/generated/integration/20_blog.md +946 -0
- package/storage/documents/generated/integration/21_catalogue.md +1834 -0
- package/storage/documents/generated/integration/22_commandes.md +2772 -0
- package/storage/documents/generated/integration/23_comptes.md +1195 -0
- package/storage/documents/generated/integration/24_recherche.md +542 -0
- package/storage/documents/hors_sujet_regles.md +43 -0
- package/storage/documents/kiubi_api.md +6690 -0
- package/storage/documents/kiubi_ruleset.md +419 -0
- package/storage/documents/kiubi_workflows.md +121 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineTool } from "../tool.ts";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const RULES_PATH = new URL("../../storage/documents/kiubi_ruleset.md", import.meta.url).pathname;
|
|
5
|
+
|
|
6
|
+
export function integrationRules(): string {
|
|
7
|
+
if (!existsSync(RULES_PATH)) {
|
|
8
|
+
return "Fichier de règles d'intégration introuvable.";
|
|
9
|
+
}
|
|
10
|
+
return readFileSync(RULES_PATH, "utf-8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const integrationRulesTool = defineTool({
|
|
14
|
+
name: "ruleset",
|
|
15
|
+
description:
|
|
16
|
+
"Retourne les règles d'intégration du moteur de template Kiubi. " +
|
|
17
|
+
"Contient la syntaxe des balises, les filtres disponibles et leurs contraintes d'utilisation. " +
|
|
18
|
+
"À consulter pour valider ou générer des templates conformes.",
|
|
19
|
+
inputSchema: {},
|
|
20
|
+
handler: async () => {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text" as const, text: integrationRules() }],
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "../tool.ts";
|
|
3
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { normalize, stem } from "#lib/stemmer";
|
|
6
|
+
|
|
7
|
+
const DOCS_BASE = new URL("../../storage/documents", import.meta.url).pathname;
|
|
8
|
+
const INTEGRATION_DIR = join(DOCS_BASE, "generated/integration");
|
|
9
|
+
const REGLES_PATH = join(DOCS_BASE, "kiubi_ruleset.md");
|
|
10
|
+
const EXEMPLES_PATH = join(DOCS_BASE, "generated/exemples.md");
|
|
11
|
+
const API_PATH = join(DOCS_BASE, "kiubi_api.md");
|
|
12
|
+
|
|
13
|
+
const TITLE_WEIGHT = 5;
|
|
14
|
+
const MIN_TOKEN_LENGTH = 3;
|
|
15
|
+
|
|
16
|
+
type SectionEntry = {
|
|
17
|
+
category: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function tokenize(text: string): string[] {
|
|
23
|
+
return normalize(text)
|
|
24
|
+
.split(/[^a-z0-9]+/)
|
|
25
|
+
.filter((w) => w.length >= MIN_TOKEN_LENGTH)
|
|
26
|
+
.map(stem);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseSections(content: string, category: string): SectionEntry[] {
|
|
30
|
+
return content
|
|
31
|
+
.split(/(?=^## )/m)
|
|
32
|
+
.filter((s) => s.trim().length > 0)
|
|
33
|
+
.map((s) => {
|
|
34
|
+
const title = s.split("\n")[0].replace(/^## /, "").trim();
|
|
35
|
+
return { category, title, content: s.trimEnd() };
|
|
36
|
+
})
|
|
37
|
+
.filter((s) => s.title.length > 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildIndex(): SectionEntry[] {
|
|
41
|
+
const entries: SectionEntry[] = [];
|
|
42
|
+
|
|
43
|
+
if (existsSync(INTEGRATION_DIR)) {
|
|
44
|
+
for (const file of readdirSync(INTEGRATION_DIR).filter((f) => f.endsWith(".md")).sort()) {
|
|
45
|
+
const content = readFileSync(join(INTEGRATION_DIR, file), "utf-8");
|
|
46
|
+
entries.push(...parseSections(content, "integration"));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (existsSync(REGLES_PATH)) {
|
|
51
|
+
entries.push(...parseSections(readFileSync(REGLES_PATH, "utf-8"), "regles"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (existsSync(EXEMPLES_PATH)) {
|
|
55
|
+
entries.push(...parseSections(readFileSync(EXEMPLES_PATH, "utf-8"), "exemples"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (existsSync(API_PATH)) {
|
|
59
|
+
entries.push(...parseSections(readFileSync(API_PATH, "utf-8"), "api"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function scoreSection(entry: SectionEntry, queryTokens: string[]): number {
|
|
66
|
+
const titleTokens = new Set(tokenize(entry.title));
|
|
67
|
+
const bodyTokens = tokenize(entry.content);
|
|
68
|
+
|
|
69
|
+
let score = 0;
|
|
70
|
+
for (const qt of queryTokens) {
|
|
71
|
+
if (titleTokens.has(qt)) score += TITLE_WEIGHT;
|
|
72
|
+
score += bodyTokens.filter((t) => t === qt).length;
|
|
73
|
+
}
|
|
74
|
+
return score;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function truncateAtLine(content: string, limit: number): { text: string; truncated: boolean } {
|
|
78
|
+
if (content.length <= limit) return { text: content, truncated: false };
|
|
79
|
+
const cut = content.lastIndexOf("\n", limit);
|
|
80
|
+
const pos = cut > 0 ? cut : limit;
|
|
81
|
+
return { text: content.slice(0, pos), truncated: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SearchDocParams = {
|
|
85
|
+
query: string;
|
|
86
|
+
max_results?: number;
|
|
87
|
+
char_limit?: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function searchDoc({ query, max_results = 5, char_limit = 3000 }: SearchDocParams): string {
|
|
91
|
+
const clampedMax = Math.min(Math.max(1, max_results), 10);
|
|
92
|
+
const clampedLimit = Math.max(100, char_limit);
|
|
93
|
+
|
|
94
|
+
const queryTokens = tokenize(query);
|
|
95
|
+
if (queryTokens.length === 0) {
|
|
96
|
+
return "Aucun mot-clé valide dans la requête.";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const scored = buildIndex()
|
|
100
|
+
.map((entry) => ({ entry, score: scoreSection(entry, queryTokens) }))
|
|
101
|
+
.filter(({ score }) => score > 0)
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, clampedMax);
|
|
104
|
+
|
|
105
|
+
if (scored.length === 0) {
|
|
106
|
+
return `Aucune section trouvée pour : « ${query} »`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return scored
|
|
110
|
+
.map(({ entry }) => {
|
|
111
|
+
const header = `### [${entry.category}] ${entry.title}`;
|
|
112
|
+
const { text, truncated } = truncateAtLine(entry.content, clampedLimit);
|
|
113
|
+
const suffix = truncated
|
|
114
|
+
? "\n[...contenu tronqué — utiliser get_doc_section pour le contenu complet]"
|
|
115
|
+
: "";
|
|
116
|
+
return `${header}\n${text}${suffix}`;
|
|
117
|
+
})
|
|
118
|
+
.join("\n\n---\n\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const searchDocTool = defineTool({
|
|
122
|
+
name: "search_doc",
|
|
123
|
+
description:
|
|
124
|
+
"Recherche plein texte dans toute la documentation Kiubi (guide integration, exemples de templates, api front-office). " +
|
|
125
|
+
"À utiliser en fallback quand le titre exact de la section n'est pas connu, " +
|
|
126
|
+
"ou pour une recherche transversale dans toute la documentation. " +
|
|
127
|
+
"Retourne les sections les plus pertinentes (contenu potentiellement tronqué : " +
|
|
128
|
+
"utiliser get_doc_section pour le contenu complet). " +
|
|
129
|
+
"Exemples : 'type billet site web index.html', 'fiche produit detail balises', " +
|
|
130
|
+
"'balises widget blog liste_billets', 'blocs composant collection item', " +
|
|
131
|
+
"'filtre mapValue', 'config.xml produit', 'symbole desc.xml structure'",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
query: z.string().min(1).describe("Mots-clés de recherche"),
|
|
134
|
+
max_results: z
|
|
135
|
+
.number()
|
|
136
|
+
.int()
|
|
137
|
+
.min(1)
|
|
138
|
+
.max(10)
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Nombre maximum de sections à retourner (défaut: 5, max: 10)"),
|
|
141
|
+
char_limit: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.min(100)
|
|
145
|
+
.optional()
|
|
146
|
+
.describe(
|
|
147
|
+
"Limite de caractères par section retournée. Augmenter (ex: 8000) pour des sections longues comme les widgets. Défaut: 3000."
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
handler: async ({ query, max_results, char_limit }) => {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text" as const,
|
|
155
|
+
text: searchDoc({ query, max_results: max_results ?? 5, char_limit: char_limit ?? 3000 }),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "../tool.ts";
|
|
3
|
+
import { ThemeInspector, ThemeChecker } from "#lib/theme-checker/index";
|
|
4
|
+
import type { ThemeReport, Issue } from "#lib/theme-checker/index";
|
|
5
|
+
|
|
6
|
+
export function validateTheme({ themePath }: { themePath: string }): string {
|
|
7
|
+
const inspector = new ThemeInspector(themePath);
|
|
8
|
+
const report = new ThemeChecker().run(inspector);
|
|
9
|
+
return formatReport(report, themePath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatReport(report: ThemeReport, themePath: string): string {
|
|
13
|
+
if (report.issues.length === 0) {
|
|
14
|
+
return `OK — aucun problème détecté dans le thème "${themePath}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const byCategory = new Map<string, { errors: Issue[]; warnings: Issue[] }>();
|
|
18
|
+
for (const issue of report.issues) {
|
|
19
|
+
if (!byCategory.has(issue.category)) {
|
|
20
|
+
byCategory.set(issue.category, { errors: [], warnings: [] });
|
|
21
|
+
}
|
|
22
|
+
const cat = byCategory.get(issue.category)!;
|
|
23
|
+
if (issue.level === 'error') cat.errors.push(issue);
|
|
24
|
+
else cat.warnings.push(issue);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const lines: string[] = [`Rapport de validation — ${themePath}`, ''];
|
|
28
|
+
const totalErrors = report.issues.filter(i => i.level === 'error').length;
|
|
29
|
+
const totalWarnings = report.issues.filter(i => i.level === 'warning').length;
|
|
30
|
+
|
|
31
|
+
for (const [category, { errors, warnings }] of byCategory) {
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
if (errors.length > 0) parts.push(`${errors.length} erreur${errors.length > 1 ? 's' : ''}`);
|
|
34
|
+
if (warnings.length > 0) parts.push(`${warnings.length} avertissement${warnings.length > 1 ? 's' : ''}`);
|
|
35
|
+
lines.push(`[${category.toUpperCase()}] — ${parts.join(', ')}`);
|
|
36
|
+
|
|
37
|
+
for (const issue of errors) {
|
|
38
|
+
const loc = issue.file ? ` ${issue.file}${issue.line ? `:${issue.line}` : ''}` : '';
|
|
39
|
+
lines.push(` ✗ ${issue.msg}${loc}`);
|
|
40
|
+
}
|
|
41
|
+
for (const issue of warnings) {
|
|
42
|
+
const loc = issue.file ? ` ${issue.file}${issue.line ? `:${issue.line}` : ''}` : '';
|
|
43
|
+
lines.push(` ⚠ ${issue.msg}${loc}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push(`Total : ${totalErrors} erreur${totalErrors > 1 ? 's' : ''}, ${totalWarnings} avertissement${totalWarnings > 1 ? 's' : ''}`);
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const validateThemeTool = defineTool({
|
|
53
|
+
name: "validate_theme",
|
|
54
|
+
description: "Vérifie la syntaxe et la cohérence de tous les fichiers du thème graphique",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
themePath: z.string().min(1).describe("Chemin absolu vers le dossier racine du thème graphique"),
|
|
57
|
+
},
|
|
58
|
+
handler: async ({ themePath }) => {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text" as const, text: validateTheme({ themePath }) }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|