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/config.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Загрузчик конфигурации
|
|
3
|
+
* @module lv24-opencode-consilium
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { dirname, resolve } from 'path';
|
|
8
|
+
import { DEFAULT_CONFIG, DEFAULT_EXPERT_PROMPTS, CONFIG_FILE_NAMES, EXCLUDED_AGENTS } from './defaults.js';
|
|
9
|
+
import { readAgentDefinition, readAgentSection } from './compat.js';
|
|
10
|
+
import { isValidVariant, variantOf, withVariant } from './variants.js';
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/** Возвращает объект только для объектных значений */
|
|
15
|
+
function recordOf(value) {
|
|
16
|
+
return isRecord(value) ? value : {};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Удаляет комментарии и висячие запятые из JSONC, не трогая строки
|
|
20
|
+
*/
|
|
21
|
+
export function stripJsonComments(text) {
|
|
22
|
+
let result = '';
|
|
23
|
+
let inString = false;
|
|
24
|
+
let inLineComment = false;
|
|
25
|
+
let inBlockComment = false;
|
|
26
|
+
let escaped = false;
|
|
27
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
28
|
+
const char = text[i] ?? '';
|
|
29
|
+
const next = text[i + 1] ?? '';
|
|
30
|
+
if (inLineComment) {
|
|
31
|
+
if (char === '\n') {
|
|
32
|
+
inLineComment = false;
|
|
33
|
+
result += char;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (inBlockComment) {
|
|
38
|
+
if (char === '*' && next === '/') {
|
|
39
|
+
inBlockComment = false;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inString) {
|
|
45
|
+
result += char;
|
|
46
|
+
if (escaped) {
|
|
47
|
+
escaped = false;
|
|
48
|
+
}
|
|
49
|
+
else if (char === '\\') {
|
|
50
|
+
escaped = true;
|
|
51
|
+
}
|
|
52
|
+
else if (char === '"') {
|
|
53
|
+
inString = false;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === '"') {
|
|
58
|
+
inString = true;
|
|
59
|
+
result += char;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === '/' && next === '/') {
|
|
63
|
+
inLineComment = true;
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (char === '/' && next === '*') {
|
|
68
|
+
inBlockComment = true;
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
result += char;
|
|
73
|
+
}
|
|
74
|
+
return removeTrailingCommas(result);
|
|
75
|
+
}
|
|
76
|
+
function removeTrailingCommas(text) {
|
|
77
|
+
let result = '';
|
|
78
|
+
let inString = false;
|
|
79
|
+
let escaped = false;
|
|
80
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
81
|
+
const char = text[i] ?? '';
|
|
82
|
+
if (inString) {
|
|
83
|
+
result += char;
|
|
84
|
+
if (escaped) {
|
|
85
|
+
escaped = false;
|
|
86
|
+
}
|
|
87
|
+
else if (char === '\\') {
|
|
88
|
+
escaped = true;
|
|
89
|
+
}
|
|
90
|
+
else if (char === '"') {
|
|
91
|
+
inString = false;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === '"') {
|
|
96
|
+
inString = true;
|
|
97
|
+
result += char;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === ',') {
|
|
101
|
+
let lookahead = i + 1;
|
|
102
|
+
while (lookahead < text.length && /\s/.test(text[lookahead] ?? '')) {
|
|
103
|
+
lookahead += 1;
|
|
104
|
+
}
|
|
105
|
+
const following = text[lookahead];
|
|
106
|
+
if (following === '}' || following === ']') {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
result += char;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Читает JSON/JSONC файл
|
|
116
|
+
*/
|
|
117
|
+
export function readJsonFile(path) {
|
|
118
|
+
const content = readFileSync(path, 'utf-8').replace(/^\uFEFF/, '');
|
|
119
|
+
return JSON.parse(stripJsonComments(content));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Глубокое клонирование конфигурации с сохранением функций
|
|
123
|
+
*/
|
|
124
|
+
export function cloneConfig(config) {
|
|
125
|
+
return {
|
|
126
|
+
...config,
|
|
127
|
+
experts: config.experts.map(expert => ({ ...expert })),
|
|
128
|
+
chair: { ...config.chair },
|
|
129
|
+
excludedAgents: config.excludedAgents ? [...config.excludedAgents] : undefined,
|
|
130
|
+
timeouts: { ...config.timeouts },
|
|
131
|
+
retry: { ...config.retry },
|
|
132
|
+
output: { ...config.output },
|
|
133
|
+
hooks: config.hooks ? { ...config.hooks } : undefined,
|
|
134
|
+
opencodeArgs: config.opencodeArgs ? [...config.opencodeArgs] : undefined,
|
|
135
|
+
plugins: config.plugins ? [...config.plugins] : undefined
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Объединяет частичный конфиг с конфигурацией по умолчанию
|
|
140
|
+
*/
|
|
141
|
+
export function mergeWithDefaults(partial) {
|
|
142
|
+
const base = cloneConfig(DEFAULT_CONFIG);
|
|
143
|
+
const result = {
|
|
144
|
+
...base,
|
|
145
|
+
...partial,
|
|
146
|
+
experts: partial.experts ? partial.experts.map(expert => ({ ...expert })) : base.experts,
|
|
147
|
+
chair: { ...base.chair, ...partial.chair },
|
|
148
|
+
excludedAgents: partial.excludedAgents ? [...partial.excludedAgents] : base.excludedAgents,
|
|
149
|
+
timeouts: { ...base.timeouts, ...partial.timeouts },
|
|
150
|
+
retry: { ...base.retry, ...partial.retry },
|
|
151
|
+
output: { ...base.output, ...partial.output },
|
|
152
|
+
hooks: partial.hooks ?? base.hooks,
|
|
153
|
+
opencodeArgs: partial.opencodeArgs ? [...partial.opencodeArgs] : base.opencodeArgs,
|
|
154
|
+
plugins: partial.plugins ? [...partial.plugins] : base.plugins
|
|
155
|
+
};
|
|
156
|
+
// Вариант модели — общий дефолт для моделей без собственного #variant.
|
|
157
|
+
if (result.variant !== undefined && isValidVariant(result.variant)) {
|
|
158
|
+
const variant = result.variant;
|
|
159
|
+
result.experts = result.experts.map(expert => {
|
|
160
|
+
if (expert.model === undefined || !expert.model.includes('/'))
|
|
161
|
+
return expert;
|
|
162
|
+
if (variantOf(expert.model) !== undefined)
|
|
163
|
+
return expert;
|
|
164
|
+
return { ...expert, model: withVariant(expert.model, variant) };
|
|
165
|
+
});
|
|
166
|
+
if (result.chair.model !== undefined &&
|
|
167
|
+
result.chair.model.includes('/') &&
|
|
168
|
+
variantOf(result.chair.model) === undefined) {
|
|
169
|
+
result.chair = {
|
|
170
|
+
...result.chair,
|
|
171
|
+
model: withVariant(result.chair.model, variant)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Возвращает все найденные файлы конфигурации OpenCode в порядке возрастания
|
|
179
|
+
* приоритета: глобальный → прямые файлы от корня к cwd → `.opencode`-файлы
|
|
180
|
+
* от корня к cwd. OpenCode сливает их именно в таком порядке.
|
|
181
|
+
*/
|
|
182
|
+
export function listOpenCodeConfigPaths(cwd) {
|
|
183
|
+
const start = resolve(cwd ?? process.cwd());
|
|
184
|
+
const paths = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const push = (candidate) => {
|
|
187
|
+
if (!seen.has(candidate) && existsSync(candidate)) {
|
|
188
|
+
seen.add(candidate);
|
|
189
|
+
paths.push(candidate);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const configHome = process.env.XDG_CONFIG_HOME || resolve(homedir(), '.config');
|
|
193
|
+
push(resolve(configHome, 'opencode', 'opencode.json'));
|
|
194
|
+
push(resolve(configHome, 'opencode', 'opencode.jsonc'));
|
|
195
|
+
const chain = [];
|
|
196
|
+
let current = start;
|
|
197
|
+
for (let depth = 0; depth < 24; depth += 1) {
|
|
198
|
+
chain.unshift(current);
|
|
199
|
+
const parent = dirname(current);
|
|
200
|
+
if (parent === current)
|
|
201
|
+
break;
|
|
202
|
+
current = parent;
|
|
203
|
+
}
|
|
204
|
+
for (const directory of chain) {
|
|
205
|
+
push(resolve(directory, 'opencode.json'));
|
|
206
|
+
push(resolve(directory, 'opencode.jsonc'));
|
|
207
|
+
}
|
|
208
|
+
for (const directory of chain) {
|
|
209
|
+
push(resolve(directory, '.opencode', 'opencode.json'));
|
|
210
|
+
push(resolve(directory, '.opencode', 'opencode.jsonc'));
|
|
211
|
+
}
|
|
212
|
+
return paths;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Находит основной файл конфигурации OpenCode (наивысший приоритет).
|
|
216
|
+
* Если заданный `opencodeConfigPath` не существует, используется автоопределение.
|
|
217
|
+
*/
|
|
218
|
+
export function findOpenCodeConfigPath(configuredPath, cwd) {
|
|
219
|
+
const start = resolve(cwd ?? process.cwd());
|
|
220
|
+
if (configuredPath) {
|
|
221
|
+
const resolved = resolve(start, configuredPath);
|
|
222
|
+
if (existsSync(resolved))
|
|
223
|
+
return resolved;
|
|
224
|
+
if (!configuredPath.endsWith('.jsonc') && existsSync(`${resolved}c`)) {
|
|
225
|
+
return `${resolved}c`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const paths = listOpenCodeConfigPaths(start);
|
|
229
|
+
return paths.length > 0 ? paths[paths.length - 1] ?? null : null;
|
|
230
|
+
}
|
|
231
|
+
/** Сливает конфигурации OpenCode: агенты и MCP объединяются по именам */
|
|
232
|
+
function mergeOpenCodeConfigs(layers) {
|
|
233
|
+
const merged = {};
|
|
234
|
+
for (const layer of layers) {
|
|
235
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
236
|
+
if (key === 'agent' || key === 'agents') {
|
|
237
|
+
merged[key] = { ...recordOf(merged[key]), ...recordOf(value) };
|
|
238
|
+
}
|
|
239
|
+
else if (key === 'mcp') {
|
|
240
|
+
const base = recordOf(merged.mcp);
|
|
241
|
+
const next = recordOf(value);
|
|
242
|
+
const mcp = { ...base, ...next };
|
|
243
|
+
if (isRecord(base.servers) || isRecord(next.servers)) {
|
|
244
|
+
mcp.servers = { ...recordOf(base.servers), ...recordOf(next.servers) };
|
|
245
|
+
}
|
|
246
|
+
merged.mcp = mcp;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
merged[key] = value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return merged;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Читает агентов из opencode.json, понимая V1 (`agent`) и V2 (`agents`).
|
|
257
|
+
* Без явного пути сливает все слои так же, как OpenCode.
|
|
258
|
+
*/
|
|
259
|
+
export function loadOpenCodeAgents(options = {}) {
|
|
260
|
+
const start = resolve(options.cwd ?? process.cwd());
|
|
261
|
+
const explicit = options.opencodeConfigPath?.trim();
|
|
262
|
+
let files = [];
|
|
263
|
+
let warning;
|
|
264
|
+
if (explicit !== undefined && explicit.length > 0) {
|
|
265
|
+
const resolved = resolve(start, explicit);
|
|
266
|
+
if (existsSync(resolved)) {
|
|
267
|
+
files = [resolved];
|
|
268
|
+
}
|
|
269
|
+
else if (!explicit.endsWith('.jsonc') && existsSync(`${resolved}c`)) {
|
|
270
|
+
files = [`${resolved}c`];
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
files = listOpenCodeConfigPaths(start);
|
|
274
|
+
warning =
|
|
275
|
+
files.length > 0
|
|
276
|
+
? `opencodeConfigPath "${explicit}" не найден; использовано автоопределение: ${files[files.length - 1]}`
|
|
277
|
+
: `opencodeConfigPath "${explicit}" не найден, конфигурация OpenCode не обнаружена`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
files = listOpenCodeConfigPaths(start);
|
|
282
|
+
}
|
|
283
|
+
if (files.length === 0) {
|
|
284
|
+
return { path: null, config: null, section: null, files, warning };
|
|
285
|
+
}
|
|
286
|
+
const layers = [];
|
|
287
|
+
const errors = [];
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
try {
|
|
290
|
+
const config = readJsonFile(file);
|
|
291
|
+
if (isRecord(config)) {
|
|
292
|
+
layers.push(config);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
errors.push(`${file}: ожидается JSON-объект`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
errors.push(`${file}: ${error.message}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const path = files[files.length - 1] ?? null;
|
|
303
|
+
if (layers.length === 0) {
|
|
304
|
+
return {
|
|
305
|
+
path,
|
|
306
|
+
config: null,
|
|
307
|
+
section: null,
|
|
308
|
+
files,
|
|
309
|
+
error: errors.join('; ') || 'конфигурация не прочитана',
|
|
310
|
+
warning
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const merged = mergeOpenCodeConfigs(layers);
|
|
314
|
+
return { path, config: merged, section: readAgentSection(merged), files, warning };
|
|
315
|
+
}
|
|
316
|
+
/** Встроенные агенты OpenCode, которые доступны без записи в opencode.json */
|
|
317
|
+
const BUILTIN_OPENCODE_AGENTS = new Set([
|
|
318
|
+
'build',
|
|
319
|
+
'plan',
|
|
320
|
+
'general',
|
|
321
|
+
'explore',
|
|
322
|
+
'compaction',
|
|
323
|
+
'title',
|
|
324
|
+
'summary'
|
|
325
|
+
]);
|
|
326
|
+
/** Проверяет наличие файлового агента (`.opencode/agents/<name>.md`) */
|
|
327
|
+
function hasAgentFile(agentName, cwd) {
|
|
328
|
+
if (agentName.includes('..'))
|
|
329
|
+
return false;
|
|
330
|
+
const start = resolve(cwd ?? process.cwd());
|
|
331
|
+
const chain = [];
|
|
332
|
+
let current = start;
|
|
333
|
+
for (let depth = 0; depth < 24; depth += 1) {
|
|
334
|
+
chain.push(current);
|
|
335
|
+
const parent = dirname(current);
|
|
336
|
+
if (parent === current)
|
|
337
|
+
break;
|
|
338
|
+
current = parent;
|
|
339
|
+
}
|
|
340
|
+
const directories = [];
|
|
341
|
+
for (const directory of chain) {
|
|
342
|
+
directories.push(resolve(directory, '.opencode', 'agents'));
|
|
343
|
+
directories.push(resolve(directory, '.opencode', 'agent'));
|
|
344
|
+
}
|
|
345
|
+
const configHome = process.env.XDG_CONFIG_HOME || resolve(homedir(), '.config');
|
|
346
|
+
directories.push(resolve(configHome, 'opencode', 'agents'));
|
|
347
|
+
directories.push(resolve(configHome, 'opencode', 'agent'));
|
|
348
|
+
return directories.some(directory => existsSync(resolve(directory, `${agentName}.md`)));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Проверяет, существует ли агент и какую модель он использует.
|
|
352
|
+
*
|
|
353
|
+
* Нужна для OpenCode V2: `opencode run --agent` не применяет модель агента
|
|
354
|
+
* из конфигурации, поэтому её приходится передавать через `--model`, а для
|
|
355
|
+
* несуществующего агента — сразу запускать default-агент.
|
|
356
|
+
*/
|
|
357
|
+
export function resolveOpenCodeAgent(agentName, options = {}) {
|
|
358
|
+
if (BUILTIN_OPENCODE_AGENTS.has(agentName)) {
|
|
359
|
+
return { exists: true };
|
|
360
|
+
}
|
|
361
|
+
if (hasAgentFile(agentName, options.cwd)) {
|
|
362
|
+
return { exists: true };
|
|
363
|
+
}
|
|
364
|
+
const snapshot = loadOpenCodeAgents(options);
|
|
365
|
+
const entry = snapshot.section?.entries[agentName];
|
|
366
|
+
if (entry === undefined) {
|
|
367
|
+
return { exists: false };
|
|
368
|
+
}
|
|
369
|
+
return { exists: true, model: readAgentDefinition(entry).model };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Возвращает модель агента из opencode.json.
|
|
373
|
+
*/
|
|
374
|
+
export function resolveOpenCodeAgentModel(agentName, options = {}) {
|
|
375
|
+
return resolveOpenCodeAgent(agentName, options).model;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Извлекает агентов из раздела opencode.json
|
|
379
|
+
*/
|
|
380
|
+
function extractAgentsFromOpenCode(section, excludedAgents) {
|
|
381
|
+
const excluded = new Set(excludedAgents);
|
|
382
|
+
const agents = [];
|
|
383
|
+
let priority = 1;
|
|
384
|
+
for (const [name, raw] of Object.entries(section.entries)) {
|
|
385
|
+
if (excluded.has(name))
|
|
386
|
+
continue;
|
|
387
|
+
const definition = readAgentDefinition(raw);
|
|
388
|
+
if (definition.mode === 'subagent')
|
|
389
|
+
continue;
|
|
390
|
+
if (definition.disabled)
|
|
391
|
+
continue;
|
|
392
|
+
// prompt в opencode.json — это системный промпт агента, OpenCode применит его сам.
|
|
393
|
+
// Для пользовательского сообщения берём task-aware промпт роли по умолчанию.
|
|
394
|
+
agents.push({
|
|
395
|
+
name,
|
|
396
|
+
enabled: true,
|
|
397
|
+
priority,
|
|
398
|
+
model: definition.model,
|
|
399
|
+
prompt: Object.prototype.hasOwnProperty.call(DEFAULT_EXPERT_PROMPTS, name)
|
|
400
|
+
? DEFAULT_EXPERT_PROMPTS[name]
|
|
401
|
+
: DEFAULT_EXPERT_PROMPTS.default
|
|
402
|
+
});
|
|
403
|
+
priority += 1;
|
|
404
|
+
}
|
|
405
|
+
return agents;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Проверяет формат модели
|
|
409
|
+
*/
|
|
410
|
+
export function isValidModelId(model) {
|
|
411
|
+
return /^[A-Za-z0-9._:@/#+-]+$/.test(model) && model.includes('/');
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Валидирует конфигурацию
|
|
415
|
+
*/
|
|
416
|
+
export function validateConfig(config) {
|
|
417
|
+
const warnings = [];
|
|
418
|
+
if (!Array.isArray(config.experts)) {
|
|
419
|
+
warnings.push('Поле experts должно быть массивом. Используются эксперты по умолчанию.');
|
|
420
|
+
return warnings;
|
|
421
|
+
}
|
|
422
|
+
if (config.experts.length === 0) {
|
|
423
|
+
warnings.push('Список экспертов пуст. Добавьте хотя бы одного эксперта.');
|
|
424
|
+
}
|
|
425
|
+
for (const expert of config.experts) {
|
|
426
|
+
if (!expert || typeof expert.name !== 'string' || expert.name.trim().length === 0) {
|
|
427
|
+
warnings.push('Обнаружен эксперт без имени.');
|
|
428
|
+
}
|
|
429
|
+
else if (!/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/.test(expert.name)) {
|
|
430
|
+
warnings.push(`Некорректное имя эксперта: ${expert.name}`);
|
|
431
|
+
}
|
|
432
|
+
if (expert?.model && !isValidModelId(expert.model)) {
|
|
433
|
+
warnings.push(`Некорректный id модели у эксперта ${expert.name}: ${expert.model}`);
|
|
434
|
+
}
|
|
435
|
+
if (expert?.timeout !== undefined && (!Number.isFinite(expert.timeout) || expert.timeout <= 0)) {
|
|
436
|
+
warnings.push(`Некорректный таймаут эксперта ${expert.name}.`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const names = config.experts.map(e => e?.name).filter((name) => Boolean(name));
|
|
440
|
+
const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
|
|
441
|
+
if (duplicates.length > 0) {
|
|
442
|
+
warnings.push(`Обнаружены дубликаты экспертов: ${[...new Set(duplicates)].join(', ')}`);
|
|
443
|
+
}
|
|
444
|
+
if (!isRecord(config.chair) || typeof config.chair.agent !== 'string' || config.chair.agent.length === 0) {
|
|
445
|
+
warnings.push('Не указан агент-председатель.');
|
|
446
|
+
}
|
|
447
|
+
else if (config.chair.model && !isValidModelId(config.chair.model)) {
|
|
448
|
+
warnings.push(`Некорректный id модели председателя: ${config.chair.model}`);
|
|
449
|
+
}
|
|
450
|
+
if (!isRecord(config.timeouts)) {
|
|
451
|
+
warnings.push('Секция timeouts отсутствует или некорректна.');
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
for (const key of ['expert', 'chair', 'shutdown']) {
|
|
455
|
+
const value = config.timeouts[key];
|
|
456
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
457
|
+
warnings.push(`Некорректный таймаут ${key}.`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (Number.isFinite(config.timeouts.expert) && config.timeouts.expert < 10000) {
|
|
461
|
+
warnings.push('Таймаут эксперта слишком маленький (< 10 сек). Возможны ложные таймауты.');
|
|
462
|
+
}
|
|
463
|
+
if (Number.isFinite(config.timeouts.chair) && config.timeouts.chair < 30000) {
|
|
464
|
+
warnings.push('Таймаут председателя слишком маленький (< 30 сек). Возможны ложные таймауты.');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!isRecord(config.retry)) {
|
|
468
|
+
warnings.push('Секция retry отсутствует или некорректна.');
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
if (!Number.isInteger(config.retry.expertRetries) || config.retry.expertRetries < 0) {
|
|
472
|
+
warnings.push('retry.expertRetries должен быть целым числом >= 0.');
|
|
473
|
+
}
|
|
474
|
+
if (!Number.isInteger(config.retry.chairRetries) || config.retry.chairRetries < 0) {
|
|
475
|
+
warnings.push('retry.chairRetries должен быть целым числом >= 0.');
|
|
476
|
+
}
|
|
477
|
+
if (!Number.isFinite(config.retry.delay) || config.retry.delay < 0) {
|
|
478
|
+
warnings.push('retry.delay должен быть числом >= 0.');
|
|
479
|
+
}
|
|
480
|
+
if (config.retry.jitter !== undefined && typeof config.retry.jitter !== 'boolean') {
|
|
481
|
+
warnings.push('retry.jitter должен быть boolean.');
|
|
482
|
+
}
|
|
483
|
+
if (config.retry.maxConcurrency !== undefined &&
|
|
484
|
+
(!Number.isInteger(config.retry.maxConcurrency) || config.retry.maxConcurrency < 1 || config.retry.maxConcurrency > 8)) {
|
|
485
|
+
warnings.push('retry.maxConcurrency должен быть целым числом от 1 до 8.');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!isRecord(config.output)) {
|
|
489
|
+
warnings.push('Секция output отсутствует или некорректна.');
|
|
490
|
+
}
|
|
491
|
+
else if (!['text', 'markdown', 'json'].includes(config.output.format)) {
|
|
492
|
+
warnings.push(`Неизвестный формат вывода: ${String(config.output.format)}`);
|
|
493
|
+
}
|
|
494
|
+
if (config.maxCost !== undefined &&
|
|
495
|
+
(!Number.isFinite(config.maxCost) || config.maxCost < 0)) {
|
|
496
|
+
warnings.push('maxCost должен быть числом >= 0 (USD).');
|
|
497
|
+
}
|
|
498
|
+
if (config.fallbackModels !== undefined) {
|
|
499
|
+
if (!Array.isArray(config.fallbackModels)) {
|
|
500
|
+
warnings.push('fallbackModels должен быть массивом моделей provider/model.');
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
const invalid = config.fallbackModels.filter(model => typeof model !== 'string' || !isValidModelId(model));
|
|
504
|
+
if (invalid.length > 0) {
|
|
505
|
+
warnings.push(`Некорректные fallbackModels: ${invalid.join(', ')}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (config.session !== undefined &&
|
|
510
|
+
(typeof config.session !== 'string' || config.session.trim().length === 0)) {
|
|
511
|
+
warnings.push('session должен быть непустой строкой.');
|
|
512
|
+
}
|
|
513
|
+
if (config.report !== undefined && !['html', 'pr', 'markdown'].includes(config.report)) {
|
|
514
|
+
warnings.push(`Неизвестный формат отчёта: ${String(config.report)}`);
|
|
515
|
+
}
|
|
516
|
+
if (config.reportFile !== undefined && config.reportFile.trim().length === 0) {
|
|
517
|
+
warnings.push('reportFile должен быть непустым путём.');
|
|
518
|
+
}
|
|
519
|
+
if (config.variant !== undefined && !isValidVariant(config.variant)) {
|
|
520
|
+
warnings.push(`Некорректный вариант модели: ${config.variant}`);
|
|
521
|
+
}
|
|
522
|
+
if (config.debounceMs !== undefined &&
|
|
523
|
+
(!Number.isInteger(config.debounceMs) || config.debounceMs < 0)) {
|
|
524
|
+
warnings.push('debounceMs должен быть целым числом >= 0.');
|
|
525
|
+
}
|
|
526
|
+
return warnings;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Находит файл конфигурации
|
|
530
|
+
*/
|
|
531
|
+
function findConfigFile(explicitPath) {
|
|
532
|
+
if (explicitPath) {
|
|
533
|
+
const resolved = resolve(process.cwd(), explicitPath);
|
|
534
|
+
return existsSync(resolved) ? resolved : null;
|
|
535
|
+
}
|
|
536
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
537
|
+
const filePath = resolve(process.cwd(), fileName);
|
|
538
|
+
if (existsSync(filePath)) {
|
|
539
|
+
return filePath;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Нормализует промпт из конфига
|
|
546
|
+
*/
|
|
547
|
+
function parsePromptConfig(promptConfig, expertName) {
|
|
548
|
+
if (typeof promptConfig === 'string') {
|
|
549
|
+
return promptConfig;
|
|
550
|
+
}
|
|
551
|
+
if (typeof promptConfig === 'function') {
|
|
552
|
+
return promptConfig;
|
|
553
|
+
}
|
|
554
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_EXPERT_PROMPTS, expertName)) {
|
|
555
|
+
return DEFAULT_EXPERT_PROMPTS[expertName] ?? ((task) => task);
|
|
556
|
+
}
|
|
557
|
+
return DEFAULT_EXPERT_PROMPTS.default ?? ((task) => task);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Загружает конфигурацию из файла
|
|
561
|
+
*/
|
|
562
|
+
export function loadConfig(explicitPath) {
|
|
563
|
+
const warnings = [];
|
|
564
|
+
const errors = [];
|
|
565
|
+
const configPath = findConfigFile(explicitPath);
|
|
566
|
+
if (explicitPath && !configPath) {
|
|
567
|
+
const message = `Файл конфигурации не найден: ${explicitPath}. Используются значения по умолчанию.`;
|
|
568
|
+
warnings.push(message);
|
|
569
|
+
errors.push(message);
|
|
570
|
+
}
|
|
571
|
+
let userConfig = {};
|
|
572
|
+
if (configPath) {
|
|
573
|
+
try {
|
|
574
|
+
const parsed = readJsonFile(configPath);
|
|
575
|
+
if (isRecord(parsed)) {
|
|
576
|
+
userConfig = parsed;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
const message = `Конфигурация ${configPath} должна быть JSON-объектом.`;
|
|
580
|
+
warnings.push(message);
|
|
581
|
+
errors.push(message);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
const message = `Ошибка при чтении ${configPath}: ${error.message}`;
|
|
586
|
+
warnings.push(message);
|
|
587
|
+
errors.push(message);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (Array.isArray(userConfig.experts)) {
|
|
591
|
+
userConfig.experts = userConfig.experts
|
|
592
|
+
.filter((expert) => isRecord(expert) && typeof expert.name === 'string')
|
|
593
|
+
.map(expert => ({
|
|
594
|
+
...expert,
|
|
595
|
+
prompt: parsePromptConfig(expert.prompt, expert.name)
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
else if (userConfig.experts !== undefined) {
|
|
599
|
+
warnings.push('Поле experts должно быть массивом. Используются эксперты по умолчанию.');
|
|
600
|
+
delete userConfig.experts;
|
|
601
|
+
}
|
|
602
|
+
// Автоопределение агентов из opencode.json, если эксперты не заданы явно
|
|
603
|
+
if (userConfig.autoDiscoverAgents !== false) {
|
|
604
|
+
const snapshot = loadOpenCodeAgents({
|
|
605
|
+
opencodeConfigPath: userConfig.opencodeConfigPath,
|
|
606
|
+
cwd: userConfig.cwd
|
|
607
|
+
});
|
|
608
|
+
if (snapshot.warning !== undefined) {
|
|
609
|
+
warnings.push(snapshot.warning);
|
|
610
|
+
}
|
|
611
|
+
if (snapshot.section !== null) {
|
|
612
|
+
const section = snapshot.section;
|
|
613
|
+
const excludedAgents = [...EXCLUDED_AGENTS, ...(userConfig.excludedAgents || [])];
|
|
614
|
+
const discovered = extractAgentsFromOpenCode(section, excludedAgents);
|
|
615
|
+
if (discovered.length > 0 && !userConfig.experts) {
|
|
616
|
+
userConfig.experts = discovered;
|
|
617
|
+
}
|
|
618
|
+
if (section.duplicated.length > 0) {
|
|
619
|
+
warnings.push(`Агенты заданы и в agent, и в agents (OpenCode V2 использует agents): ${section.duplicated.join(', ')}. ` +
|
|
620
|
+
'Оставьте одно определение, иначе V1 и V2 могут взять разные модели.');
|
|
621
|
+
}
|
|
622
|
+
// Модель председателя тоже берём из opencode.json, чтобы отчёты были точными.
|
|
623
|
+
const chairName = userConfig.chair?.agent ?? DEFAULT_CONFIG.chair.agent;
|
|
624
|
+
const chairEntry = section.entries[chairName];
|
|
625
|
+
const chairModel = chairEntry === undefined ? undefined : readAgentDefinition(chairEntry).model;
|
|
626
|
+
if (chairModel !== undefined && chairModel.length > 0) {
|
|
627
|
+
userConfig.chair = {
|
|
628
|
+
...(userConfig.chair ?? { agent: DEFAULT_CONFIG.chair.agent }),
|
|
629
|
+
model: chairModel
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (snapshot.path !== null) {
|
|
634
|
+
warnings.push(`Не удалось прочитать ${snapshot.path}: ${snapshot.error ?? 'неизвестная ошибка'}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const mergedConfig = mergeWithDefaults(userConfig);
|
|
638
|
+
warnings.push(...validateConfig(mergedConfig));
|
|
639
|
+
return {
|
|
640
|
+
config: mergedConfig,
|
|
641
|
+
configPath,
|
|
642
|
+
warnings,
|
|
643
|
+
errors
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Создаёт минимальный конфиг
|
|
648
|
+
*/
|
|
649
|
+
export function createMinimalConfig(experts = []) {
|
|
650
|
+
const expertConfigs = experts.length > 0
|
|
651
|
+
? experts.map((name, index) => ({
|
|
652
|
+
name,
|
|
653
|
+
enabled: true,
|
|
654
|
+
priority: index + 1,
|
|
655
|
+
prompt: Object.prototype.hasOwnProperty.call(DEFAULT_EXPERT_PROMPTS, name)
|
|
656
|
+
? DEFAULT_EXPERT_PROMPTS[name] ?? ((task) => task)
|
|
657
|
+
: DEFAULT_EXPERT_PROMPTS.default ?? ((task) => task)
|
|
658
|
+
}))
|
|
659
|
+
: DEFAULT_CONFIG.experts.map(expert => ({ ...expert }));
|
|
660
|
+
return mergeWithDefaults({ experts: expertConfigs });
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Генерирует пример конфигурационного файла
|
|
664
|
+
*/
|
|
665
|
+
export function generateExampleConfig() {
|
|
666
|
+
return JSON.stringify({
|
|
667
|
+
version: '1.4.0',
|
|
668
|
+
experts: [
|
|
669
|
+
{
|
|
670
|
+
name: 'arch',
|
|
671
|
+
enabled: true,
|
|
672
|
+
priority: 1,
|
|
673
|
+
timeout: 300000,
|
|
674
|
+
retries: 1,
|
|
675
|
+
prompt: 'Как архитектор, проанализируй задачу кратко (3-5 предложений): ${task}'
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: 'ux',
|
|
679
|
+
enabled: true,
|
|
680
|
+
priority: 2
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: 'ba',
|
|
684
|
+
enabled: true,
|
|
685
|
+
priority: 3
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'sec',
|
|
689
|
+
enabled: true,
|
|
690
|
+
priority: 4
|
|
691
|
+
}
|
|
692
|
+
],
|
|
693
|
+
chair: {
|
|
694
|
+
agent: 'consilium',
|
|
695
|
+
timeout: 600000,
|
|
696
|
+
maxExpertTextLength: 3000
|
|
697
|
+
},
|
|
698
|
+
excludedAgents: [...EXCLUDED_AGENTS],
|
|
699
|
+
timeouts: {
|
|
700
|
+
expert: 300000,
|
|
701
|
+
chair: 600000,
|
|
702
|
+
shutdown: 5000
|
|
703
|
+
},
|
|
704
|
+
retry: {
|
|
705
|
+
expertRetries: 1,
|
|
706
|
+
chairRetries: 1,
|
|
707
|
+
delay: 1000,
|
|
708
|
+
exponentialBackoff: true,
|
|
709
|
+
maxConcurrency: 3
|
|
710
|
+
},
|
|
711
|
+
output: {
|
|
712
|
+
file: false,
|
|
713
|
+
format: 'markdown',
|
|
714
|
+
progress: true,
|
|
715
|
+
logLevel: 'info'
|
|
716
|
+
},
|
|
717
|
+
standalone: true,
|
|
718
|
+
autoApprove: false,
|
|
719
|
+
autoDiscoverAgents: true
|
|
720
|
+
}, null, 2);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Возвращает директорию конфигурационного файла (для относительных путей)
|
|
724
|
+
*/
|
|
725
|
+
export function configDirectory(configPath) {
|
|
726
|
+
return configPath ? dirname(configPath) : process.cwd();
|
|
727
|
+
}
|
|
728
|
+
//# sourceMappingURL=config.js.map
|