lv24-opencode-consilium 0.0.0-stage → 1.4.1

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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +511 -3
  3. package/bin/cli.js +19 -0
  4. package/bin/mcp.js +24 -0
  5. package/consilium.config.example.json +84 -0
  6. package/dist/agents.d.ts +79 -0
  7. package/dist/agents.d.ts.map +1 -0
  8. package/dist/agents.js +1051 -0
  9. package/dist/agents.js.map +1 -0
  10. package/dist/artifacts.d.ts +75 -0
  11. package/dist/artifacts.d.ts.map +1 -0
  12. package/dist/artifacts.js +464 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/cli.d.ts +12 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +1392 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/compat.d.ts +160 -0
  19. package/dist/compat.d.ts.map +1 -0
  20. package/dist/compat.js +419 -0
  21. package/dist/compat.js.map +1 -0
  22. package/dist/config.d.ts +112 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +728 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/consensus.d.ts +73 -0
  27. package/dist/consensus.d.ts.map +1 -0
  28. package/dist/consensus.js +273 -0
  29. package/dist/consensus.js.map +1 -0
  30. package/dist/defaults.d.ts +34 -0
  31. package/dist/defaults.d.ts.map +1 -0
  32. package/dist/defaults.js +102 -0
  33. package/dist/defaults.js.map +1 -0
  34. package/dist/doctor.d.ts +27 -0
  35. package/dist/doctor.d.ts.map +1 -0
  36. package/dist/doctor.js +798 -0
  37. package/dist/doctor.js.map +1 -0
  38. package/dist/fallback.d.ts +50 -0
  39. package/dist/fallback.d.ts.map +1 -0
  40. package/dist/fallback.js +134 -0
  41. package/dist/fallback.js.map +1 -0
  42. package/dist/git.d.ts +28 -0
  43. package/dist/git.d.ts.map +1 -0
  44. package/dist/git.js +71 -0
  45. package/dist/git.js.map +1 -0
  46. package/dist/index.d.ts +100 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +653 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/mcp.d.ts +20 -0
  51. package/dist/mcp.d.ts.map +1 -0
  52. package/dist/mcp.js +1215 -0
  53. package/dist/mcp.js.map +1 -0
  54. package/dist/models.d.ts +97 -0
  55. package/dist/models.d.ts.map +1 -0
  56. package/dist/models.js +873 -0
  57. package/dist/models.js.map +1 -0
  58. package/dist/plugin.d.ts +59 -0
  59. package/dist/plugin.d.ts.map +1 -0
  60. package/dist/plugin.js +223 -0
  61. package/dist/plugin.js.map +1 -0
  62. package/dist/presets.d.ts +27 -0
  63. package/dist/presets.d.ts.map +1 -0
  64. package/dist/presets.js +86 -0
  65. package/dist/presets.js.map +1 -0
  66. package/dist/replay.d.ts +37 -0
  67. package/dist/replay.d.ts.map +1 -0
  68. package/dist/replay.js +103 -0
  69. package/dist/replay.js.map +1 -0
  70. package/dist/report.d.ts +28 -0
  71. package/dist/report.d.ts.map +1 -0
  72. package/dist/report.js +300 -0
  73. package/dist/report.js.map +1 -0
  74. package/dist/smart-experts.d.ts +44 -0
  75. package/dist/smart-experts.d.ts.map +1 -0
  76. package/dist/smart-experts.js +242 -0
  77. package/dist/smart-experts.js.map +1 -0
  78. package/dist/stream.d.ts +50 -0
  79. package/dist/stream.d.ts.map +1 -0
  80. package/dist/stream.js +219 -0
  81. package/dist/stream.js.map +1 -0
  82. package/dist/tool-run.d.ts +49 -0
  83. package/dist/tool-run.d.ts.map +1 -0
  84. package/dist/tool-run.js +143 -0
  85. package/dist/tool-run.js.map +1 -0
  86. package/dist/types.d.ts +352 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +6 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/variants.d.ts +30 -0
  91. package/dist/variants.d.ts.map +1 -0
  92. package/dist/variants.js +111 -0
  93. package/dist/variants.js.map +1 -0
  94. package/dist/verify.d.ts +33 -0
  95. package/dist/verify.d.ts.map +1 -0
  96. package/dist/verify.js +80 -0
  97. package/dist/verify.js.map +1 -0
  98. package/dist/watch.d.ts +73 -0
  99. package/dist/watch.d.ts.map +1 -0
  100. package/dist/watch.js +350 -0
  101. package/dist/watch.js.map +1 -0
  102. package/docs/README.ru.md +508 -0
  103. package/docs/README.zh.md +196 -0
  104. package/docs/session-and-plugin.md +642 -0
  105. package/package.json +88 -6
  106. package/schema/config.schema.json +100 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1392 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview CLI интерфейс для Consilium
