@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,208 @@
|
|
|
1
|
+
import {z} from "zod";
|
|
2
|
+
import {defineTool} from "../tool.ts";
|
|
3
|
+
import {ThemeInspector, resolveLangs} from "#lib/theme-checker/index";
|
|
4
|
+
import type {TypeDefinition, ModelDef, SymbolDef, ComponentDef} from "#lib/theme-checker/types";
|
|
5
|
+
import {join} from "node:path";
|
|
6
|
+
|
|
7
|
+
type ListType = "billet_page" | "symbole" | "composant" | "produit" | "billet_blog" | "template";
|
|
8
|
+
|
|
9
|
+
type TypeVariant = {
|
|
10
|
+
lang: string ;
|
|
11
|
+
title?: string;
|
|
12
|
+
position: number | null;
|
|
13
|
+
path: string;
|
|
14
|
+
files: string[];
|
|
15
|
+
fields: string[];
|
|
16
|
+
collection: string[];
|
|
17
|
+
zones: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TYPE_MAP: Record<ListType, (inspector: ThemeInspector) => TypeDefinition[]> = {
|
|
21
|
+
billet_page: (i) => i.listCms(),
|
|
22
|
+
symbole: (i) => i.listSymbols(),
|
|
23
|
+
composant: (i) => i.listComponents(),
|
|
24
|
+
produit: (i) => i.listProducts(),
|
|
25
|
+
billet_blog: (i) => i.listBlogs(),
|
|
26
|
+
template: (i) => i.listModels(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PATH_MAP: Record<ListType, string> = {
|
|
30
|
+
billet_page: 'billets',
|
|
31
|
+
symbole: 'symboles',
|
|
32
|
+
composant: 'composants',
|
|
33
|
+
produit: 'produits',
|
|
34
|
+
billet_blog: 'billets_blog',
|
|
35
|
+
template: 'templates',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function isModelDef(d: TypeDefinition): d is ModelDef {
|
|
39
|
+
return 'zones' in d && 'structure' in d && !('fields' in d);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSymbolDef(d: TypeDefinition): d is SymbolDef {
|
|
43
|
+
return 'zones' in d && 'fields' in d && 'structure' in d;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isComponentDef(d: TypeDefinition): d is ComponentDef {
|
|
47
|
+
return 'hasCollection' in d;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatDetail(variant: TypeVariant): string {
|
|
51
|
+
const lines: string[] = [
|
|
52
|
+
'',
|
|
53
|
+
`Version [${variant.lang}]`
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if (variant.title) {
|
|
57
|
+
lines.push(`Intitulé : ${variant.title}`);
|
|
58
|
+
}
|
|
59
|
+
if (variant.position) {
|
|
60
|
+
lines.push(`Position : ${variant.position}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(`Dossier : ${variant.path}`);
|
|
63
|
+
|
|
64
|
+
if (variant.files.length > 0) {
|
|
65
|
+
lines.push('Fichiers :');
|
|
66
|
+
lines.push(...variant.files);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (variant.fields.length > 0) {
|
|
70
|
+
lines.push('Champs :');
|
|
71
|
+
lines.push(...variant.fields);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (variant.collection.length > 0) {
|
|
75
|
+
lines.push('Collection :');
|
|
76
|
+
lines.push(...variant.collection);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (variant.zones.length > 0) {
|
|
80
|
+
lines.push('Zones :');
|
|
81
|
+
lines.push(...variant.zones);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function detailTypeForLang(themePath: string, lang: string, type: ListType, name: string) : TypeVariant | null {
|
|
88
|
+
const inspector = new ThemeInspector(themePath, lang);
|
|
89
|
+
const items = TYPE_MAP[type](inspector);
|
|
90
|
+
const item = items.find(i => i.id === name);
|
|
91
|
+
|
|
92
|
+
if (!item) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const label = 'name' in item && item.name ? item.name
|
|
97
|
+
: 'desc' in item && item.name ? item.name
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
const basePath = join(inspector.getRelLgPath(PATH_MAP[type]), name);
|
|
101
|
+
|
|
102
|
+
const files = [];
|
|
103
|
+
switch (type) {
|
|
104
|
+
case "composant" :
|
|
105
|
+
case "produit" :
|
|
106
|
+
case "billet_page" :
|
|
107
|
+
files.push(basePath + '/index.html // template');
|
|
108
|
+
files.push(basePath + '/config.xml // définition');
|
|
109
|
+
break;
|
|
110
|
+
case "billet_blog" :
|
|
111
|
+
files.push(basePath + '/config.xml // définition');
|
|
112
|
+
files.push('Attention : le template est commun à tous les billets du blog dans ' + join(inspector.getRelLgPath('widgets/blog/detail_billet/index.html')));
|
|
113
|
+
break;
|
|
114
|
+
case "symbole" :
|
|
115
|
+
case "template" :
|
|
116
|
+
files.push(basePath + '/index.html // template');
|
|
117
|
+
files.push(basePath + '/desc.xml // définition');
|
|
118
|
+
files.push(basePath + '/structure.xhml // structure des zones');
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fields = [];
|
|
123
|
+
const collection = [];
|
|
124
|
+
const zones = [];
|
|
125
|
+
|
|
126
|
+
if ("fields" in item && item.fields && item.fields.length > 0) {
|
|
127
|
+
for (const field of item.fields) {
|
|
128
|
+
const def = field.default ? ` (défaut: ${field.default})` : '';
|
|
129
|
+
fields.push(`${field.field} : ${field.name} [${field.type}]${def}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isComponentDef(item) && item.hasCollection && item.collection.length > 0) {
|
|
134
|
+
for (const field of item.collection) {
|
|
135
|
+
const def = field.default ? ` (défaut: ${field.default})` : '';
|
|
136
|
+
collection.push(`${field.field} : ${field.name} [${field.type}]${def}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ((isModelDef(item) || isSymbolDef(item)) && item.zones.length > 0) {
|
|
141
|
+
for (const zone of item.zones) {
|
|
142
|
+
const content = zone.isContent ? ' [contenu]' : '';
|
|
143
|
+
zones.push(`${zone.id} : ${zone.name}${content} (colmax: ${zone.colmax})`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
lang:lang,
|
|
149
|
+
title: label,
|
|
150
|
+
position: ('position' in item) ? item.position : null,
|
|
151
|
+
path: basePath,
|
|
152
|
+
files: files,
|
|
153
|
+
fields: fields,
|
|
154
|
+
collection: collection,
|
|
155
|
+
zones: zones,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function detailType({themePath, type, name, lang}: {themePath: string; type: ListType; name: string; lang?: string[] | null}): string {
|
|
160
|
+
const resolved = resolveLangs(themePath, lang);
|
|
161
|
+
if ('error' in resolved) return `Erreur : ${resolved.error}`;
|
|
162
|
+
|
|
163
|
+
const variants = resolved.map((l) => {
|
|
164
|
+
return {lang: l, detail: detailTypeForLang(themePath, l, type, name)};
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
if (variants.filter(x => x.detail !== null).length === 0) {
|
|
168
|
+
return `Aucun élément "${name}" de type "${type}" trouvé dans le thème "${themePath}"`; // TODO add fuzzy search accross all lang
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const blocks = [
|
|
172
|
+
`Type : ${type}`,
|
|
173
|
+
`Identifiant : ${name}`
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
blocks.push(...variants.map(
|
|
177
|
+
(variant) =>{
|
|
178
|
+
if (variant.detail === null) {
|
|
179
|
+
if (lang === null || lang === undefined) return null;
|
|
180
|
+
return `\nVersion [${variant.lang}]\nType non défini`
|
|
181
|
+
}
|
|
182
|
+
return formatDetail(variant.detail);
|
|
183
|
+
}
|
|
184
|
+
).filter(x => x !== null));
|
|
185
|
+
return blocks.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const detailTypeTool = defineTool({
|
|
189
|
+
name: "detail_type",
|
|
190
|
+
description:
|
|
191
|
+
"Retourne le détail d'un élément du thème graphique (champs, zones, collection) à partir de son type et de son identifiant. " +
|
|
192
|
+
"lang : si omis (ou vide), inspecte tous les dossiers de langue (2 lettres) trouvés à la racine du thème ; si " +
|
|
193
|
+
"précisé, uniquement les langues listées (erreur si un dossier de langue demandé n'existe pas). " +
|
|
194
|
+
"Le résultat liste le détail (ou l'absence) de l'élément par langue.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
themePath: z.string().min(1).describe("Chemin absolu vers le dossier racine du thème graphique"),
|
|
197
|
+
type: z.enum(["billet_page", "symbole", "composant", "produit", "billet_blog", "template"]).describe("Type d'élément"),
|
|
198
|
+
name: z.string().min(1).describe("Identifiant de l'élément (son nom de répertoire)"),
|
|
199
|
+
lang: z.array(z.string().regex(/^[a-z]{2}$/, "code langue sur 2 lettres minuscules attendu (ex. \"fr\")"))
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Codes langue (2 lettres minuscules, ex. [\"fr\",\"de\"]) à inspecter. Omis ou vide : tous les dossiers de langue du thème."),
|
|
202
|
+
},
|
|
203
|
+
handler: async ({themePath, type, name, lang}) => {
|
|
204
|
+
return {
|
|
205
|
+
content: [{type: "text" as const, text: detailType({themePath, type, name, lang})}],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
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 } 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 EXEMPLES_PATH = join(DOCS_BASE, "generated/exemples.md");
|
|
10
|
+
const API_PATH = join(DOCS_BASE, "kiubi_api.md");
|
|
11
|
+
const WORKFLOWS_PATH = join(DOCS_BASE, "kiubi_workflows.md");
|
|
12
|
+
|
|
13
|
+
type Category = "integration" | "exemples" | "api" | "workflows";
|
|
14
|
+
|
|
15
|
+
const singleDocument = {
|
|
16
|
+
"exemples" : EXEMPLES_PATH,
|
|
17
|
+
"api" : API_PATH,
|
|
18
|
+
"workflows" : WORKFLOWS_PATH,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function listH2Titles(content: string): Array<{section: string}> {
|
|
22
|
+
return content
|
|
23
|
+
.split("\n")
|
|
24
|
+
.filter((line) => /^## /.test(line))
|
|
25
|
+
.map((line) => {
|
|
26
|
+
return {section: line.replace(/^## /, "").trim()}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getIntegrationSections(): Array<{ section: string; file: string }> {
|
|
31
|
+
if (!existsSync(INTEGRATION_DIR)) return [];
|
|
32
|
+
|
|
33
|
+
return readdirSync(INTEGRATION_DIR)
|
|
34
|
+
.filter((f) => f.endsWith(".md"))
|
|
35
|
+
.sort()
|
|
36
|
+
.flatMap((file) => {
|
|
37
|
+
const content = readFileSync(join(INTEGRATION_DIR, file), "utf-8");
|
|
38
|
+
const titles = listH2Titles(content);
|
|
39
|
+
return titles.length > 0 ? titles.map((title) => ({ section: title.section, file: file })) : [{ section: file, file: file }];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listSections(category: "all" | Category): string {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (category === "all" || category === "integration") {
|
|
47
|
+
const sections = getIntegrationSections();
|
|
48
|
+
const lines = sections.map((s) => ` - ${s.section}`).join("\n");
|
|
49
|
+
parts.push(`La catégorie "integration" contient les chapitres suivants :\n${lines}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (category === "all" || category === "exemples") {
|
|
53
|
+
if (existsSync(EXEMPLES_PATH)) {
|
|
54
|
+
const content = readFileSync(EXEMPLES_PATH, "utf-8");
|
|
55
|
+
const lines = listH2Titles(content)
|
|
56
|
+
.map((t) => ` - ${t.section}`)
|
|
57
|
+
.join("\n");
|
|
58
|
+
parts.push(`La catégorie "exemples" contient les chapitres suivants :\n${lines}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (category === "all" || category === "api") {
|
|
63
|
+
if (existsSync(API_PATH)) {
|
|
64
|
+
const content = readFileSync(API_PATH, "utf-8");
|
|
65
|
+
const lines = listH2Titles(content)
|
|
66
|
+
.map((t) => ` - ${t.section}`)
|
|
67
|
+
.join("\n");
|
|
68
|
+
parts.push(`La catégorie "api" contient les chapitres suivants :\n${lines}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (category === "all" || category === "workflows") {
|
|
73
|
+
if (existsSync(WORKFLOWS_PATH)) {
|
|
74
|
+
const content = readFileSync(WORKFLOWS_PATH, "utf-8");
|
|
75
|
+
const lines = listH2Titles(content)
|
|
76
|
+
.map((t) => ` - ${t.section}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
parts.push(`La catégorie "workflows" contient les workflows suivants :\n${lines}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parts.join("\n\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function matchSection<T extends {section: string}>(list: T[], matchingWords: string[]): T | null {
|
|
86
|
+
|
|
87
|
+
if (matchingWords.length === 0 || list.length === 0) return null;
|
|
88
|
+
|
|
89
|
+
const scored = list.map((item) => {
|
|
90
|
+
const normSection = normalize(item.section);
|
|
91
|
+
const score = matchingWords.reduce(
|
|
92
|
+
(acc, word) => acc + (normSection.includes(word) ? 1 : 0),
|
|
93
|
+
0
|
|
94
|
+
);
|
|
95
|
+
return { item, score };
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const max = scored.reduce((best, current) => {
|
|
99
|
+
return current.score > best.score ? current : best;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (max.score === 0) return null;
|
|
103
|
+
|
|
104
|
+
return max.item;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function extractSectionFromFile(content: string, section: string) : string | null {
|
|
108
|
+
|
|
109
|
+
const rawSections = content.split(/(?=^## )/m).filter((s) => s.trim().length > 0);
|
|
110
|
+
const match = rawSections.find((s) => s.split("\n")[0].replace(/^## /, "").trim() === section) ?? '';
|
|
111
|
+
if (match) {
|
|
112
|
+
return match.trimEnd();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function fetchSection(category: Category, section: string): string {
|
|
119
|
+
|
|
120
|
+
let matchingWords = normalize(section)
|
|
121
|
+
.split(/[^a-z0-9]+/)
|
|
122
|
+
.filter((w) => w.length >= 3);
|
|
123
|
+
|
|
124
|
+
if (category === "integration") {
|
|
125
|
+
const sectionList = getIntegrationSections();
|
|
126
|
+
const matchingSection = matchSection(sectionList, matchingWords);
|
|
127
|
+
|
|
128
|
+
if (matchingSection) {
|
|
129
|
+
const content = readFileSync(join(INTEGRATION_DIR, matchingSection.file), "utf-8");
|
|
130
|
+
return extractSectionFromFile(content, matchingSection.section) ?? 'Erreur innatendue';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const titles = sectionList.map((s) => ` - ${s.section}`).join("\n");
|
|
134
|
+
return `Aucune section ne correspond à « ${section} » dans la catégorie "integration".\n\nSections disponibles :\n${titles}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const filePath = singleDocument[category];
|
|
138
|
+
if (!existsSync(filePath)) {
|
|
139
|
+
return `Fichier de documentation introuvable pour la catégorie "${category}".`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(filePath, "utf-8");
|
|
143
|
+
const sectionList = listH2Titles(content);
|
|
144
|
+
|
|
145
|
+
if (category === 'workflows') {
|
|
146
|
+
matchingWords = normalize(section)
|
|
147
|
+
.split(/[^a-z0-9]+/)
|
|
148
|
+
.map((s) => (s.length === 1 ? `workflow ${s}` : s));
|
|
149
|
+
}
|
|
150
|
+
const matchingSection = matchSection(sectionList, matchingWords);
|
|
151
|
+
|
|
152
|
+
if (matchingSection) {
|
|
153
|
+
return extractSectionFromFile(content, matchingSection.section) ?? 'Erreur innatendue';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const titles = sectionList
|
|
157
|
+
.filter((t) => t.section.length > 0)
|
|
158
|
+
.map((t) => ` - ${t.section}`)
|
|
159
|
+
.join("\n");
|
|
160
|
+
return `Aucune section ne correspond à « ${section} » dans la catégorie "${category}".\n\nSections disponibles :\n${titles}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getDocSection(category: string, section?: string): string {
|
|
164
|
+
if (category === "all" || !section) {
|
|
165
|
+
return listSections(category as "all" | Category);
|
|
166
|
+
}
|
|
167
|
+
return fetchSection(category as Category, section);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const getDocSectionTool = defineTool({
|
|
171
|
+
name: "get_doc_section",
|
|
172
|
+
description:
|
|
173
|
+
"Accès à la documentation Kiubi : guide d'intégration, catalogue d'exemples, API front-office et workflows. L'outil fonctionne selon 2 modes : \n" +
|
|
174
|
+
" - Sans le paramètre facultatif 'section' : liste les titres de chapitres disponibles pour la catégorie (ou pour toutes les catégories avec category='all'). \n" +
|
|
175
|
+
" - Avec le paramètre facultatif 'section' : retourne le contenu complet du chapitre demandé (recherche insensible à la casse, partielle). \n" +
|
|
176
|
+
"Si le chapitre n'est pas trouvé, retourne la liste des chapitres disponibles dans la catégorie.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
category: z
|
|
179
|
+
.enum(["all", "integration", "exemples", "api", "workflows"])
|
|
180
|
+
.describe(
|
|
181
|
+
"Catégorie de documentation : 'all' pour lister toute la documentation, " +
|
|
182
|
+
"'integration' pour le guide d'intégration des thèmes graphiques, " +
|
|
183
|
+
"'exemples' pour le catalogue d'exemples de templates et widgets, " +
|
|
184
|
+
"'api' pour les endpoints de l'API front-office Kiubi, " +
|
|
185
|
+
"'workflows' pour les workflows d'intégration étape par étape."
|
|
186
|
+
),
|
|
187
|
+
section: z
|
|
188
|
+
.string()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe(
|
|
191
|
+
"Titre (ou fragment) du chapitre à récupérer. " +
|
|
192
|
+
"Si omis, retourne la liste des chapitres de la catégorie. " +
|
|
193
|
+
"Exemples : 'Blocs', 'Widgets', 'Site web'."
|
|
194
|
+
),
|
|
195
|
+
},
|
|
196
|
+
handler: async ({ category, section }) => {
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text" as const, text: getDocSection(category, section) }],
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {z} from "zod";
|
|
2
|
+
import {defineTool} from "../tool.ts";
|
|
3
|
+
import {ThemeInspector, resolveLangs} from "#lib/theme-checker/index";
|
|
4
|
+
import type {TypeDefinition} from "#lib/theme-checker/types";
|
|
5
|
+
import {join} from "node:path";
|
|
6
|
+
|
|
7
|
+
export const LIST_TYPES = ["billet_page", "symbole", "composant", "produit", "billet_blog", "template"] as const;
|
|
8
|
+
type ListType = typeof LIST_TYPES[number];
|
|
9
|
+
|
|
10
|
+
const TYPE_MAP: Record<ListType, (inspector: ThemeInspector) => {items: TypeDefinition[], subdir: string}> = {
|
|
11
|
+
billet_page: (i) => {
|
|
12
|
+
return {items : i.listCms(), subdir: 'billets'}
|
|
13
|
+
},
|
|
14
|
+
symbole: (i) => {
|
|
15
|
+
return {items : i.listSymbols(), subdir: 'symboles'}
|
|
16
|
+
},
|
|
17
|
+
composant: (i) => {
|
|
18
|
+
return {items : i.listComponents(), subdir: 'composants'}
|
|
19
|
+
},
|
|
20
|
+
produit: (i) => {
|
|
21
|
+
return {items : i.listProducts(), subdir: 'produits'}
|
|
22
|
+
},
|
|
23
|
+
billet_blog: (i) => {
|
|
24
|
+
return {items : i.listBlogs(), subdir: 'billets_blog'}
|
|
25
|
+
},
|
|
26
|
+
template: (i) => {
|
|
27
|
+
return {items : i.listModels(), subdir: 'templates'}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function listTypesForLang(themePath: string, lang: string, type: ListType): string {
|
|
32
|
+
const inspector = new ThemeInspector(themePath, lang);
|
|
33
|
+
const infos = TYPE_MAP[type](inspector);
|
|
34
|
+
const path = join(lang, infos.subdir);
|
|
35
|
+
|
|
36
|
+
const lines = [`Éléments de type "${type}" [${lang}] :`, ''];
|
|
37
|
+
|
|
38
|
+
if (infos.items.length === 0) {
|
|
39
|
+
lines.push(`Aucun élément de type "${type}" trouvé dans le thème "${themePath}" pour la langue "${lang}".`)
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
infos.items.forEach(item => {
|
|
44
|
+
lines.push('- ' + join(path, item.id) + (item.name !== '' ? ' : ' + item.name : ''));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (type === "billet_blog") {
|
|
48
|
+
const wPath = inspector.getRelLgPath('widgets/blog/detail_billet/index.html');
|
|
49
|
+
lines.push('', `Attention : les config.xml sont dans ${path}/ mais le template est centralisé dans ${wPath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listTypes({themePath, type, lang}: { themePath: string; type: ListType; lang?: string[] | null }): string {
|
|
56
|
+
const resolved = resolveLangs(themePath, lang);
|
|
57
|
+
if ('error' in resolved) return `Erreur : ${resolved.error}`;
|
|
58
|
+
|
|
59
|
+
const blocks = resolved.map(l => `${listTypesForLang(themePath, l, type)}`);
|
|
60
|
+
return blocks.join('\n\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const listTypesTool = defineTool({
|
|
64
|
+
name: "list_types",
|
|
65
|
+
description: "Liste tous les éléments présent dans le thème graphique d'un type : \n" +
|
|
66
|
+
" - billet_page (billets des pages)\n" +
|
|
67
|
+
" - billet_blog (billets du blog)\n" +
|
|
68
|
+
" - composant\n" +
|
|
69
|
+
" - produit\n" +
|
|
70
|
+
" - symbole\n" +
|
|
71
|
+
" - template (templates principaux)\n" +
|
|
72
|
+
"lang : si omis (ou vide), liste dans tous les dossiers de langue (2 lettres) trouvés à la racine du thème ; " +
|
|
73
|
+
"si précisé, uniquement les langues listées (erreur si un dossier de langue demandé n'existe pas). " +
|
|
74
|
+
"Le résultat liste les éléments par langue.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
themePath: z.string().min(1).describe("Chemin absolu vers le dossier racine du thème graphique"),
|
|
77
|
+
type: z.enum(["billet_page", "symbole", "composant", "produit", "billet_blog", "template"]).describe("Type d'élément à lister"),
|
|
78
|
+
lang: z.array(z.string().regex(/^[a-z]{2}$/, "code langue sur 2 lettres minuscules attendu (ex. \"fr\")"))
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Codes langue (2 lettres minuscules, ex. [\"fr\",\"de\"]) à lister. Omis ou vide : tous les dossiers de langue du thème."),
|
|
81
|
+
},
|
|
82
|
+
handler: async ({themePath, type, lang}) => {
|
|
83
|
+
return {
|
|
84
|
+
content: [{type: "text" as const, text: listTypes({themePath, type, lang})}],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "../tool.ts";
|
|
3
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { resolveLangs } from "#lib/theme-checker/index";
|
|
6
|
+
|
|
7
|
+
type WidgetInfo = {
|
|
8
|
+
name: string;
|
|
9
|
+
templates: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type CategoryInfo = {
|
|
13
|
+
name: string;
|
|
14
|
+
widgets: WidgetInfo[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function scanLangWidgets(themePath: string, lang: string): CategoryInfo[] | null {
|
|
18
|
+
const widgetsDir = join(themePath, lang, "widgets");
|
|
19
|
+
if (!existsSync(widgetsDir)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return scanWidgetsDir(widgetsDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function listWidgets({ themePath, lang }: { themePath: string; lang?: string[] | null }): string {
|
|
26
|
+
const resolved = resolveLangs(themePath, lang);
|
|
27
|
+
if ('error' in resolved) return `Erreur : ${resolved.error}`;
|
|
28
|
+
|
|
29
|
+
// Regroupe les widgets par langue avant de les fusionner par clé "catégorie/widget",
|
|
30
|
+
// pour pouvoir afficher toutes les versions (ou l'absence) de chaque widget.
|
|
31
|
+
const widgetKeys = new Map<string, { category: string; widget: string }>();
|
|
32
|
+
const templatesByLang = new Map<string, Map<string, string[]>>();
|
|
33
|
+
|
|
34
|
+
for (const l of resolved) {
|
|
35
|
+
const categories = scanLangWidgets(themePath, l);
|
|
36
|
+
const templates = new Map<string, string[]>();
|
|
37
|
+
|
|
38
|
+
for (const category of categories ?? []) {
|
|
39
|
+
for (const widget of category.widgets) {
|
|
40
|
+
const key = `${category.name}/${widget.name}`;
|
|
41
|
+
widgetKeys.set(key, { category: category.name, widget: widget.name });
|
|
42
|
+
templates.set(key, widget.templates);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
templatesByLang.set(l, templates);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (widgetKeys.size === 0) {
|
|
50
|
+
return `Aucun widget trouvé dans le thème "${themePath}" pour la/les langue(s) ${resolved.join(', ')}.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
|
|
55
|
+
for (const key of [...widgetKeys.keys()].sort()) {
|
|
56
|
+
const { category, widget } = widgetKeys.get(key)!;
|
|
57
|
+
lines.push(`Widget ${category} / ${widget} :`);
|
|
58
|
+
|
|
59
|
+
for (const l of resolved) {
|
|
60
|
+
lines.push(` Version [${l}]`);
|
|
61
|
+
const templates = templatesByLang.get(l)!.get(key);
|
|
62
|
+
if (templates) {
|
|
63
|
+
lines.push(` Dossier : ${l}/widgets/${category}/${widget}`);
|
|
64
|
+
lines.push(` Fichiers :`);
|
|
65
|
+
for (const template of templates) {
|
|
66
|
+
lines.push(` - ${template}`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
lines.push(` Widget non défini`);
|
|
70
|
+
}
|
|
71
|
+
lines.push("");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n").trimEnd();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scanWidgetsDir(widgetsDir: string): CategoryInfo[] {
|
|
79
|
+
const categories: CategoryInfo[] = [];
|
|
80
|
+
|
|
81
|
+
let categoryEntries: string[];
|
|
82
|
+
try {
|
|
83
|
+
categoryEntries = readdirSync(widgetsDir);
|
|
84
|
+
} catch {
|
|
85
|
+
return categories;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const categoryName of categoryEntries.sort()) {
|
|
89
|
+
const categoryPath = join(widgetsDir, categoryName);
|
|
90
|
+
|
|
91
|
+
let widgetEntries: string[];
|
|
92
|
+
try {
|
|
93
|
+
widgetEntries = readdirSync(categoryPath);
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const widgets: WidgetInfo[] = [];
|
|
99
|
+
|
|
100
|
+
for (const widgetName of widgetEntries.sort()) {
|
|
101
|
+
const widgetPath = join(categoryPath, widgetName);
|
|
102
|
+
const indexPath = join(widgetPath, "index.html");
|
|
103
|
+
|
|
104
|
+
if (!existsSync(indexPath)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const templates: string[] = ["index.html"];
|
|
109
|
+
|
|
110
|
+
let widgetChildren: string[];
|
|
111
|
+
try {
|
|
112
|
+
widgetChildren = readdirSync(widgetPath);
|
|
113
|
+
} catch {
|
|
114
|
+
widgetChildren = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Collect alternative .html files at widget level (excluding index.html)
|
|
118
|
+
const htmlFiles = widgetChildren
|
|
119
|
+
.filter((f) => f.endsWith(".html") && f !== "index.html")
|
|
120
|
+
.sort();
|
|
121
|
+
templates.push(...htmlFiles);
|
|
122
|
+
|
|
123
|
+
// Collect alternative templates from subdirectories (level 3)
|
|
124
|
+
const subDirs = widgetChildren
|
|
125
|
+
.filter((f) => !f.endsWith(".html"))
|
|
126
|
+
.sort();
|
|
127
|
+
|
|
128
|
+
for (const subDir of subDirs) {
|
|
129
|
+
const subDirPath = join(widgetPath, subDir);
|
|
130
|
+
let subEntries: string[];
|
|
131
|
+
try {
|
|
132
|
+
subEntries = readdirSync(subDirPath);
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const subHtmlFiles = subEntries
|
|
138
|
+
.filter((f) => f.endsWith(".html"))
|
|
139
|
+
.sort();
|
|
140
|
+
|
|
141
|
+
for (const htmlFile of subHtmlFiles) {
|
|
142
|
+
templates.push(`${subDir}/${htmlFile}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
widgets.push({ name: widgetName, templates });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (widgets.length > 0) {
|
|
150
|
+
categories.push({ name: categoryName, widgets });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return categories;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const listWidgetsTool = defineTool({
|
|
158
|
+
name: "list_widgets",
|
|
159
|
+
description:
|
|
160
|
+
"Liste tous les widgets présents dans le thème graphique et leurs templates alternatifs. " +
|
|
161
|
+
"À utiliser avant toute création de template secondaire de widget. " +
|
|
162
|
+
"Retourne un bloc par widget (catégorie / nom), avec pour chaque langue : le dossier et la liste des " +
|
|
163
|
+
"templates (index.html + templates alternatifs), ou la mention \"Widget non défini\" si ce widget n'existe " +
|
|
164
|
+
"pas pour cette langue. " +
|
|
165
|
+
"lang : si omis (ou vide), inclut tous les dossiers de langue (2 lettres) trouvés à la racine du thème ; " +
|
|
166
|
+
"si précisé, uniquement les langues listées (erreur si un dossier de langue demandé n'existe pas).",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
themePath: z
|
|
169
|
+
.string()
|
|
170
|
+
.min(1)
|
|
171
|
+
.describe("Chemin absolu vers le dossier racine du thème graphique"),
|
|
172
|
+
lang: z.array(z.string().regex(/^[a-z]{2}$/, "code langue sur 2 lettres minuscules attendu (ex. \"fr\")"))
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Codes langue (2 lettres minuscules, ex. [\"fr\",\"de\"]) à lister. Omis ou vide : tous les dossiers de langue du thème."),
|
|
175
|
+
},
|
|
176
|
+
handler: async ({ themePath, lang }) => {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text" as const, text: listWidgets({ themePath, lang }) }],
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
});
|