@maxisoft/instantcms-mcp 1.2.3
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 +308 -0
- package/dist/__tests__/artifact-tool.test.js +196 -0
- package/dist/__tests__/db-tool.test.js +152 -0
- package/dist/__tests__/define-tool.test.js +106 -0
- package/dist/__tests__/find-tool.test.js +185 -0
- package/dist/__tests__/get-component-api.test.js +144 -0
- package/dist/__tests__/hardening.test.js +73 -0
- package/dist/__tests__/hooks-tool.test.js +173 -0
- package/dist/__tests__/mcp-integration-extra.test.js +166 -0
- package/dist/__tests__/mcp-integration.test.js +53 -0
- package/dist/__tests__/pagination.test.js +114 -0
- package/dist/__tests__/performance-baseline.test.js +56 -0
- package/dist/__tests__/project-patch.test.js +144 -0
- package/dist/__tests__/project-source.test.js +44 -0
- package/dist/__tests__/project-workflows-extra.test.js +165 -0
- package/dist/__tests__/project-workflows.test.js +51 -0
- package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
- package/dist/__tests__/serialization.test.js +170 -0
- package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
- package/dist/__tests__/template-development.test.js +78 -0
- package/dist/__tests__/template-productivity.test.js +87 -0
- package/dist/__tests__/tools.test.js +2143 -0
- package/dist/data/components.js +2818 -0
- package/dist/data/controllers-map.js +7571 -0
- package/dist/data/core-api.js +9356 -0
- package/dist/data/database-schema.js +4555 -0
- package/dist/data/events-map.js +1339 -0
- package/dist/data/fields-map.js +3692 -0
- package/dist/data/hooks.js +2257 -0
- package/dist/data/js-api.js +123 -0
- package/dist/data/libs-api.js +264 -0
- package/dist/data/routes-map.js +203 -0
- package/dist/data/schemas-validation.js +255 -0
- package/dist/data/schemas.js +1404 -0
- package/dist/data/traits-map.js +1004 -0
- package/dist/data/version-profiles.js +41 -0
- package/dist/data/widgets-map.js +131 -0
- package/dist/data/wysiwyg-map.js +486 -0
- package/dist/generated/components-source.js +14748 -0
- package/dist/generated/hooks-source.js +3399 -0
- package/dist/generated/knowledge-meta.js +101 -0
- package/dist/index.js +13 -0
- package/dist/registry/database-tools.js +112 -0
- package/dist/registry/extension-tools.js +473 -0
- package/dist/registry/generator-tools.js +506 -0
- package/dist/registry/knowledge-tools.js +441 -0
- package/dist/registry/language-tools.js +99 -0
- package/dist/registry/meta-tools.js +152 -0
- package/dist/registry/project-tools.js +48 -0
- package/dist/registry/resources.js +98 -0
- package/dist/registry/source-tools.js +211 -0
- package/dist/registry/template-development-tools.js +79 -0
- package/dist/server.js +32 -0
- package/dist/tools/addon-tool.js +1437 -0
- package/dist/tools/admin-partial-tool.js +498 -0
- package/dist/tools/api-tool.js +294 -0
- package/dist/tools/artifact-tool.js +95 -0
- package/dist/tools/cache-tool.js +360 -0
- package/dist/tools/component-tool.js +415 -0
- package/dist/tools/controllers-tool.js +125 -0
- package/dist/tools/cron-tool.js +197 -0
- package/dist/tools/crud-tool.js +659 -0
- package/dist/tools/db-tool.js +198 -0
- package/dist/tools/email-tool.js +222 -0
- package/dist/tools/external-api-tool.js +596 -0
- package/dist/tools/filter-tool.js +341 -0
- package/dist/tools/form-tool.js +275 -0
- package/dist/tools/grid-tool.js +189 -0
- package/dist/tools/hooks-tool.js +104 -0
- package/dist/tools/import-export-tool.js +548 -0
- package/dist/tools/lang-tool.js +251 -0
- package/dist/tools/layout-override-tool.js +125 -0
- package/dist/tools/layout-tool.js +548 -0
- package/dist/tools/maria-tool.js +127 -0
- package/dist/tools/mariadb.js +201 -0
- package/dist/tools/migration-tool.js +329 -0
- package/dist/tools/oauth-tool.js +520 -0
- package/dist/tools/parser/components-parser.js +76 -0
- package/dist/tools/parser/controllers-parser.js +313 -0
- package/dist/tools/parser/core-parser.js +294 -0
- package/dist/tools/parser/coverage-generator.js +424 -0
- package/dist/tools/parser/events-parser.js +127 -0
- package/dist/tools/parser/fields-parser.js +382 -0
- package/dist/tools/parser/hooks-parser.js +106 -0
- package/dist/tools/parser/sql-parser.js +197 -0
- package/dist/tools/parser/traits-parser.js +161 -0
- package/dist/tools/parser/widgets-parser.js +150 -0
- package/dist/tools/permission-tool.js +348 -0
- package/dist/tools/project-patch-tool.js +73 -0
- package/dist/tools/project-source-tool.js +188 -0
- package/dist/tools/project-workflow-tool.js +186 -0
- package/dist/tools/requirement-tool.js +212 -0
- package/dist/tools/scaffold-tool.js +815 -0
- package/dist/tools/seo-tool.js +412 -0
- package/dist/tools/source-tool.js +155 -0
- package/dist/tools/template-development-tool.js +271 -0
- package/dist/tools/template-overrides-tool.js +294 -0
- package/dist/tools/template-productivity-tool.js +319 -0
- package/dist/tools/template-tool.js +665 -0
- package/dist/tools/test-tool.js +183 -0
- package/dist/tools/webhook-tool.js +427 -0
- package/dist/tools/widget-tool.js +331 -0
- package/dist/tools/wysiwyg-tool.js +137 -0
- package/dist/types/scaffold.js +68 -0
- package/dist/utils/define-tool.js +37 -0
- package/dist/utils/find-tool.js +97 -0
- package/dist/utils/mcp-result.js +18 -0
- package/dist/utils/pagination.js +27 -0
- package/dist/utils/serialization.js +27 -0
- package/package.json +94 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Widget scaffolding tool for InstantCMS
|
|
4
|
+
* Generates widgets with options, templates, and configuration
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.widgetToolSchema = exports.WidgetFieldTypeEnum = void 0;
|
|
8
|
+
exports.scaffoldWidget = scaffoldWidget;
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const scaffold_1 = require("../types/scaffold");
|
|
11
|
+
/**
|
|
12
|
+
* Widget option field types
|
|
13
|
+
*/
|
|
14
|
+
exports.WidgetFieldTypeEnum = zod_1.z.enum([
|
|
15
|
+
'text',
|
|
16
|
+
'number',
|
|
17
|
+
'select',
|
|
18
|
+
'checkbox',
|
|
19
|
+
'textarea',
|
|
20
|
+
'image',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Generates widget class
|
|
24
|
+
*/
|
|
25
|
+
function generateWidgetClass(name, widget, Widget, options, options_config) {
|
|
26
|
+
const optionDefaults = options
|
|
27
|
+
.map(o => {
|
|
28
|
+
const defaultVal = typeof o.default === 'string' ? `'${o.default}'` : (o.default ?? 'null');
|
|
29
|
+
return ` '${o.name}' => ${defaultVal},`;
|
|
30
|
+
})
|
|
31
|
+
.join('\n');
|
|
32
|
+
return `<?php
|
|
33
|
+
// InstantCMS 2. ${name}/widgets/${widget}.php
|
|
34
|
+
|
|
35
|
+
class ${Widget}Widget extends cmsWidget {
|
|
36
|
+
|
|
37
|
+
public $is_cacheable = ${options_config.with_cache};
|
|
38
|
+
|
|
39
|
+
public function run() {
|
|
40
|
+
$options = $this->getOptions([
|
|
41
|
+
${optionDefaults}
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
$data = [
|
|
45
|
+
'widget' => $this,
|
|
46
|
+
'options' => $options,
|
|
47
|
+
'title' => $options['title'],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return $this->renderTemplate('${widget}', $data);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public function getCacheKey() {
|
|
54
|
+
return [
|
|
55
|
+
cmsConfig::get('cur_lang'),
|
|
56
|
+
$this->getOption('widget_id'),
|
|
57
|
+
$this->getOption('page'),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function validateOptions($options) {
|
|
62
|
+
return $options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public function getSizeOptions() {
|
|
66
|
+
return [
|
|
67
|
+
'full' => 'Во всю ширину',
|
|
68
|
+
'half' => 'Половина ширины',
|
|
69
|
+
'third' => 'Треть ширины',
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generates widget options class
|
|
76
|
+
*/
|
|
77
|
+
function generateWidgetOptions(widget, Widget, options) {
|
|
78
|
+
const fieldsCode = options
|
|
79
|
+
.map(o => {
|
|
80
|
+
switch (o.type) {
|
|
81
|
+
case 'text':
|
|
82
|
+
return ` $form->addField('${o.name}', new fieldString('${o.label}'));`;
|
|
83
|
+
case 'number':
|
|
84
|
+
return ` $form->addField('${o.name}', new fieldNumber('${o.label}'));`;
|
|
85
|
+
case 'select': {
|
|
86
|
+
const optsStr = (o.options || [])
|
|
87
|
+
.map(opt => `'${opt.value}' => '${opt.label}'`)
|
|
88
|
+
.join(', ');
|
|
89
|
+
return ` $form->addField('${o.name}', new fieldList('${o.label}', ['options' => [${optsStr}]]));`;
|
|
90
|
+
}
|
|
91
|
+
case 'checkbox':
|
|
92
|
+
return ` $form->addField('${o.name}', new fieldCheckbox('${o.label}'));`;
|
|
93
|
+
case 'textarea':
|
|
94
|
+
return ` $form->addField('${o.name}', new fieldText('${o.label}'));`;
|
|
95
|
+
case 'image':
|
|
96
|
+
return ` $form->addField('${o.name}', new fieldImage('${o.label}'));`;
|
|
97
|
+
default:
|
|
98
|
+
return ` $form->addField('${o.name}', new fieldString('${o.label}'));`;
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.join('\n');
|
|
102
|
+
return `<?php
|
|
103
|
+
// InstantCMS 2. widgets/${widget}.options.php
|
|
104
|
+
|
|
105
|
+
class ${Widget}WidgetOptions {
|
|
106
|
+
|
|
107
|
+
public static function getConfigForm($form) {
|
|
108
|
+
${fieldsCode}
|
|
109
|
+
|
|
110
|
+
return $form;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static function getDefaultOptions() {
|
|
114
|
+
return [
|
|
115
|
+
${options
|
|
116
|
+
.map(o => {
|
|
117
|
+
const defaultVal = typeof o.default === 'string' ? `'${o.default}'` : (o.default ?? 'null');
|
|
118
|
+
return ` '${o.name}' => ${defaultVal},`;
|
|
119
|
+
})
|
|
120
|
+
.join('\n')}
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public static function getOptionLabels() {
|
|
125
|
+
return [
|
|
126
|
+
${options.map(o => ` '${o.name}' => '${o.label}',`).join('\n')}
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Generates widget template
|
|
133
|
+
*/
|
|
134
|
+
function generateWidgetTemplate(widget, _Widget, _options) {
|
|
135
|
+
return `<?php
|
|
136
|
+
// InstantCMS 2. widgets/${widget}.html.php
|
|
137
|
+
|
|
138
|
+
\\$widget = \\$data['widget'];
|
|
139
|
+
\\$options = \\$data['options'];
|
|
140
|
+
\\$title = \\$options['title'] ?? '';
|
|
141
|
+
\\$limit = \\$options['limit'] ?? 5;
|
|
142
|
+
?>
|
|
143
|
+
|
|
144
|
+
<?php if (\\$title): ?>
|
|
145
|
+
<h3 class="widget-title"><?php echo \\$title; ?></h3>
|
|
146
|
+
<?php endif; ?>
|
|
147
|
+
|
|
148
|
+
<div class="${widget}-widget">
|
|
149
|
+
<div class="widget-content">
|
|
150
|
+
<!-- Widget content here -->
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<style>
|
|
155
|
+
.widget-${widget} {
|
|
156
|
+
padding: 0;
|
|
157
|
+
}
|
|
158
|
+
.widget-${widget} .widget-title {
|
|
159
|
+
margin: 0 0 15px 0;
|
|
160
|
+
font-size: 18px;
|
|
161
|
+
}
|
|
162
|
+
.widget-${widget} .widget-content {
|
|
163
|
+
padding: 10px 0;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Generates widget CSS styles
|
|
170
|
+
*/
|
|
171
|
+
function generateWidgetStyles(widget, _options) {
|
|
172
|
+
return `/* InstantCMS 2. ${widget} widget styles */
|
|
173
|
+
|
|
174
|
+
.${widget}-widget {
|
|
175
|
+
padding: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.${widget}-widget .widget-title {
|
|
179
|
+
margin: 0 0 15px 0;
|
|
180
|
+
font-size: 18px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.${widget}-widget .widget-content {
|
|
185
|
+
padding: 10px 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.${widget}-widget .item {
|
|
189
|
+
padding: 8px 0;
|
|
190
|
+
border-bottom: 1px solid #eee;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.${widget}-widget .item:last-child {
|
|
194
|
+
border-bottom: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.${widget}-widget .item-title {
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.${widget}-widget .item-date {
|
|
202
|
+
font-size: 12px;
|
|
203
|
+
color: #999;
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Generates widget configuration
|
|
209
|
+
*/
|
|
210
|
+
function generateWidgetConfig(name, widget, Widget, options) {
|
|
211
|
+
return `<?php
|
|
212
|
+
// InstantCMS 2. system/config/widgets/${widget}.php
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
'widget' => '${widget}',
|
|
216
|
+
'component' => '${name}',
|
|
217
|
+
'class' => '${Widget}Widget',
|
|
218
|
+
'options_class' => '${Widget}WidgetOptions',
|
|
219
|
+
'title' => '${Widget}',
|
|
220
|
+
'description' => '',
|
|
221
|
+
'version' => '1.0.0',
|
|
222
|
+
'author' => '',
|
|
223
|
+
'options' => [
|
|
224
|
+
${options.map(o => ` '${o.name}' => '${o.label}',`).join('\n')}
|
|
225
|
+
],
|
|
226
|
+
'size_options' => [
|
|
227
|
+
'full',
|
|
228
|
+
'half',
|
|
229
|
+
'third',
|
|
230
|
+
],
|
|
231
|
+
];`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Generates a complete widget for InstantCMS
|
|
235
|
+
*
|
|
236
|
+
* @param opts - Configuration options for the widget
|
|
237
|
+
* @returns Object containing generated files and metadata
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const result = scaffoldWidget({
|
|
242
|
+
* addon_name: 'blog',
|
|
243
|
+
* widget_name: 'recent_posts',
|
|
244
|
+
* options: [
|
|
245
|
+
* { name: 'title', type: 'text', label: 'Заголовок', default: 'Последние записи' },
|
|
246
|
+
* { name: 'limit', type: 'number', label: 'Количество', default: 5 }
|
|
247
|
+
* ]
|
|
248
|
+
* });
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
function scaffoldWidget(opts) {
|
|
252
|
+
const { lowercase } = (0, scaffold_1.normalizeAddonName)(opts.addon_name);
|
|
253
|
+
const widget = opts.widget_name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
254
|
+
const Widget = widget
|
|
255
|
+
.split('_')
|
|
256
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
257
|
+
.join('');
|
|
258
|
+
const files = {};
|
|
259
|
+
const options_config = {
|
|
260
|
+
with_template: opts.options_config?.with_template ?? true,
|
|
261
|
+
with_styles: opts.options_config?.with_styles ?? true,
|
|
262
|
+
with_cache: opts.options_config?.with_cache ?? true,
|
|
263
|
+
};
|
|
264
|
+
const options = opts.options || [
|
|
265
|
+
{ name: 'title', type: 'text', label: 'Заголовок', default: '' },
|
|
266
|
+
{ name: 'limit', type: 'number', label: 'Количество', default: 5 },
|
|
267
|
+
];
|
|
268
|
+
files[`${lowercase}/widgets/${widget}.php`] = generateWidgetClass(lowercase, widget, Widget, options, options_config);
|
|
269
|
+
files[`${lowercase}/widgets/${widget}.options.php`] = generateWidgetOptions(widget, Widget, options);
|
|
270
|
+
files[`${lowercase}/widgets/${widget}.html.php`] = generateWidgetTemplate(widget, Widget, options);
|
|
271
|
+
if (options_config.with_styles) {
|
|
272
|
+
files[`${lowercase}/widgets/${widget}.css`] = generateWidgetStyles(widget, options);
|
|
273
|
+
}
|
|
274
|
+
files[`system/config/widgets/${widget}.php`] = generateWidgetConfig(lowercase, widget, Widget, options);
|
|
275
|
+
return {
|
|
276
|
+
addon_name: lowercase,
|
|
277
|
+
files,
|
|
278
|
+
widget_name: widget,
|
|
279
|
+
options_count: options.length,
|
|
280
|
+
options_config,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
exports.widgetToolSchema = {
|
|
284
|
+
name: 'scaffold_widget',
|
|
285
|
+
description: 'Генерация виджета InstantCMS с настройками и шаблонами',
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
addon_name: { type: 'string', description: 'Имя компонента' },
|
|
290
|
+
widget_name: { type: 'string', description: 'Имя виджета' },
|
|
291
|
+
options: zod_1.z
|
|
292
|
+
.array(zod_1.z.object({
|
|
293
|
+
name: zod_1.z.string().describe('Имя опции'),
|
|
294
|
+
type: zod_1.z
|
|
295
|
+
.enum(['text', 'number', 'select', 'checkbox', 'textarea', 'image'])
|
|
296
|
+
.describe('Тип поля'),
|
|
297
|
+
label: zod_1.z.string().describe('Название'),
|
|
298
|
+
options: zod_1.z
|
|
299
|
+
.array(zod_1.z.object({ value: zod_1.z.string(), label: zod_1.z.string() }))
|
|
300
|
+
.optional()
|
|
301
|
+
.describe('Опции для select'),
|
|
302
|
+
default: zod_1.z
|
|
303
|
+
.union([zod_1.z.string(), zod_1.z.boolean(), zod_1.z.number()])
|
|
304
|
+
.optional()
|
|
305
|
+
.describe('Значение по умолчанию'),
|
|
306
|
+
}))
|
|
307
|
+
.optional()
|
|
308
|
+
.describe('Опции виджета'),
|
|
309
|
+
options_config: zod_1.z
|
|
310
|
+
.object({
|
|
311
|
+
with_template: zod_1.z.boolean().optional().describe('С шаблоном'),
|
|
312
|
+
with_styles: zod_1.z.boolean().optional().describe('Со стилями'),
|
|
313
|
+
with_cache: zod_1.z.boolean().optional().describe('С кэшированием'),
|
|
314
|
+
})
|
|
315
|
+
.optional()
|
|
316
|
+
.describe('Конфигурация'),
|
|
317
|
+
},
|
|
318
|
+
required: ['addon_name', 'widget_name'],
|
|
319
|
+
},
|
|
320
|
+
inputExamples: [
|
|
321
|
+
{
|
|
322
|
+
addon_name: 'blog',
|
|
323
|
+
widget_name: 'recent_posts',
|
|
324
|
+
options: [
|
|
325
|
+
{ name: 'title', type: 'text', label: 'Заголовок', default: 'Последние записи' },
|
|
326
|
+
{ name: 'limit', type: 'number', label: 'Количество', default: 5 },
|
|
327
|
+
{ name: 'show_date', type: 'checkbox', label: 'Показывать дату', default: true },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listWysiwygEditors = listWysiwygEditors;
|
|
4
|
+
exports.getWysiwygEditor = getWysiwygEditor;
|
|
5
|
+
exports.getWysiwygOptions = getWysiwygOptions;
|
|
6
|
+
exports.getWysiwygPlugins = getWysiwygPlugins;
|
|
7
|
+
exports.searchWysiwygEditors = searchWysiwygEditors;
|
|
8
|
+
exports.getWysiwygButtons = getWysiwygButtons;
|
|
9
|
+
const wysiwyg_map_js_1 = require("../data/wysiwyg-map.js");
|
|
10
|
+
function listWysiwygEditors() {
|
|
11
|
+
return {
|
|
12
|
+
total: wysiwyg_map_js_1.wysiwygMap.editorCount,
|
|
13
|
+
editors: wysiwyg_map_js_1.wysiwygMap.editors.map(e => ({
|
|
14
|
+
name: e.name,
|
|
15
|
+
className: e.className,
|
|
16
|
+
description: e.description,
|
|
17
|
+
features: e.features,
|
|
18
|
+
plugins_count: e.plugins?.length ?? 0,
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function getWysiwygEditor(editorName) {
|
|
23
|
+
const editor = wysiwyg_map_js_1.wysiwygMap.byName[editorName.toLowerCase()];
|
|
24
|
+
if (!editor) {
|
|
25
|
+
const similar = Object.keys(wysiwyg_map_js_1.wysiwygMap.byName).filter(name => name.includes(editorName.toLowerCase()));
|
|
26
|
+
return {
|
|
27
|
+
error: `Редактор "${editorName}" не найден`,
|
|
28
|
+
available: Object.keys(wysiwyg_map_js_1.wysiwygMap.byName),
|
|
29
|
+
similar: similar.length > 0 ? similar : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
name: editor.name,
|
|
34
|
+
className: editor.className,
|
|
35
|
+
filePath: editor.filePath,
|
|
36
|
+
optionsFormPath: editor.optionsFormPath,
|
|
37
|
+
description: editor.description,
|
|
38
|
+
features: editor.features,
|
|
39
|
+
options: editor.options,
|
|
40
|
+
options_descriptions: editor.optionsDescriptions,
|
|
41
|
+
plugins: editor.plugins,
|
|
42
|
+
buttons: editor.buttons,
|
|
43
|
+
usage: {
|
|
44
|
+
instantiation: `cmsWysiwyg::getEditor('${editor.name}', $config);`,
|
|
45
|
+
display: `$editor = cmsWysiwyg::getEditor('${editor.name}');\n$editor->displayEditor('content', $content);`,
|
|
46
|
+
options_form: editor.optionsFormPath,
|
|
47
|
+
},
|
|
48
|
+
example_config: buildExampleConfig(editor),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function getWysiwygOptions(editorName) {
|
|
52
|
+
const editor = wysiwyg_map_js_1.wysiwygMap.byName[editorName.toLowerCase()];
|
|
53
|
+
if (!editor) {
|
|
54
|
+
return { error: `Редактор "${editorName}" не найден` };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
editor: editor.name,
|
|
58
|
+
options: Object.entries(editor.optionsDescriptions).map(([key, opt]) => ({
|
|
59
|
+
name: key,
|
|
60
|
+
type: opt.type,
|
|
61
|
+
description: opt.description,
|
|
62
|
+
default: opt.default,
|
|
63
|
+
current: editor.options[key],
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function getWysiwygPlugins(editorName) {
|
|
68
|
+
const editor = wysiwyg_map_js_1.wysiwygMap.byName[editorName.toLowerCase()];
|
|
69
|
+
if (!editor) {
|
|
70
|
+
return { error: `Редактор "${editorName}" не найден` };
|
|
71
|
+
}
|
|
72
|
+
if (!editor.plugins) {
|
|
73
|
+
return {
|
|
74
|
+
editor: editor.name,
|
|
75
|
+
plugins: [],
|
|
76
|
+
note: `${editor.name} не поддерживает плагины`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
editor: editor.name,
|
|
81
|
+
plugins: editor.plugins,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function searchWysiwygEditors(query) {
|
|
85
|
+
const lower = query.toLowerCase();
|
|
86
|
+
const results = wysiwyg_map_js_1.wysiwygMap.editors.filter(e => e.name.includes(lower) ||
|
|
87
|
+
e.description.toLowerCase().includes(lower) ||
|
|
88
|
+
e.features.some(f => f.toLowerCase().includes(lower)) ||
|
|
89
|
+
e.plugins?.some(p => p.name.includes(lower) || p.description.toLowerCase().includes(lower)));
|
|
90
|
+
return {
|
|
91
|
+
query,
|
|
92
|
+
total: results.length,
|
|
93
|
+
results: results.map(e => ({
|
|
94
|
+
name: e.name,
|
|
95
|
+
description: e.description,
|
|
96
|
+
matching_features: e.features.filter(f => f.toLowerCase().includes(lower)),
|
|
97
|
+
matching_plugins: e.plugins?.filter(p => p.name.includes(lower) || p.description.toLowerCase().includes(lower)) ?? [],
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function getWysiwygButtons(editorName) {
|
|
102
|
+
const editor = wysiwyg_map_js_1.wysiwygMap.byName[editorName.toLowerCase()];
|
|
103
|
+
if (!editor) {
|
|
104
|
+
return { error: `Редактор "${editorName}" не найден` };
|
|
105
|
+
}
|
|
106
|
+
if (editorName === 'markitup' && editor.buttons) {
|
|
107
|
+
return {
|
|
108
|
+
editor: editor.name,
|
|
109
|
+
type: 'markitup_buttons',
|
|
110
|
+
buttons: editor.buttons.map((btn, index) => ({
|
|
111
|
+
id: index,
|
|
112
|
+
...(typeof btn === 'string' ? { name: btn } : btn),
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (editor.buttons && editor.buttons.every(b => typeof b === 'string')) {
|
|
117
|
+
return {
|
|
118
|
+
editor: editor.name,
|
|
119
|
+
type: 'toolbar_buttons',
|
|
120
|
+
buttons: editor.buttons,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
editor: editor.name,
|
|
125
|
+
buttons: editor.buttons ?? [],
|
|
126
|
+
note: `${editor.name} не имеет списка кнопок`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function buildExampleConfig(editor) {
|
|
130
|
+
const example = {};
|
|
131
|
+
Object.entries(editor.optionsDescriptions).forEach(([key, opt]) => {
|
|
132
|
+
if (opt.default !== undefined) {
|
|
133
|
+
example[key] = opt.default;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return example;
|
|
137
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createError = createError;
|
|
4
|
+
exports.createSuccess = createSuccess;
|
|
5
|
+
exports.validateAddonName = validateAddonName;
|
|
6
|
+
exports.normalizeAddonName = normalizeAddonName;
|
|
7
|
+
exports.sanitizePath = sanitizePath;
|
|
8
|
+
exports.getCurrentDate = getCurrentDate;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a standardized error response
|
|
11
|
+
*/
|
|
12
|
+
function createError(code, message, details) {
|
|
13
|
+
return {
|
|
14
|
+
success: false,
|
|
15
|
+
error: { code, message, details },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a standardized success response
|
|
20
|
+
*/
|
|
21
|
+
function createSuccess(data) {
|
|
22
|
+
return {
|
|
23
|
+
success: true,
|
|
24
|
+
data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates addon name format
|
|
29
|
+
*/
|
|
30
|
+
function validateAddonName(name) {
|
|
31
|
+
if (!name) {
|
|
32
|
+
return { valid: false, error: 'Addon name is required' };
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
error: 'Addon name must start with lowercase letter and contain only lowercase letters, numbers, and underscores',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (name.length > 50) {
|
|
41
|
+
return { valid: false, error: 'Addon name must be 50 characters or less' };
|
|
42
|
+
}
|
|
43
|
+
return { valid: true };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Converts addon name to various case formats
|
|
47
|
+
*/
|
|
48
|
+
function normalizeAddonName(name) {
|
|
49
|
+
const lowercase = name.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
50
|
+
const UpperCamelCase = lowercase
|
|
51
|
+
.split('_')
|
|
52
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
53
|
+
.join('');
|
|
54
|
+
const lowercaseSnake = lowercase.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
55
|
+
return { lowercase, UpperCamelCase, lowercaseSnake };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sanitizes a string for use in file paths
|
|
59
|
+
*/
|
|
60
|
+
function sanitizePath(str) {
|
|
61
|
+
return str.replace(/[^a-zA-Z0-9_\/-]/g, '_').replace(/__+/g, '_');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generates a timestamp for use in version strings
|
|
65
|
+
*/
|
|
66
|
+
function getCurrentDate() {
|
|
67
|
+
return new Date().toISOString().split('T')[0];
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineTool = defineTool;
|
|
4
|
+
exports.defineToolWithManualResult = defineToolWithManualResult;
|
|
5
|
+
const mcp_result_js_1 = require("./mcp-result.js");
|
|
6
|
+
/**
|
|
7
|
+
* Регистрирует MCP-инструмент и нормализует контракт ответа.
|
|
8
|
+
*
|
|
9
|
+
* Любое исключение из handler превращается в errorResult (isError: true,
|
|
10
|
+
* code: TOOL_EXECUTION_ERROR), не пробрасывается наружу. Это сохраняет
|
|
11
|
+
* инвариант MCP: каждый tools/call возвращает корректный результат.
|
|
12
|
+
*
|
|
13
|
+
* Schema намеренно типизирована как `unknown`, чтобы не воевать с zod v4 ↔
|
|
14
|
+
* MCP SDK generic-конфликтами; контракт реэкспортируется из sdk в site-вызова.
|
|
15
|
+
*/
|
|
16
|
+
function defineTool(server, name, description, schema, handler) {
|
|
17
|
+
server.tool(name, description, schema, async (rawArgs) => {
|
|
18
|
+
try {
|
|
19
|
+
const data = await handler((rawArgs ?? {}));
|
|
20
|
+
return (0, mcp_result_js_1.successResult)(data);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
return (0, mcp_result_js_1.errorResult)('TOOL_EXECUTION_ERROR', message, {
|
|
25
|
+
tool: name,
|
|
26
|
+
cause: err instanceof Error ? err.stack : undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Вариант, когда handler сам решает вернуть success- или error-result
|
|
33
|
+
* (например, для ожидаемых отказов вроде "unknown lookup").
|
|
34
|
+
*/
|
|
35
|
+
function defineToolWithManualResult(server, name, description, schema, handler) {
|
|
36
|
+
server.tool(name, description, schema, handler);
|
|
37
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Matching логика для find_tool: token-based matching с ранжированием.
|
|
4
|
+
*
|
|
5
|
+
* Приоритет:
|
|
6
|
+
* 1. exact match ключевого слова (category.keywords.includes(query.normalized))
|
|
7
|
+
* 2. prefix match (keyword.startsWith(queryToken) || queryToken.startsWith(keyword))
|
|
8
|
+
* 3. substring match (keyword.includes(queryToken) || queryToken.includes(keyword))
|
|
9
|
+
*
|
|
10
|
+
* Запрос токенизируется по Unicode-словным границам; multi-word запросы
|
|
11
|
+
* матчатся, если ВСЕ токены попадают в keywords одной категории.
|
|
12
|
+
* Возвращает категории, отсортированные по score desc, плюс дополнительные
|
|
13
|
+
* метрики.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.rankToolCategories = rankToolCategories;
|
|
17
|
+
exports.findToolCategories = findToolCategories;
|
|
18
|
+
const MAX_TOKEN_LENGTH = 64;
|
|
19
|
+
function normalize(s) {
|
|
20
|
+
return s.toLowerCase().trim();
|
|
21
|
+
}
|
|
22
|
+
function tokenize(s) {
|
|
23
|
+
const norm = normalize(s);
|
|
24
|
+
// Unicode-словные границы, фильтр пустых и длинных токенов.
|
|
25
|
+
return norm
|
|
26
|
+
.split(/[\s,.!?;:"'()[\]{}<>|/\\\-_+=]+/u)
|
|
27
|
+
.filter(t => t.length > 0 && t.length <= MAX_TOKEN_LENGTH);
|
|
28
|
+
}
|
|
29
|
+
function scoreCategory(queryTokens, category) {
|
|
30
|
+
if (queryTokens.length === 0 || category.keywords.length === 0) {
|
|
31
|
+
return { score: 0, matched: [] };
|
|
32
|
+
}
|
|
33
|
+
const keywords = category.keywords.map(normalize);
|
|
34
|
+
const matched = [];
|
|
35
|
+
let totalScore = 0;
|
|
36
|
+
for (const token of queryTokens) {
|
|
37
|
+
let bestScore = 0;
|
|
38
|
+
let matchedKeyword = null;
|
|
39
|
+
for (const keyword of keywords) {
|
|
40
|
+
let s = 0;
|
|
41
|
+
if (keyword === token) {
|
|
42
|
+
s = 100;
|
|
43
|
+
}
|
|
44
|
+
else if (keyword.startsWith(token) || token.startsWith(keyword)) {
|
|
45
|
+
s = 60;
|
|
46
|
+
}
|
|
47
|
+
else if (keyword.includes(token) || token.includes(keyword)) {
|
|
48
|
+
s = 30;
|
|
49
|
+
}
|
|
50
|
+
if (s > bestScore) {
|
|
51
|
+
bestScore = s;
|
|
52
|
+
matchedKeyword = keyword;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (bestScore > 0) {
|
|
56
|
+
totalScore += bestScore;
|
|
57
|
+
if (matchedKeyword)
|
|
58
|
+
matched.push(matchedKeyword);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Multi-word: matched at least once per token.
|
|
62
|
+
if (matched.length < queryTokens.length) {
|
|
63
|
+
// Partial match: даём меньший score, чтобы категории с полным совпадением
|
|
64
|
+
// были выше.
|
|
65
|
+
totalScore = Math.floor(totalScore * 0.5);
|
|
66
|
+
}
|
|
67
|
+
return { score: totalScore, matched };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Ранжирует категории по релевантности запросу. Возвращает не более maxResults,
|
|
71
|
+
* отсортированных по score desc. Категории с score=0 НЕ включаются.
|
|
72
|
+
*/
|
|
73
|
+
function rankToolCategories(query, categories, maxResults = 5) {
|
|
74
|
+
const tokens = tokenize(query);
|
|
75
|
+
if (tokens.length === 0)
|
|
76
|
+
return [];
|
|
77
|
+
return categories
|
|
78
|
+
.map(category => {
|
|
79
|
+
const { score, matched } = scoreCategory(tokens, category);
|
|
80
|
+
return { category: category.category, tools: category.tools, score, matchedTokens: matched };
|
|
81
|
+
})
|
|
82
|
+
.filter(result => result.score > 0)
|
|
83
|
+
.sort((a, b) => b.score - a.score)
|
|
84
|
+
.slice(0, maxResults);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Удобная обёртка: вернуть список объектов ToolCategory в исходном формате,
|
|
88
|
+
* отсортированных по релевантности. Пустой результат → полный каталог.
|
|
89
|
+
*/
|
|
90
|
+
function findToolCategories(query, categories, fallbackAll = true) {
|
|
91
|
+
const ranked = rankToolCategories(query, categories);
|
|
92
|
+
const matches = categories.filter(c => ranked.some(r => r.category === c.category));
|
|
93
|
+
if (matches.length === 0 && fallbackAll) {
|
|
94
|
+
return { matches: categories, ranked: [] };
|
|
95
|
+
}
|
|
96
|
+
return { matches, ranked };
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.successResult = successResult;
|
|
4
|
+
exports.errorResult = errorResult;
|
|
5
|
+
function successResult(data, summary) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: 'text', text: summary ?? JSON.stringify(data, null, 2) }],
|
|
8
|
+
structuredContent: data,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function errorResult(code, message, details = {}) {
|
|
12
|
+
const data = { code, message, ...details };
|
|
13
|
+
return {
|
|
14
|
+
isError: true,
|
|
15
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
16
|
+
structuredContent: data,
|
|
17
|
+
};
|
|
18
|
+
}
|