4
+ * @module lv24-opencode-consilium/cli
5
+ */
6
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { dirname, relative, resolve } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { Consilium, loadConfig, generateExampleConfig, mergeWithDefaults } from './index.js';
11
+ import { buildCouncilAgents, buildConsiliumConfigFromModels, fetchFreeModels, selectCouncilModels } from './models.js';
12
+ import { applyPreset, listPresets } from './presets.js';
13
+ import { getGitDiff, formatDiffForPrompt } from './git.js';
14
+ import { formatDoctorReport, runDoctor } from './doctor.js';
15
+ import { verifyModels } from './verify.js';
16
+ import { startMcpServer } from './mcp.js';
17
+ import { getOpenCodeVersion } from './agents.js';
18
+ import { findOpenCodeConfigPath, readJsonFile } from './config.js';
19
+ import { diffArtifacts, loadArtifact, resolveArtifactRoot } from './artifacts.js';
20
+ import { renderReport } from './report.js';
21
+ import { planReplay } from './replay.js';
22
+ import { isValidVariant, variantOf, withVariant } from './variants.js';
23
+ import { createProjectWatcher, installPreCommitHook, uninstallPreCommitHook } from './watch.js';
24
+ import { parseOpenCodeVersion, readAgentDefinition, readAgentSection, readMcpSection, upsertAgents, upsertMcpServer } from './compat.js';
25
+ const COMMANDS = new Set(['run', 'init', 'models', 'doctor', 'mcp', 'watch', 'hooks', 'help', 'version']);
26
+ class UsageError extends Error {
27
+ }
28
+ function requireValue(argv, index, flag) {
29
+ const value = argv[index];
30
+ if (value === undefined || (value.startsWith('-') && value !== '-')) {
31
+ throw new UsageError(`Флаг ${flag} требует значение`);
32
+ }
33
+ return value;
34
+ }
35
+ function requireNumericValue(argv, index, flag) {
36
+ const value = argv[index];
37
+ if (value === undefined) {
38
+ throw new UsageError(`Флаг ${flag} требует значение`);
39
+ }
40
+ return value;
41
+ }
42
+ function parsePositiveInt(value, flag) {
43
+ const parsed = Number(value);
44
+ if (!Number.isInteger(parsed) || parsed <= 0) {
45
+ throw new UsageError(`Флаг ${flag} ожидает положительное целое число, получено: ${value}`);
46
+ }
47
+ return parsed;
48
+ }
49
+ function parseNonNegativeInt(value, flag) {
50
+ const parsed = Number(value);
51
+ if (!Number.isInteger(parsed) || parsed < 0) {
52
+ throw new UsageError(`Флаг ${flag} ожидает целое число >= 0, получено: ${value}`);
53
+ }
54
+ return parsed;
55
+ }
56
+ function parseNonNegativeFloat(value, flag) {
57
+ const parsed = Number(value);
58
+ if (!Number.isFinite(parsed) || parsed < 0) {
59
+ throw new UsageError(`Флаг ${flag} ожидает число >= 0, получено: ${value}`);
60
+ }
61
+ return parsed;
62
+ }
63
+ function parseCsv(value) {
64
+ return value
65
+ .split(',')
66
+ .map(item => item.trim())
67
+ .filter(item => item.length > 0);
68
+ }
69
+ /**
70
+ * Парсит аргументы командной строки
71
+ */
72
+ export function parseArgs(argv) {
73
+ const options = { task: '' };
74
+ const taskParts = [];
75
+ let afterTerminator = false;
76
+ for (let i = 0; i < argv.length; i += 1) {
77
+ const arg = argv[i] ?? '';
78
+ if (afterTerminator) {
79
+ taskParts.push(arg);
80
+ continue;
81
+ }
82
+ switch (arg) {
83
+ case '--':
84
+ afterTerminator = true;
85
+ break;
86
+ case '-h':
87
+ case '--help':
88
+ options.help = true;
89
+ break;
90
+ case '-v':
91
+ case '--version':
92
+ options.version = true;
93
+ break;
94
+ case '-V':
95
+ case '--verbose':
96
+ options.verbose = true;
97
+ break;
98
+ case '-s':
99
+ case '--silent':
100
+ options.silent = true;
101
+ break;
102
+ case '-c':
103
+ case '--config':
104
+ options.config = requireValue(argv, ++i, arg);
105
+ break;
106
+ case '-e':
107
+ case '--experts':
108
+ options.experts = requireValue(argv, ++i, arg);
109
+ break;
110
+ case '--chair':
111
+ options.chair = requireValue(argv, ++i, arg);
112
+ break;
113
+ case '--expert-timeout':
114
+ options.expertTimeout = parsePositiveInt(requireNumericValue(argv, ++i, arg), arg);
115
+ break;
116
+ case '--chair-timeout':
117
+ options.chairTimeout = parsePositiveInt(requireNumericValue(argv, ++i, arg), arg);
118
+ break;
119
+ case '-o':
120
+ case '--output':
121
+ options.output = requireValue(argv, ++i, arg);
122
+ break;
123
+ case '-f':
124
+ case '--format': {
125
+ const format = requireValue(argv, ++i, arg);
126
+ if (format !== 'text' && format !== 'markdown' && format !== 'json') {
127
+ throw new UsageError(`Неизвестный формат: ${format} (text, markdown, json)`);
128
+ }
129
+ options.format = format;
130
+ break;
131
+ }
132
+ case '-l':
133
+ case '--log-level': {
134
+ const level = requireValue(argv, ++i, arg);
135
+ if (!['silent', 'error', 'warn', 'info', 'debug'].includes(level)) {
136
+ throw new UsageError(`Неизвестный уровень логирования: ${level}`);
137
+ }
138
+ options.logLevel = level;
139
+ break;
140
+ }
141
+ case '-r':
142
+ case '--retries':
143
+ options.retries = parseNonNegativeInt(requireNumericValue(argv, ++i, arg), arg);
144
+ break;
145
+ case '--preset':
146
+ options.preset = requireValue(argv, ++i, arg);
147
+ break;
148
+ case '--limit':
149
+ options.limit = parsePositiveInt(requireNumericValue(argv, ++i, arg), arg);
150
+ break;
151
+ case '--task-file':
152
+ options.taskFile = requireValue(argv, ++i, arg);
153
+ break;
154
+ case '--base':
155
+ options.diffBase = requireValue(argv, ++i, arg);
156
+ break;
157
+ case '--list-agents':
158
+ options.listAgents = true;
159
+ break;
160
+ case '--init':
161
+ options.init = true;
162
+ break;
163
+ case '--dry-run':
164
+ options.dryRun = true;
165
+ break;
166
+ case '--free':
167
+ options.free = true;
168
+ break;
169
+ case '--refresh':
170
+ options.refresh = true;
171
+ break;
172
+ case '--available':
173
+ options.available = true;
174
+ break;
175
+ case '--json':
176
+ options.json = true;
177
+ options.format = 'json';
178
+ break;
179
+ case '--apply':
180
+ options.apply = true;
181
+ break;
182
+ case '--force':
183
+ options.force = true;
184
+ break;
185
+ case '--diff':
186
+ options.diff = true;
187
+ break;
188
+ case '--staged':
189
+ options.diffStaged = true;
190
+ break;
191
+ case '--no-save':
192
+ options.noSave = true;
193
+ break;
194
+ case '--verify':
195
+ options.verify = true;
196
+ break;
197
+ case '--no-verify':
198
+ options.noVerify = true;
199
+ break;
200
+ case '--max-cost':
201
+ options.maxCost = parseNonNegativeFloat(requireNumericValue(argv, ++i, arg), arg);
202
+ break;
203
+ case '--fallback-models':
204
+ options.fallbackModels = parseCsv(requireValue(argv, ++i, arg));
205
+ break;
206
+ case '--session':
207
+ options.session = requireValue(argv, ++i, arg);
208
+ break;
209
+ case '--report': {
210
+ const format = requireValue(argv, ++i, arg);
211
+ if (format !== 'html' && format !== 'pr' && format !== 'markdown') {
212
+ throw new UsageError(`Неизвестный формат отчёта: ${format} (html, pr, markdown)`);
213
+ }
214
+ options.report = format;
215
+ break;
216
+ }
217
+ case '--report-file':
218
+ options.reportFile = requireValue(argv, ++i, arg);
219
+ break;
220
+ case '--variant':
221
+ options.variant = requireValue(argv, ++i, arg);
222
+ break;
223
+ case '--debounce':
224
+ options.debounceMs = parseNonNegativeInt(requireNumericValue(argv, ++i, arg), arg);
225
+ break;
226
+ case '--artifacts':
227
+ options.artifacts = true;
228
+ break;
229
+ case '--artifacts-dir':
230
+ options.artifactsDir = requireValue(argv, ++i, arg);
231
+ options.artifacts = options.artifactsDir;
232
+ break;
233
+ case '--no-artifacts':
234
+ options.noArtifacts = true;
235
+ break;
236
+ case '--consensus':
237
+ options.consensus = true;
238
+ break;
239
+ case '--no-consensus':
240
+ options.noConsensus = true;
241
+ break;
242
+ case '--smart-experts':
243
+ options.smartExperts = true;
244
+ break;
245
+ case '--replay':
246
+ options.replay = requireValue(argv, ++i, arg);
247
+ break;
248
+ case '--diff-runs': {
249
+ const left = requireValue(argv, ++i, arg);
250
+ const right = requireValue(argv, ++i, arg);
251
+ options.diffRuns = [left, right];
252
+ break;
253
+ }
254
+ case '--native-v2':
255
+ options.nativeV2 = true;
256
+ break;
257
+ case '--install':
258
+ options.mcpInstall = true;
259
+ break;
260
+ case '--global':
261
+ options.globalConfig = true;
262
+ break;
263
+ default:
264
+ if (arg === '-') {
265
+ options.taskFile = '-';
266
+ }
267
+ else if (!arg.startsWith('-')) {
268
+ if (options.command === undefined && taskParts.length === 0 && COMMANDS.has(arg)) {
269
+ options.command = arg;
270
+ }
271
+ else {
272
+ taskParts.push(arg);
273
+ }
274
+ }
275
+ else {
276
+ throw new UsageError(`Неизвестный флаг: ${arg}. Используйте --help.`);
277
+ }
278
+ break;
279
+ }
280
+ }
281
+ options.task = taskParts.join(' ').trim();
282
+ if (options.command === 'hooks') {
283
+ const action = options.task.trim();
284
+ if (action === 'install' || action === 'uninstall') {
285
+ options.hookAction = action;
286
+ options.task = '';
287
+ }
288
+ }
289
+ if (options.help)
290
+ options.command = 'help';
291
+ if (options.version)
292
+ options.command = 'version';
293
+ if (options.init && options.command === undefined)
294
+ options.command = 'init';
295
+ return options;
296
+ }
297
+ function printHelp() {
298
+ console.log(`
299
+ consilium - Консилиум ИИ-экспертов через OpenCode CLI
300
+
301
+ ИСПОЛЬЗОВАНИЕ:
302
+ consilium [опции] <задача>
303
+ consilium <задача> - задача читается из stdin
304
+ consilium init [--free]
305
+ consilium models [--available] [--json]
306
+ consilium doctor
307
+ consilium mcp
308
+
309
+ ОСНОВНЫЕ ОПЦИИ:
310
+ -h, --help Показать эту справку
311
+ -v, --version Показать версию
312
+ -V, --verbose Подробный вывод
313
+ -s, --silent Тихий режим (только результат)
314
+
315
+ -c, --config <файл> Путь к файлу конфигурации
316
+ -e, --experts <список> Эксперты через запятую (arch,ux,ba,sec)
317
+ --chair <агент> Агент-председатель
318
+ --preset <имя> Пресет: fast, deep, security, review, free
319
+
320
+ --expert-timeout <мс> Таймаут экспертов (по умолчанию: 300000)
321
+ --chair-timeout <мс> Таймаут председателя (по умолчанию: 600000)
322
+ -r, --retries <число> Повторные попытки
323
+
324
+ -o, --output <файл> Сохранить результат в файл
325
+ -f, --format <формат> Формат: text, markdown, json (json — чистый JSON в stdout)
326
+ --no-save Не сохранять результат
327
+ --verify (models/init) Проверить модели реальным запросом
328
+ --no-verify (init) Не проверять модели (быстрее)
329
+
330
+ --diff Добавить git diff текущих изменений к задаче
331
+ --staged Использовать staged-изменения
332
+ --base <ref> Сравнить с веткой/коммитом
333
+ --task-file <файл> Прочитать задачу из файла
334
+
335
+ АНАЛИЗ И АРТЕФАКТЫ:
336
+ --consensus Анализ согласия экспертов (в промпт председателя и отчёт)
337
+ --no-consensus Отключить анализ согласия
338
+ --smart-experts Выбрать 2–3 релевантных эксперта по задаче/диффу
339
+ --max-cost <usd> Лимит стоимости; при превышении председатель не запускается
340
+ --fallback-models a,b Запасные модели при RATE_LIMITED/MODEL_UNAVAILABLE/EMPTY_RESPONSE
341
+ --session <id> Продолжить сессию председателя; эксперты получают свои сессии
342
+ --artifacts Сохранить запуск в .consilium/runs
343
+ --artifacts-dir <путь> Сохранить артефакты в указанный каталог
344
+ --no-artifacts Не сохранять артефакты
345
+ --replay <id> Повторить сохранённый запуск
346
+ --diff-runs <a> <b> Сравнить два артефакта
347
+ --report <формат> Отчёт после запуска: html, pr, markdown
348
+ --report-file <файл> Записать отчёт в файл (по умолчанию stdout)
349
+ --variant <имя> Вариант для моделей без #variant (существующий сохраняется)
350
+
351
+ РЕЖИМ НАБЛЮДЕНИЯ:
352
+ watch [--debounce <мс>] [задача]
353
+ Перезапускать консилиум при изменениях в проекте
354
+ hooks install|uninstall Установить/удалить git pre-commit hook
355
+
356
+ КОМАНДЫ:
357
+ init [--free] [--force] [--dry-run] [--native-v2]
358
+ Создать конфиг; --free — подобрать и проверить сильнейшие бесплатные модели
359
+ models [--available] [--verify] [--limit N] [--refresh] [--json]
360
+ Рейтинг бесплатных моделей (models.dev + OpenRouter)
361
+ doctor Проверить окружение, конфиг, агентов и модели
362
+ mcp [--install] [--global]
363
+ Запустить MCP-сервер (stdio) или зарегистрировать его в opencode.json
364
+
365
+ СОВМЕСТИМОСТЬ:
366
+ Работает с OpenCode V1 и V2. Формат opencode.json определяется автоматически:
367
+ V1-формат (agent/prompt) читают обе версии, нативный V2 (agents/system)
368
+ пишется только по флагу --native-v2.
369
+
370
+ БЕСПЛАТНЫЕ МОДЕЛИ:
371
+ consilium init --free
372
+ Скачивает каталог моделей, выбирает сильнейшие бесплатные с поддержкой tools
373
+ и прописывает их в opencode.json и consilium.config.json.
374
+ --refresh — обновить кэш, --dry-run — только показать план.
375
+
376
+ MCP (OpenCode V1/V2, Claude Desktop, Codex, любой MCP-харнесс):
377
+ consilium mcp --install # ./opencode.json → mcp.servers.consilium (V2) или mcp.consilium (V1)
378
+ consilium mcp --install --global # глобальный конфиг OpenCode
379
+
380
+ Claude Desktop / Codex:
381
+ {
382
+ "mcpServers": {
383
+ "consilium": {
384
+ "command": "npx",
385
+ "args": ["-y", "lv24-opencode-consilium", "mcp"]
386
+ }
387
+ }
388
+ }
389
+
390
+ ПРИМЕРЫ:
391
+ consilium "Оценить архитектуру проекта"
392
+ consilium -e arch,sec "Проверить безопасность API"
393
+ git diff | consilium - --diff "Сделай ревью изменений"
394
+ consilium models --available
395
+ consilium doctor
396
+
397
+ ДОКУМЕНТАЦИЯ:
398
+ https://github.com/LevapVeeskela/lv24-opencode-consilium
399
+ `);
400
+ }
401
+ function readVersion() {
402
+ try {
403
+ const packagePath = fileURLToPath(new URL('../package.json', import.meta.url));
404
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf-8'));
405
+ return typeof parsed.version === 'string' ? parsed.version : '0.0.0';
406
+ }
407
+ catch {
408
+ return '0.0.0';
409
+ }
410
+ }
411
+ function log(options, message) {
412
+ if (!options.silent)
413
+ console.error(message);
414
+ }
415
+ function setupConsoleHooks(config, options) {
416
+ if (options.silent)
417
+ return;
418
+ const startTimes = new Map();
419
+ config.hooks = {
420
+ ...config.hooks,
421
+ onExpertStart: (expert) => {
422
+ startTimes.set(expert, Date.now());
423
+ console.error(`⏳ [${expert}] Запуск...`);
424
+ },
425
+ onExpertEnd: (result) => {
426
+ const started = startTimes.get(result.agent) ?? Date.now();
427
+ const duration = ((result.duration ?? (Date.now() - started)) / 1000).toFixed(1);
428
+ if (result.success) {
429
+ const model = result.model ? ` · ${result.model}` : '';
430
+ console.error(`✅ [${result.agent}] Готово (${duration}с, ${result.text.length} симв.)${model}`);
431
+ }
432
+ else {
433
+ console.error(`❌ [${result.agent}] ${result.errorCode ?? 'ОШИБКА'}: ${result.errorMessage ?? 'нет ответа'} (${duration}с)`);
434
+ }
435
+ startTimes.delete(result.agent);
436
+ },
437
+ onChairStart: () => {
438
+ console.error('⏳ [chair] Синтез запущен...');
439
+ },
440
+ onChairEnd: (result) => {
441
+ const duration = ((result.duration ?? 0) / 1000).toFixed(1);
442
+ if (result.success) {
443
+ console.error(`✅ [chair] Синтез завершён (${duration}с, ${result.text.length} симв.)`);
444
+ }
445
+ else {
446
+ console.error(`❌ [chair] Ошибка синтеза: ${result.errorMessage ?? result.errorCode ?? 'нет ответа'}`);
447
+ }
448
+ },
449
+ onLog: (level, message) => {
450
+ console.error(`[${level}] ${message}`);
451
+ }
452
+ };
453
+ }
454
+ function printResult(result, options) {
455
+ const format = options.format ?? 'text';
456
+ if (format === 'json') {
457
+ console.log(JSON.stringify(result, null, 2));
458
+ return;
459
+ }
460
+ console.log(result.text);
461
+ }
462
+ function printSummary(result, options) {
463
+ if (options.silent)
464
+ return;
465
+ const successful = result.experts.filter(expert => expert.success).length;
466
+ console.error('─'.repeat(50));
467
+ console.error(`⏱️ Время: ${(result.totalTime / 1000).toFixed(1)}с · эксперты: ${successful}/${result.experts.length}`);
468
+ if (result.usage?.cost && result.usage.cost > 0) {
469
+ console.error(`💰 Стоимость: $${result.usage.cost.toFixed(6)}`);
470
+ }
471
+ else if (result.usage) {
472
+ console.error('💰 Стоимость: $0 (бесплатные модели)');
473
+ }
474
+ if (typeof result.metadata?.savedFile === 'string') {
475
+ console.error(`💾 Сохранено: ${result.metadata.savedFile}`);
476
+ }
477
+ if (typeof result.metadata?.saveError === 'string') {
478
+ console.error(`⚠️ Не сохранено: ${result.metadata.saveError}`);
479
+ }
480
+ const artifacts = result.metadata?.artifacts;
481
+ if (artifacts !== null && typeof artifacts === 'object' && !Array.isArray(artifacts)) {
482
+ const dir = artifacts.dir;
483
+ if (typeof dir === 'string') {
484
+ console.error(`📦 Артефакт: ${dir}`);
485
+ }
486
+ }
487
+ const consensus = result.metadata?.consensus;
488
+ if (consensus !== null && typeof consensus === 'object' && !Array.isArray(consensus)) {
489
+ const agreement = consensus.agreement;
490
+ const average = consensus.averageSimilarity;
491
+ if (typeof agreement === 'string') {
492
+ const value = typeof average === 'number' ? `, ${average.toFixed(2)}` : '';
493
+ console.error(`🤝 Согласие экспертов: ${agreement}${value}`);
494
+ }
495
+ }
496
+ const cost = result.metadata?.cost;
497
+ if (cost !== null && typeof cost === 'object' && !Array.isArray(cost)) {
498
+ const limit = cost.limit;
499
+ const spent = cost.spent;
500
+ const exceeded = cost.exceeded;
501
+ if (typeof limit === 'number' && typeof spent === 'number') {
502
+ console.error(`🎯 Лимит: $${limit.toFixed(4)} · потрачено $${spent.toFixed(4)}` +
503
+ (exceeded === true ? ' — превышен' : ''));
504
+ }
505
+ }
506
+ if (result.metadata?.costTelemetry === 'missing') {
507
+ console.error('⚠️ Провайдер не сообщил стоимость: лимит maxCost не проверен');
508
+ }
509
+ }
510
+ async function readTaskFromFile(path) {
511
+ if (path === '-') {
512
+ return readStdin();
513
+ }
514
+ const resolved = resolve(process.cwd(), path);
515
+ if (!existsSync(resolved)) {
516
+ throw new UsageError(`Файл с задачей не найден: ${path}`);
517
+ }
518
+ return readFileSync(resolved, 'utf-8').trim();
519
+ }
520
+ async function readStdin() {
521
+ const chunks = [];
522
+ let size = 0;
523
+ for await (const chunk of process.stdin) {
524
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
525
+ size += buffer.length;
526
+ if (size > 1_000_000) {
527
+ throw new UsageError('stdin превышает 1 МБ');
528
+ }
529
+ chunks.push(buffer);
530
+ }
531
+ return Buffer.concat(chunks).toString('utf-8').trim();
532
+ }
533
+ function applyCliOverrides(config, options) {
534
+ let result = config;
535
+ if (options.preset) {
536
+ const known = listPresets().map(preset => preset.name);
537
+ if (!known.includes(options.preset)) {
538
+ throw new UsageError(`Неизвестный пресет: ${options.preset}. Доступны: ${known.join(', ')}`);
539
+ }
540
+ result = applyPreset(result, options.preset);
541
+ }
542
+ if (options.experts) {
543
+ const names = options.experts.split(',').map(name => name.trim()).filter(Boolean);
544
+ const available = new Set(result.experts.map(expert => expert.name));
545
+ const unknown = names.filter(name => !available.has(name));
546
+ if (unknown.length > 0) {
547
+ throw new UsageError(`Неизвестные эксперты: ${unknown.join(', ')}. Доступны: ${[...available].join(', ')}`);
548
+ }
549
+ result.experts = result.experts.filter(expert => names.includes(expert.name));
550
+ }
551
+ if (options.expertTimeout) {
552
+ result.timeouts.expert = options.expertTimeout;
553
+ result.experts = result.experts.map(expert => ({ ...expert, timeout: options.expertTimeout }));
554
+ }
555
+ if (options.chairTimeout) {
556
+ result.timeouts.chair = options.chairTimeout;
557
+ result.chair.timeout = options.chairTimeout;
558
+ }
559
+ if (options.retries !== undefined) {
560
+ result.retry.expertRetries = options.retries;
561
+ result.retry.chairRetries = options.retries;
562
+ }
563
+ if (options.format)
564
+ result.output.format = options.format;
565
+ if (options.logLevel)
566
+ result.output.logLevel = options.logLevel;
567
+ if (options.noSave) {
568
+ result.output.file = false;
569
+ }
570
+ else if (options.output !== undefined) {
571
+ result.output.file = options.output;
572
+ }
573
+ if (options.artifacts !== undefined)
574
+ result.artifacts = options.artifacts;
575
+ if (options.noArtifacts)
576
+ result.artifacts = false;
577
+ if (options.consensus === true)
578
+ result.consensus = true;
579
+ if (options.noConsensus)
580
+ result.consensus = false;
581
+ if (options.smartExperts)
582
+ result.smartExperts = true;
583
+ if (options.maxCost !== undefined)
584
+ result.maxCost = options.maxCost;
585
+ if (options.fallbackModels !== undefined)
586
+ result.fallbackModels = options.fallbackModels;
587
+ if (options.session !== undefined)
588
+ result.session = options.session;
589
+ if (options.variant !== undefined) {
590
+ if (!isValidVariant(options.variant)) {
591
+ throw new UsageError(`Некорректный вариант модели: ${options.variant} (допустимы буквы, цифры, . _ -)`);
592
+ }
593
+ const configVariant = result.variant;
594
+ result.experts = result.experts.map(expert => {
595
+ if (expert.model === undefined || !expert.model.includes('/'))
596
+ return expert;
597
+ const existing = variantOf(expert.model);
598
+ if (existing === undefined || existing === configVariant) {
599
+ return { ...expert, model: withVariant(expert.model, options.variant) };
600
+ }
601
+ return expert;
602
+ });
603
+ if (result.chair.model !== undefined && result.chair.model.includes('/')) {
604
+ const existing = variantOf(result.chair.model);
605
+ if (existing === undefined || existing === configVariant) {
606
+ result.chair = {
607
+ ...result.chair,
608
+ model: withVariant(result.chair.model, options.variant)
609
+ };
610
+ }
611
+ }
612
+ result.variant = options.variant;
613
+ }
614
+ if (options.report !== undefined)
615
+ result.report = options.report;
616
+ if (options.reportFile !== undefined)
617
+ result.reportFile = options.reportFile;
618
+ if (options.debounceMs !== undefined)
619
+ result.debounceMs = options.debounceMs;
620
+ return result;
621
+ }
622
+ function printAgentList(config) {
623
+ console.log('Эксперты:');
624
+ for (const expert of config.experts) {
625
+ const status = expert.enabled === false ? ' (отключен)' : '';
626
+ const model = expert.model ? ` — ${expert.model}` : '';
627
+ console.log(` • ${expert.name}${model}${status}`);
628
+ }
629
+ console.log('\nПредседатель:');
630
+ console.log(` • ${config.chair.agent}${config.chair.model ? ` — ${config.chair.model}` : ''}`);
631
+ console.log('\nПресеты:');
632
+ for (const preset of listPresets()) {
633
+ console.log(` • ${preset.name} — ${preset.description}`);
634
+ }
635
+ }
636
+ function printDryRun(task, config) {
637
+ const consilium = new Consilium(config);
638
+ const info = consilium.dryRun(task);
639
+ console.log('\n🔍 DRY RUN — промпты без вызова агентов\n');
640
+ for (const { agent, prompt, model } of info.expertPrompts) {
641
+ console.log(`\n[${agent.toUpperCase()}]${model ? ` (${model})` : ''}`);
642
+ console.log(prompt);
643
+ }
644
+ console.log('\n[ПРЕДСЕДАТЕЛЬ]');
645
+ console.log(info.chairPromptTemplate);
646
+ }
647
+ async function buildTask(options) {
648
+ let task = options.task;
649
+ if (options.taskFile) {
650
+ const fromFile = await readTaskFromFile(options.taskFile);
651
+ task = task ? `${fromFile}\n\n${task}` : fromFile;
652
+ }
653
+ if (options.diff || options.diffStaged || options.diffBase) {
654
+ try {
655
+ const diff = await getGitDiff({
656
+ staged: options.diffStaged,
657
+ base: options.diffBase
658
+ });
659
+ const diffText = formatDiffForPrompt(diff);
660
+ task = task
661
+ ? `${task}\n\n${diffText}`
662
+ : `Проведи ревью изменений в коде: найди баги, риски безопасности и предложи улучшения.\n\n${diffText}`;
663
+ }
664
+ catch (error) {
665
+ throw new UsageError(error.message);
666
+ }
667
+ }
668
+ return task.trim();
669
+ }
670
+ /** Определяет формат отчёта по расширению файла */
671
+ function inferReportFormat(file) {
672
+ const lower = file.toLowerCase();
673
+ if (lower.endsWith('.html') || lower.endsWith('.htm'))
674
+ return 'html';
675
+ if (lower.endsWith('.md') || lower.endsWith('.markdown'))
676
+ return 'pr';
677
+ return 'html';
678
+ }
679
+ /** Рендерит и сохраняет отчёт. Возвращает true, если отчёт напечатан в stdout. */
680
+ function writeRunReport(result, config, options) {
681
+ const formatSetting = options.report ?? config.report;
682
+ const fileSetting = options.reportFile ?? config.reportFile;
683
+ if (formatSetting === undefined && fileSetting === undefined)
684
+ return false;
685
+ const format = formatSetting ?? inferReportFormat(fileSetting ?? '');
686
+ let text;
687
+ try {
688
+ text = renderReport(result, format);
689
+ }
690
+ catch (error) {
691
+ console.error(`⚠️ Не удалось построить отчёт: ${error.message}`);
692
+ return false;
693
+ }
694
+ if (fileSetting === undefined) {
695
+ if ((options.format ?? config.output.format) === 'json') {
696
+ console.error('⚠️ Отчёт не выведен: при --format json укажите --report-file');
697
+ return false;
698
+ }
699
+ console.log(text);
700
+ return true;
701
+ }
702
+ const target = resolve(config.cwd ?? process.cwd(), fileSetting);
703
+ try {
704
+ const directory = dirname(target);
705
+ if (!existsSync(directory))
706
+ mkdirSync(directory, { recursive: true });
707
+ writeFileSync(target, text, 'utf-8');
708
+ }
709
+ catch (error) {
710
+ console.error(`⚠️ Не удалось сохранить отчёт: ${error.message}`);
711
+ return false;
712
+ }
713
+ if (!options.silent)
714
+ console.error(`📄 Отчёт: ${target}`);
715
+ return false;
716
+ }
717
+ async function commandRun(options) {
718
+ const loaded = loadConfig(options.config);
719
+ if (options.config && (!loaded.configPath || (loaded.errors?.length ?? 0) > 0)) {
720
+ throw new UsageError(loaded.errors?.[0] ?? `Файл конфигурации не найден: ${options.config}`);
721
+ }
722
+ if (!options.silent) {
723
+ for (const warning of loaded.warnings) {
724
+ console.error(`⚠️ ${warning}`);
725
+ }
726
+ }
727
+ const config = applyCliOverrides(loaded.config, options);
728
+ if (config.output.logLevel === 'silent') {
729
+ options.silent = true;
730
+ }
731
+ if (options.listAgents) {
732
+ printAgentList(config);
733
+ return 0;
734
+ }
735
+ const artifactsSetting = options.noArtifacts
736
+ ? false
737
+ : options.artifacts ?? config.artifacts;
738
+ const artifactsRoot = resolveArtifactRoot({
739
+ artifacts: artifactsSetting !== undefined && artifactsSetting !== false ? artifactsSetting : true,
740
+ cwd: config.cwd
741
+ });
742
+ if (options.diffRuns !== undefined) {
743
+ const [leftId, rightId] = options.diffRuns;
744
+ try {
745
+ const left = loadArtifact(leftId, artifactsRoot);
746
+ const right = loadArtifact(rightId, artifactsRoot);
747
+ console.log(diffArtifacts(left, right));
748
+ return 0;
749
+ }
750
+ catch (error) {
751
+ throw new UsageError(error.message);
752
+ }
753
+ }
754
+ let replayArtifact;
755
+ let task;
756
+ if (options.replay !== undefined) {
757
+ try {
758
+ replayArtifact = loadArtifact(options.replay, artifactsRoot);
759
+ }
760
+ catch (error) {
761
+ throw new UsageError(error.message);
762
+ }
763
+ const plan = planReplay(replayArtifact, {
764
+ chairAgent: config.chair.agent,
765
+ artifacts: artifactsSetting
766
+ });
767
+ task = plan.task;
768
+ const byName = new Map(config.experts.map(expert => [expert.name, expert]));
769
+ config.experts = plan.experts.map((expert, index) => {
770
+ const base = byName.get(expert.name) ?? {
771
+ name: expert.name,
772
+ enabled: true,
773
+ priority: index + 1
774
+ };
775
+ return expert.model !== undefined ? { ...base, model: expert.model } : base;
776
+ });
777
+ if (options.experts !== undefined) {
778
+ const requested = options.experts
779
+ .split(',')
780
+ .map(name => name.trim())
781
+ .filter(name => name.length > 0);
782
+ const wanted = new Set(requested);
783
+ const filtered = config.experts.filter(expert => wanted.has(expert.name));
784
+ if (filtered.length === 0) {
785
+ throw new UsageError(`В артефакте ${replayArtifact.id} нет экспертов: ${requested.join(', ')}. ` +
786
+ `Доступны: ${plan.experts.map(expert => expert.name).join(', ')}`);
787
+ }
788
+ const missing = requested.filter(name => !plan.experts.some(expert => expert.name === name));
789
+ if (missing.length > 0 && !options.silent) {
790
+ console.error(`⚠️ Нет в артефакте: ${missing.join(', ')}`);
791
+ }
792
+ config.experts = filtered;
793
+ }
794
+ config.chair = { ...config.chair, agent: plan.chair.agent };
795
+ if (plan.chair.model !== undefined) {
796
+ config.chair = { ...config.chair, model: plan.chair.model };
797
+ }
798
+ if (plan.options.consensus !== undefined)
799
+ config.consensus = plan.options.consensus;
800
+ if (plan.options.smartExperts !== undefined)
801
+ config.smartExperts = plan.options.smartExperts;
802
+ if (plan.options.fallbackModels !== undefined) {
803
+ config.fallbackModels = plan.options.fallbackModels;
804
+ }
805
+ if (plan.options.session !== undefined)
806
+ config.session = plan.options.session;
807
+ if (plan.options.artifacts !== undefined)
808
+ config.artifacts = plan.options.artifacts;
809
+ if (!options.silent) {
810
+ console.error(`↩️ Повтор артефакта ${replayArtifact.id} (${replayArtifact.createdAt})`);
811
+ for (const warning of plan.warnings) {
812
+ console.error(`⚠️ ${warning}`);
813
+ }
814
+ }
815
+ }
816
+ else {
817
+ const built = await buildTask(options);
818
+ if (!built) {
819
+ throw new UsageError('Укажите задачу: consilium "Оценить архитектуру" (или передайте её через stdin: consilium -)');
820
+ }
821
+ task = built;
822
+ }
823
+ if (options.dryRun) {
824
+ printDryRun(task, config);
825
+ return 0;
826
+ }
827
+ setupConsoleHooks(config, options);
828
+ const consilium = new Consilium(config);
829
+ if (options.verbose && !options.silent) {
830
+ consilium.onProgress(event => {
831
+ console.error(`[${new Date(event.timestamp).toISOString()}] ${event.type}`, event.agent ?? '');
832
+ });
833
+ }
834
+ let cancelled = false;
835
+ const shutdown = () => {
836
+ if (cancelled)
837
+ process.exit(130);
838
+ cancelled = true;
839
+ console.error('\n⏹️ Отмена...');
840
+ consilium.cancel();
841
+ setTimeout(() => process.exit(130), Math.max(1000, config.timeouts.shutdown)).unref();
842
+ };
843
+ process.once('SIGINT', shutdown);
844
+ process.once('SIGTERM', shutdown);
845
+ const runOptions = {};
846
+ if (options.chair)
847
+ runOptions.chair = options.chair;
848
+ if (options.output && !options.noSave)
849
+ runOptions.outputFile = options.output;
850
+ if (replayArtifact !== undefined) {
851
+ runOptions.metadata = { replayOf: replayArtifact.id };
852
+ }
853
+ try {
854
+ const result = await consilium.run(task, runOptions);
855
+ const reportPrinted = writeRunReport(result, config, options);
856
+ if (!reportPrinted)
857
+ printResult(result, options);
858
+ printSummary(result, options);
859
+ return result.success ? 0 : 1;
860
+ }
861
+ catch (error) {
862
+ console.error(`\n❌ Ошибка: ${error.message}`);
863
+ return 1;
864
+ }
865
+ }
866
+ function writeJsonAtomic(path, value) {
867
+ const dir = dirname(path);
868
+ if (!existsSync(dir))
869
+ mkdirSync(dir, { recursive: true });
870
+ const temporary = `${path}.${process.pid}.tmp`;
871
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
872
+ renameSync(temporary, path);
873
+ }
874
+ /** Путь к глобальному opencode.json(c) */
875
+ function resolveGlobalOpenCodePath() {
876
+ const configHome = process.env.XDG_CONFIG_HOME || resolve(homedir(), '.config');
877
+ const directory = resolve(configHome, 'opencode');
878
+ const json = resolve(directory, 'opencode.json');
879
+ const jsonc = resolve(directory, 'opencode.jsonc');
880
+ return existsSync(json) ? json : existsSync(jsonc) ? jsonc : json;
881
+ }
882
+ /**
883
+ * Регистрирует MCP-сервер в конфигурации OpenCode в формате установленной версии.
884
+ * V2 пишет `mcp.servers.<name>` и `disabled`, V1 — `mcp.<name>` и `enabled`.
885
+ */
886
+ async function commandMcpInstall(options) {
887
+ const global = options.globalConfig === true;
888
+ const loaded = loadConfig(options.config);
889
+ const cwd = loaded.config.cwd ?? process.cwd();
890
+ const path = global
891
+ ? resolveGlobalOpenCodePath()
892
+ : findOpenCodeConfigPath(loaded.config.opencodeConfigPath, cwd) ??
893
+ resolve(cwd, 'opencode.json');
894
+ let existing = {};
895
+ if (existsSync(path)) {
896
+ try {
897
+ const parsed = readJsonFile(path);
898
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
899
+ existing = parsed;
900
+ }
901
+ }
902
+ catch (error) {
903
+ console.error(`❌ Не удалось прочитать ${path}: ${error.message}`);
904
+ return 1;
905
+ }
906
+ }
907
+ // Формат MCP-секции определяет установленный CLI, а не формат agent-секции:
908
+ // V2 игнорирует плоские записи mcp.<name>.
909
+ let flavor;
910
+ try {
911
+ const cliMajor = parseOpenCodeVersion(await getOpenCodeVersion(loaded.config)).major;
912
+ flavor = cliMajor === 2 ? 'v2' : cliMajor === 1 ? 'v1' : undefined;
913
+ }
914
+ catch {
915
+ flavor = undefined;
916
+ }
917
+ const section = readMcpSection(existing);
918
+ const targetFlavor = flavor ?? section.flavor;
919
+ const installedEntry = targetFlavor === 'v2' ? section.servers.consilium : section.flat.consilium;
920
+ if (installedEntry !== undefined && !options.force && !options.dryRun) {
921
+ console.log(`ℹ️ MCP-сервер consilium уже есть в ${path} (используйте --force для обновления).`);
922
+ return 0;
923
+ }
924
+ const upserted = upsertMcpServer(existing, 'consilium', { command: ['npx', '-y', 'lv24-opencode-consilium', 'mcp'] }, { flavor: targetFlavor });
925
+ for (const warning of upserted.warnings) {
926
+ log(options, `⚠️ ${warning}`);
927
+ }
928
+ const proposed = {
929
+ $schema: typeof existing.$schema === 'string'
930
+ ? existing.$schema
931
+ : 'https://opencode.ai/config.json',
932
+ ...upserted.config
933
+ };
934
+ if (options.dryRun) {
935
+ console.log(JSON.stringify(proposed, null, 2));
936
+ return 0;
937
+ }
938
+ if (path.endsWith('.jsonc')) {
939
+ log(options, '⚠️ Файл .jsonc будет перезаписан без комментариев (они не сохраняются).');
940
+ }
941
+ writeJsonAtomic(path, proposed);
942
+ const key = upserted.key === 'mcp.servers' ? 'mcp.servers.consilium' : 'mcp.consilium';
943
+ const flavorLabel = upserted.flavor === 'v2' ? 'V2' : 'V1';
944
+ console.log(`✅ MCP-сервер зарегистрирован: ${path}`);
945
+ console.log(` Ключ: ${key} (${flavorLabel}-формат)`);
946
+ console.log(' Проверка: opencode mcp list');
947
+ return 0;
948
+ }
949
+ async function commandInit(options) {
950
+ const configPath = resolve(process.cwd(), 'consilium.config.json');
951
+ if (!options.free) {
952
+ if (options.nativeV2 === true) {
953
+ log(options, 'ℹ️ --native-v2 применяется только с --free: он влияет на запись агентов в opencode.json.');
954
+ }
955
+ if (options.dryRun) {
956
+ console.log(generateExampleConfig());
957
+ return 0;
958
+ }
959
+ if (existsSync(configPath) && !options.force) {
960
+ console.error('❌ Файл consilium.config.json уже существует. Используйте --force, чтобы перезаписать.');
961
+ return 1;
962
+ }
963
+ writeFileSync(configPath, `${generateExampleConfig()}\n`, 'utf-8');
964
+ console.log(`✅ Создан ${configPath}`);
965
+ console.log(' Следующий шаг: consilium init --free (подобрать бесплатные модели)');
966
+ console.log(' Или отредактируйте конфиг вручную и запустите: consilium "ваша задача"');
967
+ return 0;
968
+ }
969
+ log(options, '🔎 Загружаю каталог моделей (models.dev + OpenRouter)...');
970
+ const fetchResult = await fetchFreeModels({
971
+ forceRefresh: options.refresh,
972
+ includeUnavailable: true
973
+ });
974
+ for (const source of fetchResult.sources) {
975
+ if (!source.ok) {
976
+ log(options, `⚠️ ${source.source}: ${source.error ?? 'ошибка'} (использованы другие источники)`);
977
+ }
978
+ }
979
+ let pool = fetchResult.models;
980
+ const verification = [];
981
+ if (!options.noVerify && (!options.dryRun || options.verify)) {
982
+ const available = pool.filter(model => model.available);
983
+ const verificationPool = available.length >= 3 ? available : pool;
984
+ const candidates = verificationPool.slice(0, Math.min(12, verificationPool.length));
985
+ log(options, `🔬 Проверяю ${candidates.length} кандидатов реальным запросом (это бесплатно)...`);
986
+ const checks = await verifyModels(candidates, { timeoutMs: 60000, concurrency: 4 });
987
+ verification.push(...checks);
988
+ const working = checks.filter(check => check.ok).map(check => check.model);
989
+ if (working.length >= 3) {
990
+ pool = working;
991
+ }
992
+ else {
993
+ log(options, `⚠️ Рабочих моделей: ${working.length}. Использую рейтинг без полной фильтрации.`);
994
+ }
995
+ }
996
+ const council = selectCouncilModels(pool, 5);
997
+ if (council.length === 0) {
998
+ console.error('❌ Не удалось найти бесплатные модели. Попробуйте --refresh позже.');
999
+ return 1;
1000
+ }
1001
+ const agents = buildCouncilAgents(pool);
1002
+ const councilConfig = buildConsiliumConfigFromModels(pool);
1003
+ const loadedForPath = loadConfig(options.config);
1004
+ const targetCwd = loadedForPath.config.cwd ?? process.cwd();
1005
+ const opencodePath = findOpenCodeConfigPath(loadedForPath.config.opencodeConfigPath, targetCwd) ??
1006
+ resolve(targetCwd, 'opencode.json');
1007
+ let existing = {};
1008
+ if (existsSync(opencodePath)) {
1009
+ try {
1010
+ const parsed = readJsonFile(opencodePath);
1011
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1012
+ existing = parsed;
1013
+ }
1014
+ }
1015
+ catch (error) {
1016
+ console.error(`❌ Не удалось прочитать opencode.json: ${error.message}`);
1017
+ return 1;
1018
+ }
1019
+ }
1020
+ const section = readAgentSection(existing);
1021
+ const assignments = [];
1022
+ const definitions = {};
1023
+ for (const [name, agent] of Object.entries(agents)) {
1024
+ const current = section.entries[name];
1025
+ const currentView = current === undefined ? undefined : readAgentDefinition(current);
1026
+ const currentModel = currentView?.model;
1027
+ const kept = currentModel !== undefined && !options.force;
1028
+ const model = kept && currentModel !== undefined ? currentModel : agent.model;
1029
+ definitions[name] = {
1030
+ model,
1031
+ prompt: currentView?.prompt ?? agent.prompt,
1032
+ mode: 'primary'
1033
+ };
1034
+ assignments.push({ agent: name, model, kept });
1035
+ }
1036
+ const upserted = upsertAgents(existing, definitions, {
1037
+ flavor: options.nativeV2 === true ? 'v2' : undefined,
1038
+ migrate: options.nativeV2 === true
1039
+ });
1040
+ for (const warning of upserted.warnings) {
1041
+ log(options, `⚠️ ${warning}`);
1042
+ }
1043
+ const proposedOpenCode = {
1044
+ $schema: typeof existing.$schema === 'string'
1045
+ ? existing.$schema
1046
+ : 'https://opencode.ai/config.json',
1047
+ ...upserted.config
1048
+ };
1049
+ const defaultConfig = mergeWithDefaults({
1050
+ experts: councilConfig.experts.map(expert => ({ ...expert })),
1051
+ chair: { ...councilConfig.chair },
1052
+ autoDiscoverAgents: false
1053
+ });
1054
+ const proposedConsilium = {
1055
+ version: defaultConfig.version,
1056
+ experts: defaultConfig.experts,
1057
+ chair: defaultConfig.chair,
1058
+ excludedAgents: defaultConfig.excludedAgents,
1059
+ timeouts: defaultConfig.timeouts,
1060
+ retry: defaultConfig.retry,
1061
+ output: defaultConfig.output,
1062
+ standalone: defaultConfig.standalone,
1063
+ autoApprove: defaultConfig.autoApprove,
1064
+ autoDiscoverAgents: defaultConfig.autoDiscoverAgents,
1065
+ opencodeConfigPath: defaultConfig.opencodeConfigPath
1066
+ };
1067
+ if (options.dryRun) {
1068
+ console.log(JSON.stringify({ opencode: proposedOpenCode, consilium: proposedConsilium }, null, 2));
1069
+ return 0;
1070
+ }
1071
+ if (opencodePath.endsWith('.jsonc')) {
1072
+ log(options, '⚠️ Файл .jsonc будет перезаписан без комментариев (они не сохраняются).');
1073
+ }
1074
+ writeJsonAtomic(opencodePath, proposedOpenCode);
1075
+ console.log(`✅ Обновлён ${opencodePath}`);
1076
+ if (existsSync(configPath) && !options.force) {
1077
+ console.error(`⚠️ ${configPath} уже существует — не перезаписан (используйте --force).`);
1078
+ }
1079
+ else {
1080
+ writeJsonAtomic(configPath, proposedConsilium);
1081
+ console.log(`✅ Создан ${configPath}`);
1082
+ }
1083
+ console.log('\n📋 Состав консилиума (сильнейшие бесплатные модели):');
1084
+ for (const item of assignments) {
1085
+ console.log(` • ${item.agent} → ${item.model}${item.kept ? ' (сохранена ваша модель)' : ''}`);
1086
+ }
1087
+ if (verification.length > 0) {
1088
+ console.log('\n🔬 Проверка моделей:');
1089
+ for (const check of verification) {
1090
+ const status = check.ok ? '✅' : `❌ ${check.error ?? 'недоступна'}`;
1091
+ console.log(` ${status} ${check.model.id}`);
1092
+ }
1093
+ }
1094
+ const cacheNote = fetchResult.fromCache ? 'из кэша' : 'свежий каталог';
1095
+ console.log(`\nИсточник: ${cacheNote}, моделей-кандидатов: ${fetchResult.models.length}, выбрано: ${council.length}.`);
1096
+ console.log('Запуск: consilium "ваша задача"');
1097
+ return 0;
1098
+ }
1099
+ async function commandModels(options) {
1100
+ const result = await fetchFreeModels({
1101
+ forceRefresh: options.refresh,
1102
+ includeUnavailable: !options.available
1103
+ });
1104
+ const limit = options.limit ?? 20;
1105
+ const models = result.models.slice(0, limit);
1106
+ let checks = new Map();
1107
+ if (options.verify) {
1108
+ log(options, `🔬 Проверяю ${models.length} моделей реальным запросом...`);
1109
+ const verified = await verifyModels(models, { timeoutMs: 60000, concurrency: 4 });
1110
+ checks = new Map(verified.map(item => [item.model.id, { ok: item.ok, error: item.error }]));
1111
+ const working = verified.filter(item => item.ok).length;
1112
+ log(options, `🔬 Рабочих моделей: ${working}/${verified.length}`);
1113
+ }
1114
+ if (options.json) {
1115
+ const enriched = models.map(model => ({ ...model, verified: checks.get(model.id)?.ok }));
1116
+ console.log(JSON.stringify({ ...result, models: enriched }, null, 2));
1117
+ return 0;
1118
+ }
1119
+ if (!options.silent) {
1120
+ for (const source of result.sources) {
1121
+ if (!source.ok)
1122
+ console.error(`⚠️ ${source.source}: ${source.error ?? 'ошибка'}`);
1123
+ }
1124
+ }
1125
+ console.log(`Бесплатные модели (${result.fromCache ? 'кэш' : 'свежие данные'}, всего кандидатов: ${result.models.length}):\n`);
1126
+ const header = ['#', 'model', 'ctx', 'tools', 'score', 'доступна', 'источник', 'проверка'];
1127
+ console.log(header.join('\t'));
1128
+ models.forEach((model, index) => {
1129
+ const check = checks.get(model.id);
1130
+ console.log([
1131
+ String(index + 1),
1132
+ model.id,
1133
+ `${Math.round(model.context / 1000)}k`,
1134
+ model.toolCall ? 'yes' : 'no',
1135
+ model.score.toFixed(3),
1136
+ model.available ? 'yes' : 'no',
1137
+ model.source,
1138
+ check ? (check.ok ? 'OK' : `FAIL: ${check.error ?? ''}`) : (options.verify ? '-' : 'не проверялась')
1139
+ ].join('\t'));
1140
+ });
1141
+ if (models.length === 0) {
1142
+ console.log('Нет моделей по заданным критериям.');
1143
+ }
1144
+ return 0;
1145
+ }
1146
+ async function commandDoctor(options) {
1147
+ const loaded = loadConfig(options.config);
1148
+ if (loaded.warnings.length > 0 && !options.silent) {
1149
+ for (const warning of loaded.warnings) {
1150
+ console.error(`⚠️ ${warning}`);
1151
+ }
1152
+ }
1153
+ const checks = await runDoctor(loaded.config, {
1154
+ configPath: loaded.configPath,
1155
+ cwd: process.cwd(),
1156
+ checkModels: true
1157
+ });
1158
+ console.log(formatDoctorReport(checks));
1159
+ if ((loaded.errors?.length ?? 0) > 0) {
1160
+ return 1;
1161
+ }
1162
+ return checks.some(check => check.status === 'fail') ? 1 : 0;
1163
+ }
1164
+ /**
1165
+ * Watch-режим: перезапускает консилиум при изменениях в проекте.
1166
+ */
1167
+ async function commandWatch(options) {
1168
+ if (options.replay !== undefined) {
1169
+ throw new UsageError('watch не поддерживает --replay');
1170
+ }
1171
+ if (options.diffRuns !== undefined) {
1172
+ throw new UsageError('watch не поддерживает --diff-runs');
1173
+ }
1174
+ if (options.taskFile === '-') {
1175
+ throw new UsageError('watch не читает задачу из stdin (-)');
1176
+ }
1177
+ const loaded = loadConfig(options.config);
1178
+ if (!options.silent) {
1179
+ for (const warning of loaded.warnings) {
1180
+ console.error(`⚠️ ${warning}`);
1181
+ }
1182
+ }
1183
+ const config = applyCliOverrides(loaded.config, options);
1184
+ const cwd = config.cwd ?? process.cwd();
1185
+ const task = options.task.trim().length > 0 ? options.task.trim() : 'Сделай ревью текущих изменений';
1186
+ const debounceMs = config.debounceMs ?? 400;
1187
+ const ignore = [];
1188
+ const addIgnore = (value) => {
1189
+ if (value === undefined || value.trim().length === 0)
1190
+ return;
1191
+ const target = resolve(cwd, value);
1192
+ const rel = relative(cwd, target);
1193
+ if (rel.length > 0 && !rel.startsWith('..')) {
1194
+ ignore.push(rel.replace(/\\/g, '/'));
1195
+ }
1196
+ };
1197
+ addIgnore(options.output ?? (typeof config.output.file === 'string' ? config.output.file : undefined));
1198
+ addIgnore(options.reportFile ?? config.reportFile);
1199
+ if (config.artifacts !== undefined && config.artifacts !== false) {
1200
+ try {
1201
+ addIgnore(resolveArtifactRoot({ artifacts: config.artifacts, cwd }));
1202
+ }
1203
+ catch {
1204
+ // Некорректный каталог артефактов отловит сам запуск
1205
+ }
1206
+ }
1207
+ if (!options.silent) {
1208
+ console.error(`👀 watch: ${cwd} (debounce ${debounceMs} мс), Ctrl+C — выход`);
1209
+ if (ignore.length > 0)
1210
+ console.error(` игнорирую выводы: ${ignore.join(', ')}`);
1211
+ }
1212
+ let running = false;
1213
+ let closed = false;
1214
+ let suppressUntil = 0;
1215
+ let active;
1216
+ const runOnce = async () => {
1217
+ if (running || closed) {
1218
+ return;
1219
+ }
1220
+ running = true;
1221
+ try {
1222
+ let baseTask = task;
1223
+ if (options.taskFile !== undefined) {
1224
+ const fromFile = await readTaskFromFile(options.taskFile);
1225
+ if (fromFile.trim().length > 0)
1226
+ baseTask = fromFile.trim();
1227
+ }
1228
+ let fullTask = baseTask;
1229
+ try {
1230
+ const diff = await getGitDiff({
1231
+ cwd,
1232
+ staged: options.diffStaged,
1233
+ base: options.diffBase
1234
+ });
1235
+ if (diff.filesChanged > 0) {
1236
+ fullTask = `${baseTask}\n\n${formatDiffForPrompt(diff)}`;
1237
+ }
1238
+ }
1239
+ catch {
1240
+ // Вне git-репозитория работаем с задачей как есть
1241
+ }
1242
+ if (options.dryRun) {
1243
+ printDryRun(fullTask, config);
1244
+ return;
1245
+ }
1246
+ if (!options.silent)
1247
+ console.error('\n🔄 Запуск консилиума...');
1248
+ active = new Consilium(config);
1249
+ const result = await active.run(fullTask);
1250
+ const reportPrinted = writeRunReport(result, config, options);
1251
+ if (!reportPrinted)
1252
+ printResult(result, options);
1253
+ printSummary(result, options);
1254
+ }
1255
+ catch (error) {
1256
+ console.error(`❌ ${error.message}`);
1257
+ }
1258
+ finally {
1259
+ active = undefined;
1260
+ running = false;
1261
+ // Гасим события от собственных записей (report/-o/redirect), которые
1262
+ // fs.watch может доставить уже после завершения прогона.
1263
+ suppressUntil = Date.now() + Math.max(1000, debounceMs + 300);
1264
+ }
1265
+ };
1266
+ let watcher;
1267
+ try {
1268
+ watcher = createProjectWatcher({
1269
+ cwd,
1270
+ debounceMs,
1271
+ ignore,
1272
+ onTrigger: () => {
1273
+ // Не ставим в очередь: иначе перенаправленный в проект stdout/stderr
1274
+ // запускает новый прогон после каждого завершения (бесконечный цикл).
1275
+ if (closed || running || Date.now() < suppressUntil)
1276
+ return;
1277
+ void runOnce();
1278
+ }
1279
+ });
1280
+ }
1281
+ catch (error) {
1282
+ console.error(`❌ Не удалось запустить watch: ${error.message}`);
1283
+ return 1;
1284
+ }
1285
+ await runOnce();
1286
+ await new Promise(resolve => {
1287
+ const shutdown = () => {
1288
+ closed = true;
1289
+ active?.cancel();
1290
+ watcher.close();
1291
+ resolve();
1292
+ };
1293
+ process.once('SIGINT', shutdown);
1294
+ process.once('SIGTERM', shutdown);
1295
+ });
1296
+ return 0;
1297
+ }
1298
+ /** Устанавливает или удаляет git pre-commit hook */
1299
+ async function commandHooks(options) {
1300
+ const cwd = process.cwd();
1301
+ if (options.hookAction === undefined) {
1302
+ console.error('❌ Укажите действие: consilium hooks install|uninstall');
1303
+ return 2;
1304
+ }
1305
+ if (options.hookAction === 'uninstall') {
1306
+ const removed = uninstallPreCommitHook(cwd);
1307
+ console.log(removed
1308
+ ? '✅ consilium-блок удалён из pre-commit'
1309
+ : 'ℹ️ consilium-блок в pre-commit не найден');
1310
+ return 0;
1311
+ }
1312
+ const hookCommand = 'npx -y lv24-opencode-consilium --staged -s "Ревью staged-изменений" || exit 1';
1313
+ try {
1314
+ const hook = installPreCommitHook(cwd, hookCommand);
1315
+ console.log(`✅ pre-commit hook ${hook.created ? 'создан' : 'обновлён'}: ${hook.path}`);
1316
+ console.log(` ${hookCommand}`);
1317
+ return 0;
1318
+ }
1319
+ catch (error) {
1320
+ console.error(`❌ ${error.message}`);
1321
+ return 1;
1322
+ }
1323
+ }
1324
+ export async function main(argv = process.argv.slice(2)) {
1325
+ let options;
1326
+ try {
1327
+ options = parseArgs(argv);
1328
+ }
1329
+ catch (error) {
1330
+ if (error instanceof UsageError) {
1331
+ console.error(`❌ ${error.message}`);
1332
+ return 2;
1333
+ }
1334
+ throw error;
1335
+ }
1336
+ const command = options.command ?? 'run';
1337
+ try {
1338
+ switch (command) {
1339
+ case 'help':
1340
+ printHelp();
1341
+ return 0;
1342
+ case 'version':
1343
+ console.log(`consilium v${readVersion()}`);
1344
+ return 0;
1345
+ case 'init':
1346
+ return await commandInit(options);
1347
+ case 'models':
1348
+ return await commandModels(options);
1349
+ case 'doctor':
1350
+ return await commandDoctor(options);
1351
+ case 'mcp': {
1352
+ if (options.mcpInstall === true) {
1353
+ return await commandMcpInstall(options);
1354
+ }
1355
+ if (!options.silent)
1356
+ console.error('lv24-opencode-consilium MCP server (stdio). Логи — в stderr.');
1357
+ await startMcpServer({ version: readVersion() });
1358
+ return 0;
1359
+ }
1360
+ case 'watch':
1361
+ return await commandWatch(options);
1362
+ case 'hooks':
1363
+ return await commandHooks(options);
1364
+ case 'run':
1365
+ return await commandRun(options);
1366
+ default:
1367
+ console.error(`❌ Неизвестная команда: ${command}`);
1368
+ return 2;
1369
+ }
1370
+ }
1371
+ catch (error) {
1372
+ if (error instanceof UsageError) {
1373
+ console.error(`❌ ${error.message}`);
1374
+ return 2;
1375
+ }
1376
+ throw error;
1377
+ }
1378
+ }
1379
+ const invokedDirectly = process.argv[1]
1380
+ ? resolve(process.argv[1]) === fileURLToPath(import.meta.url)
1381
+ : false;
1382
+ if (invokedDirectly) {
1383
+ main()
1384
+ .then(code => {
1385
+ process.exitCode = code;
1386
+ })
1387
+ .catch((error) => {
1388
+ console.error('💥 Критическая ошибка:', error);
1389
+ process.exitCode = 1;
1390
+ });
1391
+ }
1392
+ //# sourceMappingURL=cli.js.map