lv24-opencode-consilium 0.0.0-stage → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +499 -3
- package/README.ru.md +500 -0
- package/README.zh.md +188 -0
- package/bin/cli.js +19 -0
- package/bin/mcp.js +24 -0
- package/consilium.config.example.json +84 -0
- package/dist/agents.d.ts +79 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +1051 -0
- package/dist/agents.js.map +1 -0
- package/dist/artifacts.d.ts +75 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +464 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1392 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +160 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +419 -0
- package/dist/compat.js.map +1 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/consensus.d.ts +73 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +273 -0
- package/dist/consensus.js.map +1 -0
- package/dist/defaults.d.ts +34 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +102 -0
- package/dist/defaults.js.map +1 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +798 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fallback.d.ts +50 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +134 -0
- package/dist/fallback.js.map +1 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +71 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/models.d.ts +97 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +873 -0
- package/dist/models.js.map +1 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +223 -0
- package/dist/plugin.js.map +1 -0
- package/dist/presets.d.ts +27 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +86 -0
- package/dist/presets.js.map +1 -0
- package/dist/replay.d.ts +37 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +103 -0
- package/dist/replay.js.map +1 -0
- package/dist/report.d.ts +28 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +300 -0
- package/dist/report.js.map +1 -0
- package/dist/smart-experts.d.ts +44 -0
- package/dist/smart-experts.d.ts.map +1 -0
- package/dist/smart-experts.js +242 -0
- package/dist/smart-experts.js.map +1 -0
- package/dist/stream.d.ts +50 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +219 -0
- package/dist/stream.js.map +1 -0
- package/dist/tool-run.d.ts +49 -0
- package/dist/tool-run.d.ts.map +1 -0
- package/dist/tool-run.js +143 -0
- package/dist/tool-run.js.map +1 -0
- package/dist/types.d.ts +352 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/variants.d.ts +30 -0
- package/dist/variants.d.ts.map +1 -0
- package/dist/variants.js +111 -0
- package/dist/variants.js.map +1 -0
- package/dist/verify.d.ts +33 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +80 -0
- package/dist/verify.js.map +1 -0
- package/dist/watch.d.ts +73 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +350 -0
- package/dist/watch.js.map +1 -0
- package/docs/session-and-plugin.md +642 -0
- package/package.json +90 -6
- package/schema/config.schema.json +100 -0
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Диагностика окружения и конфигурации Consilium
|
|
3
|
+
* @module lv24-opencode-consilium
|
|
4
|
+
*/
|
|
5
|
+
import { accessSync, constants, existsSync, statSync } from 'fs';
|
|
6
|
+
import { dirname, resolve } from 'path';
|
|
7
|
+
import { getOpenCodeVersion } from './agents.js';
|
|
8
|
+
import { fetchFreeModels } from './models.js';
|
|
9
|
+
import { findOpenCodeConfigPath, readJsonFile } from './config.js';
|
|
10
|
+
import { resolveArtifactRoot } from './artifacts.js';
|
|
11
|
+
import { describeConfigFlavor, findLegacyV1Fields, parseOpenCodeVersion, readAgentDefinition, readAgentSection, readMcpSection } from './compat.js';
|
|
12
|
+
/** Паттерн допустимой модели provider/model */
|
|
13
|
+
const MODEL_PATTERN = /^[A-Za-z0-9._:@\/#+-]+$/;
|
|
14
|
+
/** Проверяет, является ли значение обычным объектом */
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/** Извлекает текст ошибки */
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
/** Ограничивает список имён в сообщении */
|
|
23
|
+
function formatNames(names, limit = 5) {
|
|
24
|
+
if (names.length <= limit) {
|
|
25
|
+
return names.join(', ');
|
|
26
|
+
}
|
|
27
|
+
return `${names.slice(0, limit).join(', ')} и ещё ${names.length - limit}`;
|
|
28
|
+
}
|
|
29
|
+
/** Находит дубликаты в списке имён */
|
|
30
|
+
function findDuplicates(names) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const duplicates = new Set();
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
if (seen.has(name)) {
|
|
35
|
+
duplicates.add(name);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
seen.add(name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...duplicates];
|
|
42
|
+
}
|
|
43
|
+
/** Собирает валидные имена экспертов */
|
|
44
|
+
function collectExpertNames(config) {
|
|
45
|
+
const names = [];
|
|
46
|
+
if (!Array.isArray(config.experts)) {
|
|
47
|
+
return names;
|
|
48
|
+
}
|
|
49
|
+
for (const expert of config.experts) {
|
|
50
|
+
if (isRecord(expert) && typeof expert.name === 'string' && expert.name.trim().length > 0) {
|
|
51
|
+
names.push(expert.name.trim());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
/** Собирает модели экспертов и председателя */
|
|
57
|
+
function collectModelRefs(config) {
|
|
58
|
+
const refs = [];
|
|
59
|
+
if (Array.isArray(config.experts)) {
|
|
60
|
+
config.experts.forEach((expert, index) => {
|
|
61
|
+
if (!isRecord(expert)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const model = expert.model;
|
|
65
|
+
if (typeof model !== 'string' || model.trim().length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const owner = typeof expert.name === 'string' && expert.name.trim().length > 0
|
|
69
|
+
? expert.name.trim()
|
|
70
|
+
: `expert#${index}`;
|
|
71
|
+
refs.push({ owner, model: model.trim() });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (isRecord(config.chair)) {
|
|
75
|
+
const chairModel = config.chair.model;
|
|
76
|
+
if (typeof chairModel === 'string' && chairModel.trim().length > 0) {
|
|
77
|
+
refs.push({ owner: 'chair', model: chairModel.trim() });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return refs;
|
|
81
|
+
}
|
|
82
|
+
/** Возвращает префикс провайдера модели */
|
|
83
|
+
function getProvider(model) {
|
|
84
|
+
const slashIndex = model.indexOf('/');
|
|
85
|
+
return slashIndex > 0 ? model.slice(0, slashIndex) : '';
|
|
86
|
+
}
|
|
87
|
+
/** Проверяет версию Node.js */
|
|
88
|
+
function checkNodeVersion() {
|
|
89
|
+
const rawVersion = process.versions.node;
|
|
90
|
+
const major = Number.parseInt(rawVersion.split('.')[0] ?? '0', 10);
|
|
91
|
+
if (Number.isFinite(major) && major >= 18) {
|
|
92
|
+
return {
|
|
93
|
+
id: 'node-version',
|
|
94
|
+
status: 'ok',
|
|
95
|
+
message: `Node.js ${rawVersion}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
id: 'node-version',
|
|
100
|
+
status: 'fail',
|
|
101
|
+
message: `Node.js ${rawVersion} не поддерживается (требуется >= 18)`,
|
|
102
|
+
hint: 'Обновите Node.js до версии 18 или выше'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Проверяет доступность OpenCode CLI */
|
|
106
|
+
function checkOpenCodeCli(version) {
|
|
107
|
+
if (version !== null && version.trim().length > 0) {
|
|
108
|
+
const info = parseOpenCodeVersion(version);
|
|
109
|
+
const channel = info.major === 2
|
|
110
|
+
? 'V2: V1-формат конфигурации поддерживается'
|
|
111
|
+
: info.major === 1
|
|
112
|
+
? 'V1: нативный V2-формат не читается'
|
|
113
|
+
: 'версия не распознана';
|
|
114
|
+
return {
|
|
115
|
+
id: 'opencode-cli',
|
|
116
|
+
status: 'ok',
|
|
117
|
+
message: `OpenCode CLI: ${version.trim()} (${channel})`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id: 'opencode-cli',
|
|
122
|
+
status: 'fail',
|
|
123
|
+
message: 'OpenCode CLI не найден или не запускается',
|
|
124
|
+
hint: 'npm install -g opencode-ai'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Проверяет наличие файла конфигурации Consilium */
|
|
128
|
+
function checkConfigFile(configPath) {
|
|
129
|
+
if (typeof configPath === 'string' && configPath.length > 0) {
|
|
130
|
+
return {
|
|
131
|
+
id: 'config-file',
|
|
132
|
+
status: 'ok',
|
|
133
|
+
message: `Конфиг: ${configPath}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
id: 'config-file',
|
|
138
|
+
status: 'warn',
|
|
139
|
+
message: 'Конфиг не найден, используются значения по умолчанию',
|
|
140
|
+
hint: 'Выполните `consilium init`, чтобы создать конфиг'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Проверяет список экспертов */
|
|
144
|
+
function checkExperts(config) {
|
|
145
|
+
const id = 'experts-config';
|
|
146
|
+
const rawExperts = config.experts;
|
|
147
|
+
if (!Array.isArray(rawExperts) || rawExperts.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
id,
|
|
150
|
+
status: 'fail',
|
|
151
|
+
message: 'Список экспертов пуст',
|
|
152
|
+
hint: 'Добавьте экспертов в experts или выполните `consilium init`'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const names = [];
|
|
156
|
+
const invalidIndexes = [];
|
|
157
|
+
let enabled = 0;
|
|
158
|
+
for (let index = 0; index < rawExperts.length; index += 1) {
|
|
159
|
+
const entry = rawExperts[index];
|
|
160
|
+
if (!isRecord(entry)) {
|
|
161
|
+
invalidIndexes.push(index);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const name = entry.name;
|
|
165
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
166
|
+
invalidIndexes.push(index);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
names.push(name.trim());
|
|
170
|
+
}
|
|
171
|
+
if (entry.enabled !== false) {
|
|
172
|
+
enabled += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (invalidIndexes.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
status: 'fail',
|
|
179
|
+
message: `Некорректные имена экспертов: ${invalidIndexes.map(index => `#${index}`).join(', ')}`,
|
|
180
|
+
hint: 'У каждого эксперта должно быть непустое строковое поле name'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (enabled === 0) {
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
status: 'fail',
|
|
187
|
+
message: 'Нет включённых экспертов',
|
|
188
|
+
hint: 'Установите enabled: true хотя бы одному эксперту'
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const duplicates = findDuplicates(names);
|
|
192
|
+
if (duplicates.length > 0) {
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
status: 'warn',
|
|
196
|
+
message: `Дубликаты имён экспертов: ${duplicates.join(', ')}`,
|
|
197
|
+
hint: 'Агенты с одинаковыми именами неразличимы для OpenCode'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
id,
|
|
202
|
+
status: 'ok',
|
|
203
|
+
message: `Экспертов: ${rawExperts.length}, включено: ${enabled}`
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Проверяет конфигурацию председателя */
|
|
207
|
+
function checkChair(config) {
|
|
208
|
+
const id = 'chair-config';
|
|
209
|
+
const chair = config.chair;
|
|
210
|
+
if (!isRecord(chair)) {
|
|
211
|
+
return {
|
|
212
|
+
id,
|
|
213
|
+
status: 'fail',
|
|
214
|
+
message: 'Конфигурация председателя отсутствует',
|
|
215
|
+
hint: 'Добавьте объект chair с полем agent'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const agent = chair.agent;
|
|
219
|
+
if (typeof agent !== 'string' || agent.trim().length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
id,
|
|
222
|
+
status: 'fail',
|
|
223
|
+
message: 'Не задан агент-председатель',
|
|
224
|
+
hint: 'Укажите непустое значение chair.agent'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const chairAgent = agent.trim();
|
|
228
|
+
const expertNames = collectExpertNames(config);
|
|
229
|
+
if (expertNames.includes(chairAgent)) {
|
|
230
|
+
return {
|
|
231
|
+
id,
|
|
232
|
+
status: 'warn',
|
|
233
|
+
message: `Председатель «${chairAgent}» совпадает с экспертом`,
|
|
234
|
+
hint: 'Возможна путаница ролей: используйте отдельного агента для председателя'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
status: 'ok',
|
|
240
|
+
message: `Председатель: ${chairAgent}`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Проверяет таймауты */
|
|
244
|
+
function checkTimeouts(config) {
|
|
245
|
+
const id = 'timeouts';
|
|
246
|
+
const timeouts = config.timeouts;
|
|
247
|
+
if (!isRecord(timeouts)) {
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
status: 'fail',
|
|
251
|
+
message: 'Секция timeouts отсутствует',
|
|
252
|
+
hint: 'Задайте timeouts.expert, timeouts.chair и timeouts.shutdown'
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const expert = timeouts.expert;
|
|
256
|
+
const chair = timeouts.chair;
|
|
257
|
+
const shutdown = timeouts.shutdown;
|
|
258
|
+
const isPositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
259
|
+
if (!isPositiveNumber(expert) || !isPositiveNumber(chair) || !isPositiveNumber(shutdown)) {
|
|
260
|
+
return {
|
|
261
|
+
id,
|
|
262
|
+
status: 'fail',
|
|
263
|
+
message: 'Некорректные таймауты: ожидаются конечные положительные числа',
|
|
264
|
+
hint: 'Проверьте timeouts.expert, timeouts.chair и timeouts.shutdown'
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (expert < 10000 || chair < 30000) {
|
|
268
|
+
return {
|
|
269
|
+
id,
|
|
270
|
+
status: 'warn',
|
|
271
|
+
message: `Таймауты слишком малы: эксперт ${expert} мс, председатель ${chair} мс`,
|
|
272
|
+
hint: 'Рекомендуется expert >= 10000 мс и chair >= 30000 мс'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
id,
|
|
277
|
+
status: 'ok',
|
|
278
|
+
message: `Таймауты: эксперт ${expert} мс, председатель ${chair} мс, shutdown ${shutdown} мс`
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** Проверяет формат моделей и наличие ключей провайдеров */
|
|
282
|
+
function checkModelsFormat(config) {
|
|
283
|
+
const id = 'models-format';
|
|
284
|
+
const refs = collectModelRefs(config);
|
|
285
|
+
if (refs.length === 0) {
|
|
286
|
+
return {
|
|
287
|
+
id,
|
|
288
|
+
status: 'ok',
|
|
289
|
+
message: 'Модели не заданы — OpenCode выберет их автоматически'
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const invalid = refs.filter(ref => !MODEL_PATTERN.test(ref.model) || !ref.model.includes('/'));
|
|
293
|
+
if (invalid.length > 0) {
|
|
294
|
+
return {
|
|
295
|
+
id,
|
|
296
|
+
status: 'fail',
|
|
297
|
+
message: `Некорректные модели: ${formatNames(invalid.map(ref => `${ref.owner}=${ref.model}`))}`,
|
|
298
|
+
hint: 'Формат: provider/model; допустимы символы A-Z a-z 0-9 . _ : @ / # + -'
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const missingEnv = new Set();
|
|
302
|
+
for (const ref of refs) {
|
|
303
|
+
const provider = getProvider(ref.model);
|
|
304
|
+
if (provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) {
|
|
305
|
+
missingEnv.add('OPENROUTER_API_KEY');
|
|
306
|
+
}
|
|
307
|
+
else if (provider === 'opencode' && !process.env.OPENCODE_API_KEY) {
|
|
308
|
+
missingEnv.add('OPENCODE_API_KEY');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (missingEnv.size > 0) {
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
status: 'warn',
|
|
315
|
+
message: `Не заданы переменные окружения: ${[...missingEnv].join(', ')}`,
|
|
316
|
+
hint: 'Задайте ключ в окружении; для opencode CLI-авторизация через `opencode auth login` может работать'
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
id,
|
|
321
|
+
status: 'ok',
|
|
322
|
+
message: `Модели заданы для ${refs.length} агент(ов)`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** Проверяет включённые дополнительные функции */
|
|
326
|
+
function checkFeatures(config) {
|
|
327
|
+
const id = 'features';
|
|
328
|
+
const parts = [];
|
|
329
|
+
if (config.artifacts !== undefined && config.artifacts !== false) {
|
|
330
|
+
try {
|
|
331
|
+
const dir = resolveArtifactRoot({ artifacts: config.artifacts, cwd: config.cwd });
|
|
332
|
+
parts.push(`artifacts → ${dir}`);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
parts.push('artifacts (некорректный путь)');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (config.consensus === true)
|
|
339
|
+
parts.push('consensus');
|
|
340
|
+
if (config.smartExperts === true)
|
|
341
|
+
parts.push('smartExperts');
|
|
342
|
+
if (config.maxCost !== undefined)
|
|
343
|
+
parts.push(`maxCost=$${config.maxCost}`);
|
|
344
|
+
if ((config.fallbackModels?.length ?? 0) > 0) {
|
|
345
|
+
parts.push(`fallbackModels=${config.fallbackModels?.length}`);
|
|
346
|
+
}
|
|
347
|
+
if (config.session !== undefined && config.session.length > 0) {
|
|
348
|
+
parts.push(`session=${config.session}`);
|
|
349
|
+
}
|
|
350
|
+
if (parts.length === 0) {
|
|
351
|
+
return { id, status: 'ok', message: 'Дополнительные функции выключены' };
|
|
352
|
+
}
|
|
353
|
+
return { id, status: 'ok', message: `Включено: ${parts.join(', ')}` };
|
|
354
|
+
}
|
|
355
|
+
/** Читает opencode.json(c) с учётом `.opencode/` и родительских каталогов */
|
|
356
|
+
function readOpenCodeConfig(cwd, configuredPath) {
|
|
357
|
+
const path = findOpenCodeConfigPath(configuredPath, cwd);
|
|
358
|
+
if (path === null) {
|
|
359
|
+
return { path: null, config: null };
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const parsed = readJsonFile(path);
|
|
363
|
+
if (!isRecord(parsed)) {
|
|
364
|
+
return { path, config: null, error: 'ожидается JSON-объект' };
|
|
365
|
+
}
|
|
366
|
+
return { path, config: parsed };
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return { path, config: null, error: errorMessage(error) };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Проверяет opencode.json/opencode.jsonc и ссылки на агентов */
|
|
373
|
+
function checkOpenCodeConfig(config, cwd) {
|
|
374
|
+
const id = 'opencode-config';
|
|
375
|
+
const read = readOpenCodeConfig(cwd, config.opencodeConfigPath);
|
|
376
|
+
if (read.path === null) {
|
|
377
|
+
return {
|
|
378
|
+
id,
|
|
379
|
+
status: 'warn',
|
|
380
|
+
message: 'opencode.json не найден (проект, родители и глобальный конфиг)',
|
|
381
|
+
hint: 'Создайте opencode.json или задайте model экспертам и председателю'
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (read.config === null) {
|
|
385
|
+
return {
|
|
386
|
+
id,
|
|
387
|
+
status: 'fail',
|
|
388
|
+
message: `Не удалось разобрать ${read.path}: ${read.error ?? 'неизвестная ошибка'}`,
|
|
389
|
+
hint: 'Проверьте синтаксис JSON/JSONC'
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const section = readAgentSection(read.config);
|
|
393
|
+
const agents = section.entries;
|
|
394
|
+
const expertNames = collectExpertNames(config);
|
|
395
|
+
const referenced = new Set(expertNames);
|
|
396
|
+
const chairConfig = config.chair;
|
|
397
|
+
if (isRecord(chairConfig)) {
|
|
398
|
+
const chairAgent = chairConfig.agent;
|
|
399
|
+
if (typeof chairAgent === 'string' && chairAgent.trim().length > 0) {
|
|
400
|
+
referenced.add(chairAgent.trim());
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const missing = [];
|
|
404
|
+
const subagents = [];
|
|
405
|
+
for (const name of referenced) {
|
|
406
|
+
const entry = agents[name];
|
|
407
|
+
if (entry === undefined || section.invalid.includes(name)) {
|
|
408
|
+
missing.push(name);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const definition = readAgentDefinition(entry);
|
|
412
|
+
if (expertNames.includes(name) && definition.mode === 'subagent') {
|
|
413
|
+
subagents.push(name);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (missing.length > 0 || subagents.length > 0) {
|
|
417
|
+
const parts = [];
|
|
418
|
+
if (missing.length > 0) {
|
|
419
|
+
parts.push(`агенты не найдены: ${formatNames(missing)}`);
|
|
420
|
+
}
|
|
421
|
+
if (subagents.length > 0) {
|
|
422
|
+
parts.push(`эксперты в режиме subagent: ${formatNames(subagents)}`);
|
|
423
|
+
}
|
|
424
|
+
const hints = [];
|
|
425
|
+
if (missing.length > 0) {
|
|
426
|
+
hints.push('будет использован default agent; задайте model, чтобы маршрутизировать модель');
|
|
427
|
+
}
|
|
428
|
+
if (subagents.length > 0) {
|
|
429
|
+
hints.push('subagent нельзя вызвать напрямую через --agent');
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
id,
|
|
433
|
+
status: 'warn',
|
|
434
|
+
message: `${parts.join('; ')} (формат ${describeConfigFlavor(section.flavor)})`,
|
|
435
|
+
hint: hints.join('; ')
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
id,
|
|
440
|
+
status: 'ok',
|
|
441
|
+
message: `${read.path}: формат ${describeConfigFlavor(section.flavor)}, агентов ${Object.keys(agents).length}, все ссылки корректны`
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Проверяет, не потеряется ли конфигурация при переходе между V1 и V2.
|
|
446
|
+
* Учитывает установленную основную версию OpenCode.
|
|
447
|
+
*/
|
|
448
|
+
function checkOpenCodeV2Compat(cwd, major, configuredPath) {
|
|
449
|
+
const id = 'opencode-v2-compat';
|
|
450
|
+
const read = readOpenCodeConfig(cwd, configuredPath);
|
|
451
|
+
if (read.path === null) {
|
|
452
|
+
return {
|
|
453
|
+
id,
|
|
454
|
+
status: 'ok',
|
|
455
|
+
message: 'opencode.json не найден — проверка V1/V2 пропущена'
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (read.config === null) {
|
|
459
|
+
return {
|
|
460
|
+
id,
|
|
461
|
+
status: 'warn',
|
|
462
|
+
message: `Не удалось прочитать ${read.path}: ${read.error ?? 'неизвестная ошибка'}`,
|
|
463
|
+
hint: 'Проверка совместимости V1/V2 пропущена до исправления синтаксиса'
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const section = readAgentSection(read.config);
|
|
467
|
+
const mcp = readMcpSection(read.config);
|
|
468
|
+
const legacy = findLegacyV1Fields(read.config);
|
|
469
|
+
const problems = [];
|
|
470
|
+
const hints = [];
|
|
471
|
+
if (major === 2) {
|
|
472
|
+
if (Object.keys(mcp.flat).length > 0) {
|
|
473
|
+
problems.push(`плоские MCP-серверы: ${formatNames(Object.keys(mcp.flat))}`);
|
|
474
|
+
hints.push('OpenCode V2 читает только mcp.servers.<name>; выполните consilium mcp --install');
|
|
475
|
+
}
|
|
476
|
+
if (legacy.length > 0) {
|
|
477
|
+
problems.push(`поля V1 без прямого эффекта в V2: ${formatNames(legacy)}`);
|
|
478
|
+
hints.push('theme переносится в cli.json, autoupdate → update, small_model → agents.title.model, snapshot → snapshots, attachment → media');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (major === 1 && section.flavor === 'v2') {
|
|
482
|
+
problems.push('конфигурация в нативном V2-формате');
|
|
483
|
+
hints.push('OpenCode V1 не читает agents/mcp.servers; используйте V1-формат (agent/prompt)');
|
|
484
|
+
}
|
|
485
|
+
if (section.duplicated.length > 0) {
|
|
486
|
+
problems.push(`агенты заданы дважды: ${formatNames(section.duplicated)}`);
|
|
487
|
+
hints.push('V2 использует agents, V1 — agent; оставьте одно определение агента');
|
|
488
|
+
}
|
|
489
|
+
if (problems.length === 0) {
|
|
490
|
+
return {
|
|
491
|
+
id,
|
|
492
|
+
status: 'ok',
|
|
493
|
+
message: `формат ${describeConfigFlavor(section.flavor)} совместим с установленной версией OpenCode`
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
id,
|
|
498
|
+
status: 'warn',
|
|
499
|
+
message: `${problems.join('; ')} (формат ${describeConfigFlavor(section.flavor)})`,
|
|
500
|
+
hint: hints.join('; ')
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** Находит ближайший существующий предок пути */
|
|
504
|
+
function findExistingAncestor(start) {
|
|
505
|
+
let current = start;
|
|
506
|
+
for (;;) {
|
|
507
|
+
if (existsSync(current)) {
|
|
508
|
+
return current;
|
|
509
|
+
}
|
|
510
|
+
const parent = dirname(current);
|
|
511
|
+
if (parent === current) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
current = parent;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/** Проверяет доступность пути для сохранения результата */
|
|
518
|
+
function checkOutputFile(config, cwd) {
|
|
519
|
+
const id = 'output-file';
|
|
520
|
+
const output = config.output;
|
|
521
|
+
if (!isRecord(output)) {
|
|
522
|
+
return {
|
|
523
|
+
id,
|
|
524
|
+
status: 'fail',
|
|
525
|
+
message: 'Секция output отсутствует',
|
|
526
|
+
hint: 'Задайте output.file строкой или false'
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const file = output.file;
|
|
530
|
+
if (file === false) {
|
|
531
|
+
return {
|
|
532
|
+
id,
|
|
533
|
+
status: 'ok',
|
|
534
|
+
message: 'Сохранение результата отключено (output.file = false)'
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (typeof file !== 'string' || file.trim().length === 0) {
|
|
538
|
+
return {
|
|
539
|
+
id,
|
|
540
|
+
status: 'fail',
|
|
541
|
+
message: 'Некорректный output.file',
|
|
542
|
+
hint: 'Укажите непустой путь строкой или false'
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const fullPath = resolve(cwd, file);
|
|
546
|
+
const directory = dirname(fullPath);
|
|
547
|
+
const ancestor = findExistingAncestor(directory);
|
|
548
|
+
if (ancestor === null) {
|
|
549
|
+
return {
|
|
550
|
+
id,
|
|
551
|
+
status: 'fail',
|
|
552
|
+
message: `Не найдена существующая директория для ${fullPath}`,
|
|
553
|
+
hint: 'Проверьте путь output.file'
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
if (!statSync(ancestor).isDirectory()) {
|
|
558
|
+
return {
|
|
559
|
+
id,
|
|
560
|
+
status: 'fail',
|
|
561
|
+
message: `Путь ${ancestor} не является директорией`,
|
|
562
|
+
hint: 'Проверьте путь output.file'
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
accessSync(ancestor, constants.W_OK);
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
return {
|
|
569
|
+
id,
|
|
570
|
+
status: 'fail',
|
|
571
|
+
message: `Нет доступа на запись в ${ancestor}`,
|
|
572
|
+
hint: errorMessage(error)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (existsSync(fullPath)) {
|
|
576
|
+
try {
|
|
577
|
+
if (statSync(fullPath).isDirectory()) {
|
|
578
|
+
return {
|
|
579
|
+
id,
|
|
580
|
+
status: 'fail',
|
|
581
|
+
message: `output.file указывает на директорию: ${fullPath}`,
|
|
582
|
+
hint: 'Укажите путь к файлу'
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
accessSync(fullPath, constants.W_OK);
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
return {
|
|
589
|
+
id,
|
|
590
|
+
status: 'fail',
|
|
591
|
+
message: `Файл ${fullPath} недоступен для записи`,
|
|
592
|
+
hint: errorMessage(error)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
id,
|
|
597
|
+
status: 'ok',
|
|
598
|
+
message: `Файл результата: ${fullPath}`
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const note = directory === ancestor
|
|
602
|
+
? ''
|
|
603
|
+
: ` (каталог ${directory} будет создан)`;
|
|
604
|
+
return {
|
|
605
|
+
id,
|
|
606
|
+
status: 'ok',
|
|
607
|
+
message: `Файл результата: ${fullPath}${note}`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/** Проверяет кэш свободных моделей (сеть не блокирует диагностику) */
|
|
611
|
+
async function checkModelsCache() {
|
|
612
|
+
const id = 'models-cache';
|
|
613
|
+
try {
|
|
614
|
+
const result = await fetchFreeModels({ timeoutMs: 4000 });
|
|
615
|
+
const count = result.models.length;
|
|
616
|
+
const failedSources = result.sources.filter(source => !source.ok);
|
|
617
|
+
if (count === 0) {
|
|
618
|
+
return {
|
|
619
|
+
id,
|
|
620
|
+
status: 'warn',
|
|
621
|
+
message: 'Свободные модели не найдены',
|
|
622
|
+
hint: failedSources.length > 0
|
|
623
|
+
? `Недоступные источники: ${failedSources.map(source => source.source).join(', ')}`
|
|
624
|
+
: 'Обновите кэш командой `consilium models --refresh`'
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
if (failedSources.length > 0) {
|
|
628
|
+
return {
|
|
629
|
+
id,
|
|
630
|
+
status: 'warn',
|
|
631
|
+
message: `Свободных моделей: ${count}, но часть источников недоступна`,
|
|
632
|
+
hint: failedSources
|
|
633
|
+
.map(source => `${source.source}: ${source.error ?? 'ошибка'}`)
|
|
634
|
+
.join('; ')
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (result.fromCache) {
|
|
638
|
+
return {
|
|
639
|
+
id,
|
|
640
|
+
status: 'ok',
|
|
641
|
+
message: `Кэш свободных моделей: ${count}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
id,
|
|
646
|
+
status: 'ok',
|
|
647
|
+
message: `Свободных моделей: ${count}`
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
return {
|
|
652
|
+
id,
|
|
653
|
+
status: 'warn',
|
|
654
|
+
message: `Не удалось проверить каталог моделей: ${errorMessage(error)}`,
|
|
655
|
+
hint: 'Проверьте сеть; на работу консилиума это не влияет'
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Проверяет параллелизм */
|
|
660
|
+
function checkConcurrency(config) {
|
|
661
|
+
const id = 'concurrency';
|
|
662
|
+
const retry = config.retry;
|
|
663
|
+
const value = isRecord(retry) ? retry.maxConcurrency : undefined;
|
|
664
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
665
|
+
return {
|
|
666
|
+
id,
|
|
667
|
+
status: 'warn',
|
|
668
|
+
message: `Некорректный retry.maxConcurrency: ${String(value)}`,
|
|
669
|
+
hint: 'Рекомендуется целое число от 1 до 8'
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (value < 1 || value > 8) {
|
|
673
|
+
return {
|
|
674
|
+
id,
|
|
675
|
+
status: 'warn',
|
|
676
|
+
message: `retry.maxConcurrency = ${value} вне рекомендуемого диапазона 1–8`,
|
|
677
|
+
hint: 'Слишком большое значение нагружает систему и API'
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
id,
|
|
682
|
+
status: 'ok',
|
|
683
|
+
message: `Параллельных агентов: ${value}`
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/** Проверяет режим standalone */
|
|
687
|
+
function checkStandalone(config) {
|
|
688
|
+
if (config.standalone !== false) {
|
|
689
|
+
return {
|
|
690
|
+
id: 'standalone',
|
|
691
|
+
status: 'ok',
|
|
692
|
+
message: 'Изолированный сервер (--standalone) включён'
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
id: 'standalone',
|
|
697
|
+
status: 'warn',
|
|
698
|
+
message: 'Standalone отключён: может конфликтовать с запущенным OpenCode (порт managed service)',
|
|
699
|
+
hint: 'Включите standalone: true (--standalone) для изоляции'
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/** Проверяет авто-подтверждение разрешений */
|
|
703
|
+
function checkAutoApprove(config) {
|
|
704
|
+
if (config.autoApprove === true) {
|
|
705
|
+
return {
|
|
706
|
+
id: 'auto-approve',
|
|
707
|
+
status: 'warn',
|
|
708
|
+
message: 'autoApprove включён: агенты могут выполнять инструменты без подтверждения',
|
|
709
|
+
hint: 'Оставьте autoApprove: false, если не доверяете промптам и инструментам'
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
id: 'auto-approve',
|
|
714
|
+
status: 'ok',
|
|
715
|
+
message: 'Авто-подтверждение разрешений выключено'
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** Выполняет проверку, превращая исключения в fail */
|
|
719
|
+
async function safeCheck(id, fn) {
|
|
720
|
+
try {
|
|
721
|
+
return await fn();
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
return {
|
|
725
|
+
id,
|
|
726
|
+
status: 'fail',
|
|
727
|
+
message: `Проверка не выполнена: ${errorMessage(error)}`
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Запускает все проверки диагностики
|
|
733
|
+
*/
|
|
734
|
+
export async function runDoctor(config, options = {}) {
|
|
735
|
+
const cwd = options.cwd ?? config.cwd ?? process.cwd();
|
|
736
|
+
const checks = [];
|
|
737
|
+
let versionText = null;
|
|
738
|
+
try {
|
|
739
|
+
versionText = await getOpenCodeVersion(config);
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
versionText = null;
|
|
743
|
+
}
|
|
744
|
+
const cliMajor = parseOpenCodeVersion(versionText).major;
|
|
745
|
+
checks.push(await safeCheck('node-version', () => checkNodeVersion()));
|
|
746
|
+
checks.push(await safeCheck('opencode-cli', () => checkOpenCodeCli(versionText)));
|
|
747
|
+
checks.push(await safeCheck('config-file', () => checkConfigFile(options.configPath)));
|
|
748
|
+
checks.push(await safeCheck('experts-config', () => checkExperts(config)));
|
|
749
|
+
checks.push(await safeCheck('chair-config', () => checkChair(config)));
|
|
750
|
+
checks.push(await safeCheck('timeouts', () => checkTimeouts(config)));
|
|
751
|
+
checks.push(await safeCheck('models-format', () => checkModelsFormat(config)));
|
|
752
|
+
checks.push(await safeCheck('opencode-config', () => checkOpenCodeConfig(config, cwd)));
|
|
753
|
+
checks.push(await safeCheck('opencode-v2-compat', () => checkOpenCodeV2Compat(cwd, cliMajor, config.opencodeConfigPath)));
|
|
754
|
+
checks.push(await safeCheck('output-file', () => checkOutputFile(config, cwd)));
|
|
755
|
+
if (options.checkModels !== false) {
|
|
756
|
+
checks.push(await safeCheck('models-cache', () => checkModelsCache()));
|
|
757
|
+
}
|
|
758
|
+
checks.push(await safeCheck('concurrency', () => checkConcurrency(config)));
|
|
759
|
+
checks.push(await safeCheck('features', () => checkFeatures(config)));
|
|
760
|
+
checks.push(await safeCheck('standalone', () => checkStandalone(config)));
|
|
761
|
+
checks.push(await safeCheck('auto-approve', () => checkAutoApprove(config)));
|
|
762
|
+
return checks;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Форматирует отчёт диагностики для консоли
|
|
766
|
+
*/
|
|
767
|
+
export function formatDoctorReport(checks) {
|
|
768
|
+
const icons = {
|
|
769
|
+
ok: '✅',
|
|
770
|
+
warn: '⚠️',
|
|
771
|
+
fail: '❌'
|
|
772
|
+
};
|
|
773
|
+
const lines = [];
|
|
774
|
+
let okCount = 0;
|
|
775
|
+
let warnCount = 0;
|
|
776
|
+
let failCount = 0;
|
|
777
|
+
for (const check of checks) {
|
|
778
|
+
lines.push(`${icons[check.status]} [${check.id}] ${check.message}`);
|
|
779
|
+
if (check.hint) {
|
|
780
|
+
lines.push(` ↳ ${check.hint}`);
|
|
781
|
+
}
|
|
782
|
+
if (check.status === 'ok') {
|
|
783
|
+
okCount += 1;
|
|
784
|
+
}
|
|
785
|
+
else if (check.status === 'warn') {
|
|
786
|
+
warnCount += 1;
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
failCount += 1;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
lines.push('');
|
|
793
|
+
lines.push(`Итого: ${okCount} ok, ${warnCount} warnings, ${failCount} errors`);
|
|
794
|
+
const verdict = failCount > 0 ? 'FAIL' : warnCount > 0 ? 'WARN' : 'PASS';
|
|
795
|
+
lines.push(`Вердикт: ${verdict}`);
|
|
796
|
+
return lines.join('\n');
|
|
797
|
+
}
|
|
798
|
+
//# sourceMappingURL=doctor.js.map
|