dsh-edt-projects 0.9.1 → 0.11.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/README.md +13 -4
- package/cordis.patch.yml +36 -11
- package/lib/config.mjs +12 -1
- package/lib/enable.mjs +91 -0
- package/lib/index.js +248 -115
- package/lib/schema.mjs +16 -5
- package/lib/skills.mjs +15 -3
- package/locale/en.json +1 -1
- package/locale/ru.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
по требованию, добавляет настройки на проект, правила для модели и навыки
|
|
7
7
|
SonarQube/GitLab.
|
|
8
8
|
|
|
9
|
+
Плагин включается только в выбранных проектах: сессия в каталоге с
|
|
10
|
+
`.dsh-1c.json` или в пути из `projects` файла `$DSH_HOME/dsh-1c.json`. В
|
|
11
|
+
остальных проектах в контексте остаётся только `edt_config`, которым проект и
|
|
12
|
+
включается.
|
|
13
|
+
|
|
9
14
|
## Установка
|
|
10
15
|
|
|
11
16
|
```text
|
|
@@ -18,11 +23,15 @@ plugin_manager { action: "install_bundle", target: "dsh-edt-projects" }
|
|
|
18
23
|
|
|
19
24
|
## Что внутри
|
|
20
25
|
|
|
21
|
-
- `lib/` — хаб MCP-серверов:
|
|
22
|
-
проект, правила для системного промпта, провайдер
|
|
26
|
+
- `lib/` — хаб MCP-серверов: активация по проекту, монтаж по требованию
|
|
27
|
+
(`edt_pin`), настройки на проект, правила для системного промпта, провайдер
|
|
28
|
+
навыков.
|
|
23
29
|
- `skills/` — навыки `edt-mcp-project-sonar` и `edt-mcp-project-gitlab`,
|
|
24
|
-
включаются флагами `sonar.enabled` и `gitlab.enabled
|
|
25
|
-
|
|
30
|
+
включаются флагами `sonar.enabled` и `gitlab.enabled` внутри включённого
|
|
31
|
+
проекта.
|
|
32
|
+
- `cordis.patch.yml` — состав серверов, который применяет менеджер плагинов:
|
|
33
|
+
EDT, BSL Language Server по требованию, MCP-трекер и `v8std` (стандарты 1С и
|
|
34
|
+
расшифровка диагностик EDT/BSL).
|
|
26
35
|
|
|
27
36
|
## Лицензия
|
|
28
37
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
# Хаб MCP-серверов для разработки на 1С.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# -
|
|
5
|
-
# -
|
|
6
|
-
# - `
|
|
7
|
-
#
|
|
8
|
-
#
|
|
3
|
+
# Плагин стоит в профиле один раз, но активируется только в выбранных проектах:
|
|
4
|
+
# - сессия в каталоге с `.dsh-1c.json` — включена;
|
|
5
|
+
# - каталог добавлен в `projects` файла $DSH_HOME/dsh-1c.json — включена;
|
|
6
|
+
# - `enabled: true`/`enabled: false` — явное решение сильнее всего.
|
|
7
|
+
# В выключенном проекте виден только `edt_config`; в остальных сессиях ни правил
|
|
8
|
+
# 1С, ни MCP-серверов нет.
|
|
9
|
+
#
|
|
10
|
+
# Серверы описываются здесь и подключаются плагином mcp-hub (lib/index.js) в
|
|
11
|
+
# скоупе сессии:
|
|
12
|
+
# - `lazy: true` — не занимать контекст: сервер монтируется по вызову edt_pin
|
|
13
|
+
# - `provision: true` — параметры запуска добираются на edt_pin (BSL LS скачивается)
|
|
14
|
+
# - `unavailable` — сервер описан, но настроить его нечем (edt_pin объяснит, чем)
|
|
15
|
+
# `shared` больше не работает: иначе инструменты сервера протекали бы в чужие
|
|
16
|
+
# проекты.
|
|
9
17
|
#
|
|
10
18
|
# Настройки можно переопределить без перезапуска DSH — файлами
|
|
11
19
|
# $DSH_HOME/dsh-1c.json (пользователь) и <cwd>/.dsh-1c.json (проект):
|
|
12
|
-
# { "
|
|
20
|
+
# { "projects": ["C:\\git\\trade"],
|
|
21
|
+
# "wiki": { "paths": ["C:\\git\\dev-llm-wiki"], "entry": "wiki/index.md" },
|
|
13
22
|
# "servers": { "edt": { "url": "http://127.0.0.1:8765/mcp" },
|
|
14
23
|
# "bsl": { "root": "C:\\git\\trade\\src\\cf" } },
|
|
15
24
|
# "tests": { "yaxunit": { "enabled": true, "path": "tests/yaxunit", "project": "cfe_yaxunit" },
|
|
16
25
|
# "vanessa": { "enabled": true, "path": "features" } } }
|
|
17
|
-
# После правки файла:
|
|
26
|
+
# После правки файла: edt_reload (или edt_pin заново).
|
|
18
27
|
# Флаги tests включают правила тестируемости: YAxUnit — закладывать тестируемость
|
|
19
28
|
# и писать тесты в указанный проект, Vanessa — предлагать сценарные тесты.
|
|
20
29
|
#
|
|
@@ -36,6 +45,8 @@
|
|
|
36
45
|
# DSH_1C_BSL_MCP_URL адрес уже запущенного BSL LS по Streamable HTTP
|
|
37
46
|
# TASKS_MCP_URL адрес MCP-сервера системы задач
|
|
38
47
|
# TASKS_MCP_TOKEN токен системы задач
|
|
48
|
+
# V8STD_MCP_URL адрес MCP-сервера стандартов (по умолчанию https://ai.v8std.ru/mcp)
|
|
49
|
+
# V8STD_MCP_TOKEN токен своего инстанса v8std, если он нужен
|
|
39
50
|
- insert:
|
|
40
51
|
- id: mcp-hub
|
|
41
52
|
name: 'dsh-edt-projects'
|
|
@@ -44,9 +55,9 @@
|
|
|
44
55
|
paths: !!js 'process.env.DSH_1C_WIKI ? process.env.DSH_1C_WIKI.split(";").filter(Boolean) : []'
|
|
45
56
|
entry: !!js 'process.env.DSH_1C_WIKI_ENTRY ?? ""'
|
|
46
57
|
servers:
|
|
47
|
-
# EDT
|
|
58
|
+
# EDT монтируется в каждой включённой сессии: это отдельное HTTP-
|
|
59
|
+
# подключение, а не отдельный workspace в EDT.
|
|
48
60
|
edt:
|
|
49
|
-
shared: true
|
|
50
61
|
lazy: false
|
|
51
62
|
transport: streamable-http
|
|
52
63
|
url: !!js 'process.env.EDT_MCP_URL ?? "http://127.0.0.1:8765/mcp"'
|
|
@@ -62,7 +73,7 @@
|
|
|
62
73
|
maxAttempts: 1000
|
|
63
74
|
|
|
64
75
|
# BSL Language Server: 10 инструментов, нужны эпизодически.
|
|
65
|
-
# provision: на
|
|
76
|
+
# provision: на edt_pin берётся готовый jar из DSH_1C_BSL_LS_JAR, иначе из
|
|
66
77
|
# кэша, иначе скачивается последний релиз с GitHub (sha256 сверяется).
|
|
67
78
|
# Корень по умолчанию — servers.bsl.root из файла настроек проекта.
|
|
68
79
|
bsl:
|
|
@@ -81,3 +92,17 @@
|
|
|
81
92
|
headers: !!js 'process.env.TASKS_MCP_TOKEN ? { Authorization: `Bearer ${process.env.TASKS_MCP_TOKEN}` } : {}'
|
|
82
93
|
toolCallTimeoutMs: 120000
|
|
83
94
|
failOnStartupError: false
|
|
95
|
+
|
|
96
|
+
# Стандарты 1С и диагностики: публичная read-only база v8std.
|
|
97
|
+
# lazy: пять схем не занимают контекст, пока не нужны. Модель подключает
|
|
98
|
+
# сервер по edt_pin, когда EDT или BSL LS выдал код ошибки либо задан
|
|
99
|
+
# вопрос про стандарт.
|
|
100
|
+
# Свой инстанс: servers.v8std.url в файлах настроек (перекрывает это
|
|
101
|
+
# значение) или переменная V8STD_MCP_URL.
|
|
102
|
+
v8std:
|
|
103
|
+
lazy: true
|
|
104
|
+
transport: streamable-http
|
|
105
|
+
url: !!js 'process.env.V8STD_MCP_URL ?? "https://ai.v8std.ru/mcp"'
|
|
106
|
+
headers: !!js 'process.env.V8STD_MCP_TOKEN ? { Authorization: `Bearer ${process.env.V8STD_MCP_TOKEN}` } : {}'
|
|
107
|
+
toolCallTimeoutMs: 60000
|
|
108
|
+
failOnStartupError: false
|
package/lib/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Слои настроек бандла: база из патча, пользовательский файл и файл проекта.
|
|
2
2
|
//
|
|
3
3
|
// Приоритет: проект > пользователь > база. Файлы перечитываются на каждый
|
|
4
|
-
//
|
|
4
|
+
// edt_pin/edt_reload и на каждый рендер промпта, поэтому правки применяются
|
|
5
5
|
// без перезапуска DSH; разбор кэшируется по mtimeMs.
|
|
6
6
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
@@ -92,6 +92,17 @@ export function resolveConfig({ cwd, env = process.env, base = {} } = {}) {
|
|
|
92
92
|
return { config: mergeLayers(base, user.config, project.config), sources, errors };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Есть ли у проекта разбираемый файл `.dsh-1c.json`.
|
|
97
|
+
* Наличие самого файла при включении плагина учитывается только вместе с
|
|
98
|
+
* успешным разбором: битый JSON не должен включать 1С-режим.
|
|
99
|
+
*/
|
|
100
|
+
export function projectConfigLoaded(cwd) {
|
|
101
|
+
const path = projectConfigPath(cwd);
|
|
102
|
+
if (path === undefined) return false;
|
|
103
|
+
return readJsonConfig(path).config !== undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
/** Сбрасывает кэш — используется тестами. */
|
|
96
107
|
export function clearConfigCache() {
|
|
97
108
|
cache.clear();
|
package/lib/enable.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Включён ли плагин для проекта сессии.
|
|
2
|
+
//
|
|
3
|
+
// Плагин установлен в профиль один раз, но его инструменты, правила, навыки и
|
|
4
|
+
// MCP-серверы нужны только там, где работают с 1С. Поэтому активация считается
|
|
5
|
+
// от исходного каталога сессии:
|
|
6
|
+
//
|
|
7
|
+
// 1. `enabled: true` — включён;
|
|
8
|
+
// 2. `enabled: false` — выключен (сильнее файла проекта и списка);
|
|
9
|
+
// 3. разбираемый `.dsh-1c.json` в каталоге проекта — включён;
|
|
10
|
+
// 4. каталог попадает в `projects` пользовательского файла — включён;
|
|
11
|
+
// 5. иначе выключен.
|
|
12
|
+
//
|
|
13
|
+
// Сравнение путей: абсолютные пути, `~` раскрывается, хвостовой разделитель
|
|
14
|
+
// снимается, существующие пути приводятся через realpath; на Windows регистр не
|
|
15
|
+
// важен. Совпадение — равенство или вложенность по границе каталога, поэтому
|
|
16
|
+
// `C:\git\trade2` не считается проектом `C:\git\trade`.
|
|
17
|
+
import { realpathSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
const implFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Приводит путь проекта к сравнимому виду.
|
|
25
|
+
* @param value путь из настроек или каталог сессии.
|
|
26
|
+
* @param options.platform платформа для правил путей (по умолчанию текущая).
|
|
27
|
+
* @param options.home домашний каталог для `~`.
|
|
28
|
+
* @returns нормализованный абсолютный путь или undefined, если путь не абсолютный.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeProjectPath(value, { platform = process.platform, home = homedir() } = {}) {
|
|
31
|
+
if (typeof value !== 'string') return undefined;
|
|
32
|
+
const impl = implFor(platform);
|
|
33
|
+
let text = value.trim();
|
|
34
|
+
if (text === '') return undefined;
|
|
35
|
+
|
|
36
|
+
if (text === '~') text = home;
|
|
37
|
+
else if (text.startsWith('~/') || text.startsWith('~\\')) text = impl.join(home, text.slice(2));
|
|
38
|
+
|
|
39
|
+
if (!impl.isAbsolute(text)) return undefined;
|
|
40
|
+
let resolved = impl.normalize(text);
|
|
41
|
+
|
|
42
|
+
// realpath имеет смысл только для реальной платформы: путь чужой платформы
|
|
43
|
+
// здесь не существует и был бы искажён.
|
|
44
|
+
if (platform === process.platform) {
|
|
45
|
+
try {
|
|
46
|
+
resolved = realpathSync.native(resolved);
|
|
47
|
+
} catch {
|
|
48
|
+
// Каталога может ещё не быть — сравниваем нормализованный путь.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Хвостовой разделитель не должен делать «C:\git\trade\» другим проектом.
|
|
53
|
+
// Корень диска и «/» сохраняются как есть.
|
|
54
|
+
const trimmed = resolved.replace(/[\\/]+$/, '');
|
|
55
|
+
if (trimmed !== '') resolved = trimmed;
|
|
56
|
+
if (platform === 'win32') resolved = resolved.toLowerCase();
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Совпадает ли каталог с записью списка проектов (сам каталог или его потомок). */
|
|
61
|
+
export function matchesProject(cwd, entry, options = {}) {
|
|
62
|
+
const platform = options.platform ?? process.platform;
|
|
63
|
+
const left = normalizeProjectPath(cwd, { ...options, platform });
|
|
64
|
+
const right = normalizeProjectPath(entry, { ...options, platform });
|
|
65
|
+
if (left === undefined || right === undefined) return false;
|
|
66
|
+
if (left === right) return true;
|
|
67
|
+
const impl = implFor(platform);
|
|
68
|
+
const base = right.endsWith(impl.sep) ? right : right + impl.sep;
|
|
69
|
+
return left.startsWith(base);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Решение об активации вместе с причиной — причина нужна edt_config show.
|
|
74
|
+
* @returns {{ enabled: boolean, reason: string, entry?: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function activationFor({ cwd, config, hasProjectFile = false } = {}, options = {}) {
|
|
77
|
+
const value = config ?? {};
|
|
78
|
+
if (value.enabled === true) return { enabled: true, reason: 'enabled' };
|
|
79
|
+
if (value.enabled === false) return { enabled: false, reason: 'disabled' };
|
|
80
|
+
if (hasProjectFile) return { enabled: true, reason: 'project-file' };
|
|
81
|
+
|
|
82
|
+
const projects = Array.isArray(value.projects) ? value.projects : [];
|
|
83
|
+
const hit = projects.find((entry) => matchesProject(cwd, entry, options));
|
|
84
|
+
if (hit !== undefined) return { enabled: true, reason: 'allowlist', entry: hit };
|
|
85
|
+
return { enabled: false, reason: 'not-selected' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Короткая форма activationFor для гейтов в рантайме. */
|
|
89
|
+
export function isEnabledFor(input, options = {}) {
|
|
90
|
+
return activationFor(input, options).enabled;
|
|
91
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
// Хаб MCP-серверов для DSH: подключение по требованию, настройки на проект
|
|
2
2
|
// и правила для модели.
|
|
3
3
|
//
|
|
4
|
+
// Плагин стоит в профиле один раз, но активируется только в выбранных проектах:
|
|
5
|
+
// правила 1С, собственные инструменты и MCP-серверы появляются лишь там, где
|
|
6
|
+
// проект отмечен `.dsh-1c.json`, `enabled: true` или попадает в `projects`
|
|
7
|
+
// пользовательского файла (см. lib/enable.mjs). В остальных сессиях виден
|
|
8
|
+
// только `edt_config`, которым проект и включается.
|
|
9
|
+
//
|
|
4
10
|
// Зачем: каждый подключённый сервер отдаёт свои схемы инструментов в КАЖДЫЙ
|
|
5
|
-
// запрос к модели. EDT (99 инструментов) нужен
|
|
6
|
-
// и система задач — эпизодически. Поэтому
|
|
7
|
-
//
|
|
11
|
+
// запрос к модели. EDT (99 инструментов) нужен постоянно в 1С-проекте, а BSL
|
|
12
|
+
// Language Server и система задач — эпизодически. Поэтому серверы монтируются в
|
|
13
|
+
// скоуп сессии: `lazy: true` — только по вызову `edt_pin`; незаленивые — при
|
|
14
|
+
// создании сессии включённого проекта. `edt_unpin` возвращает контекст.
|
|
8
15
|
//
|
|
9
16
|
// Настройки: база из cordis.patch.yml <- $DSH_HOME/dsh-1c.json <- <cwd>/.dsh-1c.json.
|
|
10
|
-
// Файлы перечитываются на каждый pin/рендер
|
|
11
|
-
// через
|
|
12
|
-
//
|
|
13
|
-
// Серверы описываются в config.servers (см. cordis.patch.yml):
|
|
14
|
-
// edt: { shared: true, transport: streamable-http, url, headers, lazy: false }
|
|
15
|
-
// bsl: { lazy: true, provision: true, root } — jar берётся из env/кэша
|
|
16
|
-
// tasks: { lazy: true, transport: streamable-http, url, headers }
|
|
17
|
+
// Файлы перечитываются на каждый pin/рендер промпта и на каждый шаг (для
|
|
18
|
+
// включения на лету), поэтому правки применяются через edt_reload без
|
|
19
|
+
// перезапуска DSH.
|
|
17
20
|
//
|
|
18
|
-
// `shared: true`
|
|
19
|
-
//
|
|
20
|
-
// получают каждый свой BSL LS со своим корнем.
|
|
21
|
+
// `shared: true` больше не работает: сервер всегда живёт в скоупе сессии, иначе
|
|
22
|
+
// его инструменты протекали бы в чужие проекты.
|
|
21
23
|
export const name = 'mcp-hub';
|
|
22
24
|
|
|
23
25
|
export const inject = ['tools', 'workingDirectory'];
|
|
24
26
|
|
|
27
|
+
/** Собственные инструменты плагина; префикс edt_ отделяет их от `mcp__edt__*`. */
|
|
28
|
+
export const TOOL_NAMES = ['edt_servers', 'edt_pin', 'edt_unpin', 'edt_reload', 'edt_config'];
|
|
29
|
+
|
|
25
30
|
import { watch } from 'node:fs';
|
|
26
31
|
import { bslAvailable, bslSettings, ensureJar, resolveBsl, startBslSupervisor } from './bsl.mjs';
|
|
27
|
-
import { projectConfigPath, resolveConfig, userConfigPath } from './config.mjs';
|
|
32
|
+
import { projectConfigLoaded, projectConfigPath, resolveConfig, userConfigPath } from './config.mjs';
|
|
33
|
+
import { activationFor } from './enable.mjs';
|
|
28
34
|
import { normalizeWiki, targetPathOf, writeDenialReason } from './wiki.mjs';
|
|
29
35
|
import { normalizeTestTooling } from './tests.mjs';
|
|
30
36
|
import { initFile, upgradeFile } from './config-file.mjs';
|
|
@@ -33,7 +39,7 @@ import { createSkillsProvider } from './skills.mjs';
|
|
|
33
39
|
|
|
34
40
|
const HEADER = `## Работа с 1С через MCP
|
|
35
41
|
|
|
36
|
-
К этому
|
|
42
|
+
К этому проекту подключены MCP-серверы для работы с конфигурацией 1С. Правила:
|
|
37
43
|
|
|
38
44
|
- Структуру, метаданные и код ищи через MCP, а не файловым поиском. Чтение файлов и \`grep\` — запасной путь, когда MCP-инструмент недоступен или уже вернул ошибку.
|
|
39
45
|
- Проверяй утверждения о коде тем же инструментом, которым исследовал. Не выдавай непроверенное за проверенное.
|
|
@@ -43,12 +49,12 @@ const HEADER = `## Работа с 1С через MCP
|
|
|
43
49
|
const PIN_RULES = `
|
|
44
50
|
### Серверы по требованию
|
|
45
51
|
|
|
46
|
-
Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`
|
|
52
|
+
Часть серверов не подключена заранее, чтобы не занимать контекст их схемами. Посмотреть состояние — \`edt_servers\`; подключить — \`edt_pin(server)\`, отключить — \`edt_unpin(server)\`.
|
|
47
53
|
|
|
48
|
-
- Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны задачи или статусы — \`tasks\`.
|
|
49
|
-
- Для \`bsl\` передавай корень: \`
|
|
50
|
-
- Первый \`
|
|
51
|
-
- После \`
|
|
54
|
+
- Подключай сервер сам, когда задача без него не решается: нужна диагностика или типы — \`bsl\`, нужны стандарты или расшифровка кода ошибки — \`v8std\`, нужны задачи или статусы — \`tasks\`.
|
|
55
|
+
- Для \`bsl\` передавай корень: \`edt_pin("bsl", root: "<каталог конфигурации>")\`. Без корня берётся корень из настроек проекта, иначе каталог запуска DSH.
|
|
56
|
+
- Первый \`edt_pin("bsl")\` может занять время: при отсутствии готового jar сервер скачивается (~130 МБ). Предупреди пользователя, если это долго.
|
|
57
|
+
- После \`edt_unpin\` инструменты сервера исчезают из запроса. Не отключай сервер, пока работа с ним не закончена.
|
|
52
58
|
- Если сервер помечен как не настроенный, не пытайся его подключить: скажи пользователю, чего не хватает.`;
|
|
53
59
|
|
|
54
60
|
const EDT_EDIT_RULES = `
|
|
@@ -82,7 +88,7 @@ const BSL_RULES = `
|
|
|
82
88
|
- \`type_info\` и \`global_member_info\` — свойства, методы и сигнатуры типов и глобального контекста платформы. Сверяйся с ними, а не по памяти.
|
|
83
89
|
- \`definition\`, \`find_references\`, \`call_hierarchy\`, \`hover\`, \`type_at_position\`, \`document_symbols\` — навигация. Перед изменением метода посмотри \`call_hierarchy\` или \`find_references\`.
|
|
84
90
|
- Позиции в \`line\`/\`character\` нумеруются с нуля.
|
|
85
|
-
- Файлы вне рабочего каталога сервера дают \`File is not part of any registered workspace\`. Тогда переподключи сервер с нужным корнем: \`
|
|
91
|
+
- Файлы вне рабочего каталога сервера дают \`File is not part of any registered workspace\`. Тогда переподключи сервер с нужным корнем: \`edt_pin("bsl", root: "<каталог конфигурации>")\` — путь проекта видно в \`edt__list_projects\`.`;
|
|
86
92
|
|
|
87
93
|
const TASKS_RULES = `
|
|
88
94
|
### Система задач (\`tasks__*\`)
|
|
@@ -90,6 +96,18 @@ const TASKS_RULES = `
|
|
|
90
96
|
- Задачи, их статусы и комментарии — только через \`tasks__*\`. Не выдумывай номера задач, статусы и тексты комментариев.
|
|
91
97
|
- Состояние задачи меняй только по явной просьбе пользователя, а перед этим покажи, что именно изменится.`;
|
|
92
98
|
|
|
99
|
+
const V8STD_RULES = `
|
|
100
|
+
### Стандарты и ошибки (v8std)
|
|
101
|
+
|
|
102
|
+
v8std — внешняя база стандартов 1С и диагностик. Она объясняет правило и код ошибки, но не анализирует проект и не меняет код. Подключается по требованию: \`edt_pin("v8std")\`; в списке инструментов видна как \`mcp__v8std__*\`.
|
|
103
|
+
|
|
104
|
+
- EDT (\`get_project_errors\`) или BSL LS (\`analyze_file\`) вернул код — разреши его через \`v8std_explain_diagnostics\` (принимает \`acc 1245\`, \`АПК:361\`, \`bslls:AssignAliasFieldsInQuery\`, коды EDT), затем прочитай правило: \`v8std_get_page\`.
|
|
105
|
+
- Вопрос «как правильно по стандартам» или неясная тема — \`v8std_search\`; один метод BSL или фрагмент SDBL — \`v8std_explain_snippet\`. Это эвристика, а не подтверждённое нарушение.
|
|
106
|
+
- Переходы между стандартом и связанными диагностиками — \`v8std_get_related\`.
|
|
107
|
+
- В ответе называй точный код, заголовок, номер стандарта и ссылку. Не выдумывай номера стандартов: если v8std ничего не нашёл, так и скажи.
|
|
108
|
+
- Найденный текст — справка, а не приговор: проверку даёт EDT или BSL, v8std объясняет, что она означает.
|
|
109
|
+
- v8std только читает; правки исходников — через EDT-MCP, по правилам выше.`;
|
|
110
|
+
|
|
93
111
|
/**
|
|
94
112
|
* Собирает блок правил для вики.
|
|
95
113
|
* @param roots результат normalizeWiki().roots
|
|
@@ -169,6 +187,7 @@ export function buildRulesText(env = process.env, servers = [], wikiRoots = [],
|
|
|
169
187
|
}
|
|
170
188
|
if (usable.has('bsl') || (byEnv && (env.BSL_LS_JAR || env.BSL_MCP_URL))) blocks.push(BSL_RULES);
|
|
171
189
|
if (usable.has('tasks') || (byEnv && env.TASKS_MCP_URL)) blocks.push(TASKS_RULES);
|
|
190
|
+
if (usable.has('v8std') || (byEnv && env.V8STD_MCP_URL)) blocks.push(V8STD_RULES);
|
|
172
191
|
|
|
173
192
|
const lazy = servers.filter((s) => s.state === 'lazy').map((s) => s.name);
|
|
174
193
|
const unavailable = servers.filter((s) => s.state === 'unavailable').map((s) => s.name);
|
|
@@ -199,7 +218,6 @@ export function normalizeServers(config = {}) {
|
|
|
199
218
|
transport,
|
|
200
219
|
lazy: raw.lazy === true,
|
|
201
220
|
unavailable: raw.unavailable === true,
|
|
202
|
-
shared: raw.shared === true,
|
|
203
221
|
raw,
|
|
204
222
|
});
|
|
205
223
|
}
|
|
@@ -269,9 +287,24 @@ const textTool = (toolName, description, parameters, execute) => ({
|
|
|
269
287
|
execute,
|
|
270
288
|
});
|
|
271
289
|
|
|
272
|
-
/** Подпись конфигурации для обнаружения изменений при
|
|
290
|
+
/** Подпись конфигурации для обнаружения изменений при edt_reload. */
|
|
273
291
|
const signatureOf = (entry, root) => JSON.stringify({ config: mountConfig(entry), root });
|
|
274
292
|
|
|
293
|
+
const activationReason = (activation) => {
|
|
294
|
+
switch (activation.reason) {
|
|
295
|
+
case 'enabled':
|
|
296
|
+
return 'enabled: true в настройках';
|
|
297
|
+
case 'disabled':
|
|
298
|
+
return 'enabled: false в настройках';
|
|
299
|
+
case 'project-file':
|
|
300
|
+
return 'есть .dsh-1c.json проекта';
|
|
301
|
+
case 'allowlist':
|
|
302
|
+
return `путь в projects (${activation.entry})`;
|
|
303
|
+
default:
|
|
304
|
+
return 'проект не выбран: нет .dsh-1c.json и его нет в projects';
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
275
308
|
/**
|
|
276
309
|
* Каталог, от которого считаются настройки проекта, корень BSL и пути вики.
|
|
277
310
|
* Исходный каталог сессии (`session.header.cwd`) неизменяем, поэтому `cd`
|
|
@@ -299,11 +332,12 @@ export async function apply(ctx, config) {
|
|
|
299
332
|
const agentStates = new WeakMap();
|
|
300
333
|
const watchedProjects = new Set();
|
|
301
334
|
const loggedErrors = new Set();
|
|
335
|
+
let skillsControl;
|
|
302
336
|
|
|
303
337
|
const cwdOf = (agent) => sessionCwd(agent, ctx.workingDirectory);
|
|
304
338
|
|
|
305
|
-
const
|
|
306
|
-
const effective = resolveConfig({ cwd
|
|
339
|
+
const effectiveForCwd = (cwd) => {
|
|
340
|
+
const effective = resolveConfig({ cwd, env: process.env, base });
|
|
307
341
|
for (const error of effective.errors) {
|
|
308
342
|
if (loggedErrors.has(error)) continue;
|
|
309
343
|
loggedErrors.add(error);
|
|
@@ -312,6 +346,13 @@ export async function apply(ctx, config) {
|
|
|
312
346
|
return effective;
|
|
313
347
|
};
|
|
314
348
|
|
|
349
|
+
const effectiveFor = (agent) => effectiveForCwd(cwdOf(agent));
|
|
350
|
+
|
|
351
|
+
const activationOfCwd = (cwd) =>
|
|
352
|
+
activationFor({ cwd, config: effectiveForCwd(cwd).config, hasProjectFile: projectConfigLoaded(cwd) });
|
|
353
|
+
|
|
354
|
+
const activationOf = (agent) => activationOfCwd(cwdOf(agent));
|
|
355
|
+
|
|
315
356
|
const entriesFor = (effective) => {
|
|
316
357
|
const bsl = bslSettingsFor(effective);
|
|
317
358
|
return normalizeServers(effective.config?.servers).map((entry) => {
|
|
@@ -324,22 +365,23 @@ export async function apply(ctx, config) {
|
|
|
324
365
|
});
|
|
325
366
|
};
|
|
326
367
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
agentStates.set(agent, map);
|
|
368
|
+
/** Состояние активации одного агента (сессии): инструменты и смонтированные серверы. */
|
|
369
|
+
const stateOf = (agent) => {
|
|
370
|
+
let state = agentStates.get(agent);
|
|
371
|
+
if (state === undefined) {
|
|
372
|
+
state = { active: false, disposers: [], items: new Map() };
|
|
373
|
+
agentStates.set(agent, state);
|
|
334
374
|
}
|
|
335
|
-
return
|
|
375
|
+
return state;
|
|
336
376
|
};
|
|
337
377
|
|
|
378
|
+
const mapFor = (agent) => (agent === undefined ? globalState : stateOf(agent).items);
|
|
379
|
+
|
|
338
380
|
const itemFor = (agent, entry) => {
|
|
339
|
-
const map = mapFor(agent
|
|
381
|
+
const map = mapFor(agent);
|
|
340
382
|
let item = map.get(entry.name);
|
|
341
383
|
if (item === undefined) {
|
|
342
|
-
item = { entry, fiber: null, error: null, supervisor: null, signature: undefined };
|
|
384
|
+
item = { entry, fiber: null, error: null, supervisor: null, signature: undefined, suppressed: false };
|
|
343
385
|
map.set(entry.name, item);
|
|
344
386
|
} else {
|
|
345
387
|
item.entry = entry;
|
|
@@ -347,10 +389,13 @@ export async function apply(ctx, config) {
|
|
|
347
389
|
return item;
|
|
348
390
|
};
|
|
349
391
|
|
|
350
|
-
const toolNamesOf = (serverName) => {
|
|
392
|
+
const toolNamesOf = (agent, serverName) => {
|
|
351
393
|
const prefix = `mcp__${serverName}__`;
|
|
352
394
|
try {
|
|
353
|
-
return
|
|
395
|
+
return ctx.tools
|
|
396
|
+
.schemas(agent === undefined ? undefined : agent)
|
|
397
|
+
.map((schema) => schema.name)
|
|
398
|
+
.filter((toolName) => toolName.startsWith(prefix));
|
|
354
399
|
} catch {
|
|
355
400
|
return [];
|
|
356
401
|
}
|
|
@@ -359,8 +404,7 @@ export async function apply(ctx, config) {
|
|
|
359
404
|
const snapshot = (agent) => {
|
|
360
405
|
const effective = effectiveFor(agent);
|
|
361
406
|
return entriesFor(effective).map((entry) => {
|
|
362
|
-
const
|
|
363
|
-
const item = map.get(entry.name);
|
|
407
|
+
const item = mapFor(agent).get(entry.name);
|
|
364
408
|
let value = 'lazy';
|
|
365
409
|
if (entry.unavailable) value = 'unavailable';
|
|
366
410
|
if (item?.fiber && !item.error) value = 'mounted';
|
|
@@ -368,11 +412,11 @@ export async function apply(ctx, config) {
|
|
|
368
412
|
return {
|
|
369
413
|
name: entry.name,
|
|
370
414
|
state: value,
|
|
371
|
-
scope:
|
|
415
|
+
scope: agent === undefined ? 'global' : 'session',
|
|
372
416
|
transport: item?.supervisor ? 'lsp+mcp' : entry.transport,
|
|
373
417
|
root: item?.supervisor?.root ?? entry.raw?.root,
|
|
374
418
|
endpoint: item?.supervisor?.url ?? entry.raw?.url,
|
|
375
|
-
tools: toolNamesOf(entry.name).length,
|
|
419
|
+
tools: toolNamesOf(agent, entry.name).length,
|
|
376
420
|
error: item?.error,
|
|
377
421
|
};
|
|
378
422
|
});
|
|
@@ -383,7 +427,7 @@ export async function apply(ctx, config) {
|
|
|
383
427
|
await item.fiber.dispose().catch(() => {});
|
|
384
428
|
item.fiber = null;
|
|
385
429
|
}
|
|
386
|
-
await item.supervisor?.stop();
|
|
430
|
+
await Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
|
|
387
431
|
item.supervisor = null;
|
|
388
432
|
item.error = null;
|
|
389
433
|
item.signature = undefined;
|
|
@@ -406,7 +450,10 @@ export async function apply(ctx, config) {
|
|
|
406
450
|
|
|
407
451
|
if (item.fiber && !item.error) {
|
|
408
452
|
const sameRoot = item.supervisor === null || item.supervisor?.root === root;
|
|
409
|
-
if (sameRoot)
|
|
453
|
+
if (sameRoot) {
|
|
454
|
+
item.suppressed = false;
|
|
455
|
+
return { already: true, tools: toolNamesOf(agent, serverName).length };
|
|
456
|
+
}
|
|
410
457
|
await disposeItem(item);
|
|
411
458
|
}
|
|
412
459
|
|
|
@@ -425,6 +472,13 @@ export async function apply(ctx, config) {
|
|
|
425
472
|
const jar = await ensureJar(bsl, { log, warn });
|
|
426
473
|
const supervisor = await startBslSupervisor({ jar, java: bsl.java, root, port: bsl.port, log });
|
|
427
474
|
item.supervisor = supervisor;
|
|
475
|
+
// Процесс BSL LS должен умереть вместе со скоупом агента, даже если
|
|
476
|
+
// disposeItem не вызвали (сессия закрыта, плагин выгружен).
|
|
477
|
+
if (agent !== undefined) {
|
|
478
|
+
agent.ctx.effect(() => () => {
|
|
479
|
+
Promise.resolve(supervisor.stop()).catch(() => {});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
428
482
|
resolved = { transport: 'streamable-http', url: supervisor.url };
|
|
429
483
|
}
|
|
430
484
|
resolvedEntry = {
|
|
@@ -439,33 +493,39 @@ export async function apply(ctx, config) {
|
|
|
439
493
|
};
|
|
440
494
|
} catch (error) {
|
|
441
495
|
item.error = error instanceof Error ? error.message : String(error);
|
|
442
|
-
await item.supervisor?.stop();
|
|
496
|
+
await Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
|
|
443
497
|
item.supervisor = null;
|
|
444
498
|
return { already: false, tools: 0, error: item.error };
|
|
445
499
|
}
|
|
446
500
|
}
|
|
447
501
|
|
|
448
502
|
const McpClient = await import('@deepseek-ai/dsh-mcp-client');
|
|
449
|
-
const scopeCtx =
|
|
503
|
+
const scopeCtx = agent === undefined ? ctx : agent.ctx;
|
|
450
504
|
const fiber = scopeCtx.plugin(McpClient, mountConfig(resolvedEntry));
|
|
451
505
|
item.fiber = fiber;
|
|
452
506
|
item.error = null;
|
|
453
507
|
item.signature = signatureOf(resolvedEntry, root);
|
|
508
|
+
item.suppressed = false;
|
|
454
509
|
try {
|
|
455
510
|
await fiber;
|
|
456
511
|
} catch (error) {
|
|
457
512
|
item.error = error instanceof Error ? error.message : String(error);
|
|
458
513
|
}
|
|
459
|
-
return { already: false, tools: toolNamesOf(serverName).length, error: item.error };
|
|
514
|
+
return { already: false, tools: toolNamesOf(agent, serverName).length, error: item.error };
|
|
460
515
|
};
|
|
461
516
|
|
|
462
517
|
const unmount = async (agent, serverName) => {
|
|
463
518
|
const effective = effectiveFor(agent);
|
|
464
519
|
const entry = entriesFor(effective).find((candidate) => candidate.name === serverName);
|
|
465
|
-
const map =
|
|
520
|
+
const map = mapFor(agent);
|
|
466
521
|
const item = map.get(serverName);
|
|
467
|
-
if (item === undefined || !item.fiber)
|
|
522
|
+
if (item === undefined || !item.fiber) {
|
|
523
|
+
if (entry !== undefined && item !== undefined) item.suppressed = true;
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
468
526
|
await disposeItem(item);
|
|
527
|
+
// Незаленивый сервер иначе смонтируется снова на следующем шаге.
|
|
528
|
+
item.suppressed = true;
|
|
469
529
|
return true;
|
|
470
530
|
};
|
|
471
531
|
|
|
@@ -480,8 +540,7 @@ export async function apply(ctx, config) {
|
|
|
480
540
|
|
|
481
541
|
const changed = [];
|
|
482
542
|
for (const entry of targets) {
|
|
483
|
-
const
|
|
484
|
-
const item = map.get(entry.name);
|
|
543
|
+
const item = mapFor(agent).get(entry.name);
|
|
485
544
|
if (item === undefined || !item.fiber) continue;
|
|
486
545
|
const bsl = bslSettingsFor(effective);
|
|
487
546
|
const root = entry.raw.root ?? bsl.root;
|
|
@@ -496,7 +555,98 @@ export async function apply(ctx, config) {
|
|
|
496
555
|
|
|
497
556
|
// Если плагин выгружают, процессы серверов не должны остаться висеть.
|
|
498
557
|
ctx.effect(() => () => {
|
|
499
|
-
for (const item of globalState.values()) item.supervisor?.stop();
|
|
558
|
+
for (const item of globalState.values()) Promise.resolve(item.supervisor?.stop?.()).catch(() => {});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// --- активация на проект ----------------------------------------------------
|
|
562
|
+
const invalidateSkills = () => {
|
|
563
|
+
try {
|
|
564
|
+
skillsControl?.invalidate?.();
|
|
565
|
+
} catch {
|
|
566
|
+
// Инвалидация каталога — вспомогательная операция, не валим шаг.
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const watchFile = (path) => {
|
|
571
|
+
if (path === undefined || watchedProjects.has(path)) return;
|
|
572
|
+
watchedProjects.add(path);
|
|
573
|
+
try {
|
|
574
|
+
const watcher = watch(path, { persistent: false }, () => {
|
|
575
|
+
invalidateSkills();
|
|
576
|
+
ctx.logger?.warn?.(`mcp-hub: настройки изменились (${path}). Примените их через edt_reload.`);
|
|
577
|
+
});
|
|
578
|
+
watcher.on('error', () => {});
|
|
579
|
+
ctx.effect(() => () => watcher.close());
|
|
580
|
+
} catch {
|
|
581
|
+
// Файла может ещё не быть — наблюдение не критично.
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const activate = async (agent) => {
|
|
586
|
+
const state = stateOf(agent);
|
|
587
|
+
if (state.active) return;
|
|
588
|
+
state.active = true;
|
|
589
|
+
for (const definition of [serversTool(), pinTool(), unpinTool(), reloadTool()]) {
|
|
590
|
+
state.disposers.push(agent.ctx.tools.register(definition));
|
|
591
|
+
}
|
|
592
|
+
watchFile(projectConfigPath(cwdOf(agent)));
|
|
593
|
+
|
|
594
|
+
const effective = effectiveFor(agent);
|
|
595
|
+
for (const entry of entriesFor(effective)) {
|
|
596
|
+
if (entry.lazy || entry.unavailable) continue;
|
|
597
|
+
try {
|
|
598
|
+
await mount(agent, entry.name);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
ctx.logger?.warn?.(`mcp-hub: ${entry.name} failed to mount: ${error instanceof Error ? error.message : String(error)}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const deactivate = async (agent) => {
|
|
606
|
+
const state = agentStates.get(agent);
|
|
607
|
+
if (state === undefined || !state.active) return;
|
|
608
|
+
state.active = false;
|
|
609
|
+
for (const dispose of state.disposers.splice(0)) {
|
|
610
|
+
try {
|
|
611
|
+
dispose();
|
|
612
|
+
} catch {
|
|
613
|
+
// Инструменты могли быть уже сняты выгрузкой скоупа.
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const item of state.items.values()) await disposeItem(item);
|
|
617
|
+
state.items.clear();
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Сводит состояние сессии с текущими настройками: включает проект на лету
|
|
622
|
+
* после edt_config init/правки файла и выключает после enabled:false.
|
|
623
|
+
*/
|
|
624
|
+
const reconcile = async (agent) => {
|
|
625
|
+
if (agent === undefined) return;
|
|
626
|
+
const { enabled } = activationOf(agent);
|
|
627
|
+
const state = agentStates.get(agent);
|
|
628
|
+
if (enabled) {
|
|
629
|
+
if (state === undefined || !state.active) await activate(agent);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
await deactivate(agent);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
ctx.on('agent/created', async ({ agent }) => {
|
|
636
|
+
try {
|
|
637
|
+
await reconcile(agent);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
ctx.logger?.warn?.(`mcp-hub: активация для сессии не удалась: ${error instanceof Error ? error.message : String(error)}`);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
644
|
+
try {
|
|
645
|
+
await reconcile(agent);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
ctx.logger?.warn?.(`mcp-hub: активация для сессии не удалась: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
|
+
}
|
|
649
|
+
return next();
|
|
500
650
|
});
|
|
501
651
|
|
|
502
652
|
// --- вики: чтение разрешено, запись отклоняется ---
|
|
@@ -509,10 +659,10 @@ export async function apply(ctx, config) {
|
|
|
509
659
|
return writeDenialReason(exec.name, target, roots, { cwd });
|
|
510
660
|
});
|
|
511
661
|
|
|
512
|
-
|
|
662
|
+
const serversTool = () =>
|
|
513
663
|
textTool(
|
|
514
|
-
'
|
|
515
|
-
'List MCP servers: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
|
|
664
|
+
'edt_servers',
|
|
665
|
+
'List MCP servers of the 1C plugin: state (mounted, lazy, unavailable, failed), scope, transport, tool count, workspace root and endpoint. Settings are read from $DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json.',
|
|
516
666
|
{},
|
|
517
667
|
async (_args, exec) => {
|
|
518
668
|
const rows = snapshot(exec?.agent).map((item) => {
|
|
@@ -527,11 +677,10 @@ export async function apply(ctx, config) {
|
|
|
527
677
|
});
|
|
528
678
|
return { text: rows.length > 0 ? rows.join('\n') : 'No MCP servers are configured.' };
|
|
529
679
|
},
|
|
530
|
-
)
|
|
531
|
-
);
|
|
680
|
+
);
|
|
532
681
|
|
|
533
682
|
const serverParam = {
|
|
534
|
-
server: { type: 'string', description: 'Server name as reported by
|
|
683
|
+
server: { type: 'string', description: 'Server name as reported by edt_servers.', required: true },
|
|
535
684
|
root: {
|
|
536
685
|
type: 'string',
|
|
537
686
|
description:
|
|
@@ -540,50 +689,48 @@ export async function apply(ctx, config) {
|
|
|
540
689
|
mode: { type: 'string', description: 'For a provisioned server: "hybrid" (LSP+MCP with workspace roots, default) or "stdio".' },
|
|
541
690
|
};
|
|
542
691
|
|
|
543
|
-
|
|
692
|
+
const pinTool = () =>
|
|
544
693
|
textTool(
|
|
545
|
-
'
|
|
694
|
+
'edt_pin',
|
|
546
695
|
'Mount a configured MCP server now, so its tools appear in this session. Re-reads the settings files first, so edited ports or roots apply without restarting DSH.',
|
|
547
696
|
serverParam,
|
|
548
697
|
async (args, exec) => {
|
|
549
698
|
const result = await mount(exec?.agent, args.server, { root: args.root, mode: args.mode });
|
|
550
|
-
if (result.error) throw new Error(`
|
|
699
|
+
if (result.error) throw new Error(`edt_pin(${args.server}) failed: ${result.error}`);
|
|
551
700
|
if (result.already) return { text: `${args.server} is already mounted (${result.tools} tools).` };
|
|
552
701
|
return { text: `${args.server} mounted (${result.tools} tools). They are available from the next step.` };
|
|
553
702
|
},
|
|
554
|
-
)
|
|
555
|
-
);
|
|
703
|
+
);
|
|
556
704
|
|
|
557
|
-
|
|
705
|
+
const unpinTool = () =>
|
|
558
706
|
textTool(
|
|
559
|
-
'
|
|
707
|
+
'edt_unpin',
|
|
560
708
|
'Unmount a mounted MCP server and remove its tools from the session, freeing prompt space. Do not unmount a server while work with it is in progress.',
|
|
561
709
|
{ server: serverParam.server },
|
|
562
710
|
async (args, exec) => {
|
|
563
711
|
const wasMounted = await unmount(exec?.agent, args.server);
|
|
564
712
|
return { text: wasMounted ? `${args.server} unmounted.` : `${args.server} was not mounted.` };
|
|
565
713
|
},
|
|
566
|
-
)
|
|
567
|
-
);
|
|
714
|
+
);
|
|
568
715
|
|
|
569
|
-
|
|
716
|
+
const reloadTool = () =>
|
|
570
717
|
textTool(
|
|
571
|
-
'
|
|
718
|
+
'edt_reload',
|
|
572
719
|
'Re-read the settings files ($DSH_HOME/dsh-1c.json and <cwd>/.dsh-1c.json) and restart mounted servers whose url, headers, command or root changed. Use after editing a settings file; no DSH restart needed.',
|
|
573
720
|
{ server: { type: 'string', description: 'Only this server; omit to check every configured server.' } },
|
|
574
721
|
async (args, exec) => {
|
|
722
|
+
invalidateSkills();
|
|
575
723
|
const changed = await reload(exec?.agent, args.server);
|
|
576
724
|
if (changed.length === 0) return { text: 'Настройки перечитаны, изменений в подключённых серверах нет.' };
|
|
577
725
|
const rows = changed.map((item) => `${item.name}: ${item.error === undefined ? 'перезапущен' : `ошибка — ${item.error}`}`);
|
|
578
726
|
return { text: `Настройки перечитаны:\n${rows.join('\n')}` };
|
|
579
727
|
},
|
|
580
|
-
)
|
|
581
|
-
);
|
|
728
|
+
);
|
|
582
729
|
|
|
583
|
-
|
|
730
|
+
const configTool = () =>
|
|
584
731
|
textTool(
|
|
585
|
-
'
|
|
586
|
-
'
|
|
732
|
+
'edt_config',
|
|
733
|
+
'Manage dsh-edt-projects settings: which projects enable the 1C plugin (enabled/projects), wiki paths, test tooling, MCP servers and bundled skills. action=show prints the effective settings, their sources, whether this project is enabled and validation warnings; init creates the project .dsh-1c.json (which enables the plugin for it); upgrade adds only missing keys, keeping your values; validate reports type errors and unknown keys.',
|
|
587
734
|
{
|
|
588
735
|
action: {
|
|
589
736
|
type: 'string',
|
|
@@ -597,12 +744,14 @@ export async function apply(ctx, config) {
|
|
|
597
744
|
async (args, exec) => {
|
|
598
745
|
const cwd = cwdOf(exec?.agent);
|
|
599
746
|
const path = args.scope === 'user' ? userConfigPath(process.env) : projectConfigPath(cwd);
|
|
600
|
-
if (path === undefined) throw new Error('
|
|
747
|
+
if (path === undefined) throw new Error('edt_config: cannot resolve the settings path');
|
|
601
748
|
|
|
602
749
|
if (args.action === 'show') {
|
|
603
750
|
const effective = effectiveFor(exec?.agent);
|
|
604
751
|
const { errors, warnings } = validateConfig(effective.config);
|
|
752
|
+
const activation = activationOf(exec?.agent);
|
|
605
753
|
const head = [
|
|
754
|
+
`Проект: ${activation.enabled ? 'включён' : 'выключен'} — ${activationReason(activation)}`,
|
|
606
755
|
`Источники: ${effective.sources.length > 0 ? effective.sources.join(', ') : 'только cordis.patch.yml'}`,
|
|
607
756
|
`Действующие настройки:\n${stringifyConfig(effective.config)}`,
|
|
608
757
|
];
|
|
@@ -614,15 +763,21 @@ export async function apply(ctx, config) {
|
|
|
614
763
|
if (args.action === 'init') {
|
|
615
764
|
const created = initFile(path);
|
|
616
765
|
if (created.status === 'exists') return { text: `${path} уже существует — используйте action=upgrade, чтобы дополнить его.` };
|
|
617
|
-
if (created.status === 'error') throw new Error(`
|
|
618
|
-
|
|
766
|
+
if (created.status === 'error') throw new Error(`edt_config init: ${created.error}`);
|
|
767
|
+
invalidateSkills();
|
|
768
|
+
const hint =
|
|
769
|
+
args.scope === 'user'
|
|
770
|
+
? 'Дальше впишите пути проектов в projects, чтобы включить плагин там.'
|
|
771
|
+
: 'Проект включён со следующего шага: появятся правила 1С и mcp__edt__*.';
|
|
772
|
+
return { text: `Создан ${path}. ${hint}` };
|
|
619
773
|
}
|
|
620
774
|
|
|
621
775
|
if (args.action === 'upgrade') {
|
|
622
776
|
const dryRun = args.dryRun !== false;
|
|
623
777
|
const result = upgradeFile(path, { dryRun });
|
|
624
|
-
if (result.status === 'error') throw new Error(`
|
|
778
|
+
if (result.status === 'error') throw new Error(`edt_config upgrade: ${result.error}`);
|
|
625
779
|
if (result.status === 'noop') return { text: `${path}: актуален, добавлять нечего.` };
|
|
780
|
+
if (!dryRun) invalidateSkills();
|
|
626
781
|
const lines = [`${path}: ${dryRun ? 'будет добавлено' : 'добавлено'} ${result.added.length} ключ(ей):`];
|
|
627
782
|
for (const key of result.added) lines.push(` + ${key}`);
|
|
628
783
|
if (dryRun) lines.push('Это был предварительный просмотр. Повторите с dryRun: false, чтобы записать.');
|
|
@@ -640,35 +795,25 @@ export async function apply(ctx, config) {
|
|
|
640
795
|
for (const warning of warnings) lines.push(` ! ${warning}`);
|
|
641
796
|
return { text: lines.join('\n') };
|
|
642
797
|
},
|
|
643
|
-
)
|
|
644
|
-
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// edt_config остаётся глобальным: это единственная точка входа, которой
|
|
801
|
+
// включают проект, поэтому он не может зависеть от активации.
|
|
802
|
+
ctx.tools.register(configTool());
|
|
645
803
|
|
|
646
804
|
// --- навыки, поставляемые с бандлом ---
|
|
647
|
-
//
|
|
648
|
-
//
|
|
805
|
+
// Провайдер глобальный, но отдаёт навыки только для включённого проекта;
|
|
806
|
+
// инвалидация каталога при правке настроек — через control.invalidate().
|
|
649
807
|
ctx.inject(['skills'], (inner) => {
|
|
650
|
-
inner.skills.registerProvider(() =>
|
|
651
|
-
|
|
808
|
+
inner.skills.registerProvider((control) => {
|
|
809
|
+
skillsControl = control;
|
|
810
|
+
return createSkillsProvider({
|
|
652
811
|
resolveConfig: (cwd) => resolveConfig({ cwd: cwd ?? process.cwd(), env: process.env, base }).config,
|
|
812
|
+
isEnabled: (cwd, config) => activationFor({ cwd, config, hasProjectFile: projectConfigLoaded(cwd) }).enabled,
|
|
653
813
|
logger: ctx.logger,
|
|
654
|
-
}),
|
|
655
|
-
);
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
// --- наблюдение за файлами настроек ---
|
|
659
|
-
const watchFile = (path) => {
|
|
660
|
-
if (path === undefined || watchedProjects.has(path)) return;
|
|
661
|
-
watchedProjects.add(path);
|
|
662
|
-
try {
|
|
663
|
-
const watcher = watch(path, { persistent: false }, () => {
|
|
664
|
-
ctx.logger?.warn?.(`mcp-hub: настройки изменились (${path}). Примените их через mcp_reload.`);
|
|
665
814
|
});
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
} catch {
|
|
669
|
-
// Файла может ещё не быть — наблюдение не критично.
|
|
670
|
-
}
|
|
671
|
-
};
|
|
815
|
+
});
|
|
816
|
+
});
|
|
672
817
|
|
|
673
818
|
watchFile(userConfigPath(process.env));
|
|
674
819
|
watchFile(projectConfigPath(cwdOf(undefined)));
|
|
@@ -680,25 +825,13 @@ export async function apply(ctx, config) {
|
|
|
680
825
|
interpolate: false,
|
|
681
826
|
text: (context) => {
|
|
682
827
|
const agent = context?.agent;
|
|
828
|
+
// В выключенном проекте секция пустая: renderPrompt выбрасывает её,
|
|
829
|
+
// поэтому в промпте не остаётся ни строчки про 1С.
|
|
830
|
+
if (agent === undefined || !activationOf(agent).enabled) return '';
|
|
683
831
|
const effective = effectiveFor(agent);
|
|
684
832
|
const { roots } = normalizeWiki(effective.config?.wiki, { cwd: cwdOf(agent) });
|
|
685
833
|
return buildRulesText(process.env, snapshot(agent), roots, effective.config?.tests);
|
|
686
834
|
},
|
|
687
835
|
});
|
|
688
836
|
});
|
|
689
|
-
|
|
690
|
-
// Серверы без `lazy` подключаются на старте и ЖДУТСЯ: иначе их инструменты
|
|
691
|
-
// опаздывают к первому запросу и модель видит пустой набор MCP.
|
|
692
|
-
// Ошибка подключения не валит загрузку: сервер останется в состоянии failed,
|
|
693
|
-
// а mcp-client продолжит реконнект в фоне.
|
|
694
|
-
const eager = entriesFor(effectiveFor(undefined)).filter((entry) => !entry.lazy && !entry.unavailable);
|
|
695
|
-
await Promise.all(
|
|
696
|
-
eager.map(async (entry) => {
|
|
697
|
-
try {
|
|
698
|
-
await mount(undefined, entry.name);
|
|
699
|
-
} catch (error) {
|
|
700
|
-
ctx.logger?.warn?.(`mcp-hub: ${entry.name} failed to mount: ${error instanceof Error ? error.message : String(error)}`);
|
|
701
|
-
}
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
704
837
|
}
|
package/lib/schema.mjs
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Правила апгрейда: существующие значения не меняются, недостающие ключи
|
|
5
5
|
// добавляются, неизвестные ключи сохраняются, версия поднимается.
|
|
6
|
-
export const CONFIG_VERSION =
|
|
6
|
+
export const CONFIG_VERSION = 2;
|
|
7
7
|
|
|
8
8
|
/** Значения по умолчанию для нового файла. Пустые строки и массивы
|
|
9
|
-
* нормализаторы игнорируют, поэтому init ничего не включает сам по себе.
|
|
9
|
+
* нормализаторы игнорируют, поэтому init ничего не включает сам по себе.
|
|
10
|
+
* `enabled` намеренно не входит в defaults: `upgrade` не должен выключать
|
|
11
|
+
* проект, который включён самим фактом своего `.dsh-1c.json`. */
|
|
10
12
|
export const DEFAULTS = {
|
|
11
13
|
version: CONFIG_VERSION,
|
|
14
|
+
projects: [],
|
|
12
15
|
wiki: { paths: [], entry: '' },
|
|
13
16
|
servers: {
|
|
14
17
|
edt: { url: 'http://127.0.0.1:8765/mcp' },
|
|
@@ -26,15 +29,17 @@ export const DEFAULTS = {
|
|
|
26
29
|
/** Известные ключи: путь -> описание и тип. Используются валидацией и README. */
|
|
27
30
|
export const KNOWN = {
|
|
28
31
|
version: { type: 'number', description: 'Версия схемы настроек.' },
|
|
32
|
+
enabled: { type: 'boolean', description: 'Включить или выключить плагин для этого проекта (сильнее файла проекта и списка projects).' },
|
|
33
|
+
projects: { type: 'array', description: 'Каталоги проектов, где плагин включён (пользовательский файл).' },
|
|
29
34
|
'wiki.paths': { type: 'array', description: 'Каталоги вики (абсолютные или относительно проекта).' },
|
|
30
35
|
'wiki.entry': { type: 'string', description: 'Входная точка вики, например wiki/index.md.' },
|
|
31
36
|
'servers': { type: 'object', description: 'Настройки MCP-серверов по именам.' },
|
|
32
37
|
'servers.*.url': { type: 'string', description: 'Адрес MCP-сервера (streamable-http).' },
|
|
33
38
|
'servers.*.headers': { type: 'object', description: 'Заголовки запросов, например Authorization.' },
|
|
34
39
|
'servers.*.root': { type: 'string', description: 'Корень рабочего пространства (для bsl).' },
|
|
35
|
-
'servers.*.lazy': { type: 'boolean', description: 'Не подключать на старте, только по
|
|
36
|
-
'servers.*.shared': { type: 'boolean', description: '
|
|
37
|
-
'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на
|
|
40
|
+
'servers.*.lazy': { type: 'boolean', description: 'Не подключать на старте, только по edt_pin.' },
|
|
41
|
+
'servers.*.shared': { type: 'boolean', description: 'Устарело: серверы всегда монтируются в скоупе сессии.' },
|
|
42
|
+
'servers.*.provision': { type: 'boolean', description: 'Параметры запуска добираются на edt_pin.' },
|
|
38
43
|
'servers.*.toolCallTimeoutMs': { type: 'number', description: 'Таймаут вызова инструмента.' },
|
|
39
44
|
'servers.*.failOnStartupError': { type: 'boolean', description: 'Падать, если стартовое подключение не удалось.' },
|
|
40
45
|
'servers.*.reconnect': { type: 'object', description: 'Политика переподключения.' },
|
|
@@ -171,6 +176,12 @@ export function validateConfig(config) {
|
|
|
171
176
|
if (!config.wiki.paths.every((item) => typeof item === 'string')) errors.push('wiki.paths: ожидался массив строк');
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
if (config.projects !== undefined && !Array.isArray(config.projects)) {
|
|
180
|
+
errors.push(`projects: ожидался массив путей, получено ${typeof config.projects}`);
|
|
181
|
+
} else if (Array.isArray(config.projects) && !config.projects.every((item) => typeof item === 'string')) {
|
|
182
|
+
errors.push('projects: ожидался массив строк');
|
|
183
|
+
}
|
|
184
|
+
|
|
174
185
|
return { errors, warnings };
|
|
175
186
|
}
|
|
176
187
|
|
package/lib/skills.mjs
CHANGED
|
@@ -63,15 +63,24 @@ export function skillFile(name) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Создаёт провайдер навыков.
|
|
65
65
|
* @param options.resolveConfig вызывается с cwd и возвращает действующий конфиг.
|
|
66
|
+
* @param options.isEnabled вызывается с (cwd, config) и говорит, активирован ли
|
|
67
|
+
* плагин для этого проекта; для выключенного проекта навыков нет.
|
|
66
68
|
* @param options.logger необязательный логгер для предупреждений.
|
|
67
69
|
*/
|
|
68
|
-
export function createSkillsProvider({ resolveConfig, logger } = {}) {
|
|
70
|
+
export function createSkillsProvider({ resolveConfig, isEnabled, logger } = {}) {
|
|
69
71
|
const read = (name) => {
|
|
70
72
|
const path = skillFile(name);
|
|
71
73
|
if (!existsSync(path)) return undefined;
|
|
72
74
|
return { path, ...parseFrontmatter(readFileSync(path, 'utf8')) };
|
|
73
75
|
};
|
|
74
76
|
|
|
77
|
+
const configFor = (cwd) => (typeof resolveConfig === 'function' ? resolveConfig(cwd) : {}) ?? {};
|
|
78
|
+
|
|
79
|
+
const allowed = (cwd) => {
|
|
80
|
+
if (typeof isEnabled !== 'function') return true;
|
|
81
|
+
return isEnabled(cwd, configFor(cwd)) === true;
|
|
82
|
+
};
|
|
83
|
+
|
|
75
84
|
const toCandidate = (name) => {
|
|
76
85
|
const parsed = read(name);
|
|
77
86
|
if (parsed === undefined || parsed.meta.description === undefined) return undefined;
|
|
@@ -91,7 +100,9 @@ export function createSkillsProvider({ resolveConfig, logger } = {}) {
|
|
|
91
100
|
name: 'edt-mcp-bundle-skills',
|
|
92
101
|
/** Отдаёт только навыки, включённые в конфиге проекта. */
|
|
93
102
|
list(options = {}) {
|
|
94
|
-
const
|
|
103
|
+
const cwd = options.cwd ?? process.cwd();
|
|
104
|
+
if (!allowed(cwd)) return [];
|
|
105
|
+
const config = configFor(cwd);
|
|
95
106
|
const candidates = [];
|
|
96
107
|
for (const skill of SKILLS) {
|
|
97
108
|
if (!skill.enabled(config)) continue;
|
|
@@ -105,7 +116,8 @@ export function createSkillsProvider({ resolveConfig, logger } = {}) {
|
|
|
105
116
|
return candidates;
|
|
106
117
|
},
|
|
107
118
|
/** Возвращает тело навыка без frontmatter. */
|
|
108
|
-
get(candidate) {
|
|
119
|
+
get(candidate, options = {}) {
|
|
120
|
+
if (!allowed(options.cwd ?? process.cwd())) return undefined;
|
|
109
121
|
const parsed = read(candidate?.name);
|
|
110
122
|
if (parsed === undefined) return undefined;
|
|
111
123
|
return {
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "1C:EDT + BSL MCP",
|
|
4
|
-
"description": "MCP hub for 1C development: EDT is
|
|
4
|
+
"description": "MCP hub for 1C development: active only in selected projects (.dsh-1c.json or the projects list). There EDT is connected up front, while BSL Language Server (diagnostics, types, navigation) and a task tracker are mounted on demand via edt_pin; 1C rules enter the prompt only there."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/ru.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "1C:EDT + BSL MCP",
|
|
4
|
-
"description": "Хаб MCP-серверов для разработки на 1С: EDT подключён
|
|
4
|
+
"description": "Хаб MCP-серверов для разработки на 1С: включается только в выбранных проектах (файл .dsh-1c.json или список projects). В них EDT подключён сразу, BSL Language Server (диагностика, типы, навигация) и система задач — по требованию через edt_pin; правила 1С появляются в промпте только там."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edt-projects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP hub for 1C development: EDT
|
|
5
|
+
"description": "Project-scoped MCP hub for 1C development: active only in selected projects, EDT connected up front there, BSL Language Server, a task tracker and the v8std standards MCP mounted on demand, bundled SonarQube and GitLab skills, with model-facing usage rules",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|