/
AleksVolobuev
/
cli-nord
Обзор
Документация
Войти
/
AleksVolobuev
/
cli-nord
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/cli.ts
278 строк
13 KB
AleksVolobuev
feat: add local browser UI
06 авг 2026, 19:06
06 авг 2026, 19:06
4815245
Код
Авторство
О чём код?
import { Command } from 'commander'; // Версия берётся из package.json, чтобы CLI не расходился с пакетом. tsup/esbuild // инлайнит значение в бандл при сборке, так что рантайм-доступа к файлу не требуется. import pkg from '../package.json' with { type: 'json' }; import { runAbout } from './commands/about.js'; import { runCalibrationEvaluate, runCalibrationExport } from './commands/calibration.js'; import { runCasesDelete, runCasesExport, runCasesImport, runCasesLint, runCasesList, runCasesPrune, runCasesRate, runCasesReview, runCasesShow, } from './commands/cases.js'; import { runConfigGet, runConfigPath, runConfigSet, runConfigTest } from './commands/config.js'; import { runDataExport, runDataImport } from './commands/data.js'; import { runDoctor } from './commands/doctor.js'; import { runHistory } from './commands/history.js'; import { runInteractiveMenu } from './commands/menu.js'; import { runPlay } from './commands/play.js'; import { runReview } from './commands/review.js'; import { runStats, runStatsClear } from './commands/stats.js'; import { runStudyEvaluate } from './commands/study.js'; import { errorText } from './commands/style.js'; import { runThemes } from './commands/themes.js'; import { runTrain } from './commands/train.js'; import { runWeb } from './commands/web.js'; const program = new Command(); function printCommandError(err: unknown): void { console.error(errorText((err as Error).message)); process.exitCode = 1; } function wrapAction<Args extends unknown[]>(action: (...args: Args) => void | Promise<void>) { return async (...args: Args): Promise<void> => { try { await action(...args); } catch (err) { printCommandError(err); } }; } program .name('nord') .description('CLI-тренажёр мышления по циклу OODA (НОРД: Наблюдение-Ориентация-Решение-Действие)') .version(pkg.version) .addHelpText('after', '\nЗапуск без команды открывает интерактивное меню: nord'); program.action(wrapAction(runInteractiveMenu)); program .command('web') .description('Запустить локальный браузерный интерфейс') .option('--port <number>', 'локальный порт; 0 выбирает свободный', '4317') .option('--no-open', 'не открывать системный браузер') .action(wrapAction(runWeb)); program .command('play') .description('Сыграть один кейс') .option( '--theme <themeId>', 'тема кейса (relationships, tactics, military, negotiation, medical, cyber)', ) .option('--case <caseId>', 'id конкретного кейса') .option('--source <source>', 'источник кейса: builtin | llm | imported | cached', 'builtin') .option('--time <seconds>', 'переопределить лимит времени кейса в секундах') .option('--difficulty <difficulty>', 'сложность кейса: easy | normal | hard') .option('--formal', 'разрешить только кейс со статусом reviewed') .action(wrapAction(runPlay)); program .command('train') .description('Тренировочная сессия из 2–3 кейсов с фокусом и адаптивной прогрессией') .option('--count <n>', 'число кейсов в сессии: 2 или 3', '3') .option('--source <source>', 'источник: builtin | cached | imported', 'builtin') .option('--formal', 'использовать только кейсы со статусом reviewed') .action(wrapAction(runTrain)); program .command('history') .description('Показать историю попыток') .option('--theme <themeId>', 'фильтр по теме') .option('--limit <n>', 'ограничить число записей', '20') .action(wrapAction(runHistory)); program .command('review <attemptId>') .description('Разобрать конкретную попытку: таймлайн, сигналы, решения и выводы') .action(wrapAction(runReview)); const statsCmd = program .command('stats') .description('Показать агрегированную статистику по этапам OODA') .option('--theme <themeId>', 'фильтр по теме') .option('--family <trainingFamily>', 'фильтр по семейству сопоставимых кейсов') .option('--difficulty <difficulty>', 'фильтр по сложности: easy | normal | hard') .option('--formula <version>', 'фильтр по версии формулы оценки') .option('--recent <n>', 'тренд и метрики по последним N попыткам', '5') .action(wrapAction(runStats)); statsCmd .command('clear') .description('Очистить всю статистику прохождений в локальной БД') .option('--yes', 'подтвердить необратимое удаление без повторного запроса') .action(wrapAction(runStatsClear)); const studyCmd = program .command('study') .description('Проверка учебного эффекта и переноса по пререгистрированному протоколу'); studyCmd .command('evaluate <file>') .description('Проверить JSON-отчёт исследования и вывести допустимый вывод') .option('--json', 'машиночитаемый результат') .option('--require-supported', 'вернуть код 1, если отчёт не получил verdict supported') .action(wrapAction(runStudyEvaluate)); const calibrationCmd = program .command('calibration') .description('Экспорт наблюдаемых траекторий и проверка калибровки scoring/profile/difficulty'); calibrationCmd .command('export <file>') .description('Экспортировать локальные builtin-попытки для пилотного набора') .action(wrapAction(runCalibrationExport)); calibrationCmd .command('evaluate <file>') .description('Проверить observed held-out отчёт калибровки') .option('--json', 'машиночитаемый результат') .option('--require-calibrated', 'вернуть код 1, если формула не прошла scoped-калибровку') .option('--require-cross-theme', 'вернуть код 1, если межтематические агрегаты недопустимы') .action(wrapAction(runCalibrationEvaluate)); program .command('themes') .description('Список доступных тем и количества кейсов') .action(wrapAction(runThemes)); program .command('about') .description('Методологическая справка: цикл OODA, механики, калибровка и профили') .action(wrapAction(runAbout)); const casesCmd = program .command('cases') .description('Каталог встроенных, пользовательских и кэшированных кейсов'); casesCmd .command('list') .description('Показать список кейсов') .option('--theme <themeId>', 'фильтр по теме') .option('--source <source>', 'фильтр по источнику: builtin | llm | imported | cached') .option('--difficulty <difficulty>', 'фильтр по сложности: easy | normal | hard') .option('--status <status>', 'фильтр по статусу предметного ревью: draft | reviewed') .action(wrapAction(runCasesList)); casesCmd .command('show <caseId>') .description('Показать детали кейса') .action(wrapAction(runCasesShow)); casesCmd .command('delete <caseId>') .description('Удалить пользовательский или кэшированный LLM-кейс') .option('--with-attempts', 'удалить также историю попыток по этому кейсу') .action(wrapAction(runCasesDelete)); casesCmd .command('lint [caseId]') .description('Проверить кейсы эвристиками анти-метагейма (длина/маркеры/критерии)') .option('--strict', 'вернуть ненулевой код выхода при наличии замечаний') .action(wrapAction(runCasesLint)); casesCmd .command('import <file>') .description('Строго проверить и импортировать пользовательский JSON-кейс') .action(wrapAction(runCasesImport)); casesCmd .command('export <caseId> [file]') .description('Экспортировать кейс в переносимый JSON (по умолчанию <caseId>.json)') .action(wrapAction(runCasesExport)); casesCmd .command('rate <caseId> <rating>') .description('Оценить LLM-кейс от 1 до 5') .action(wrapAction(runCasesRate)); casesCmd .command('review <caseId>') .description('Зафиксировать предметное ревью текущей содержательной версии кейса') .requiredOption('--reviewer <name>', 'имя или идентификатор рецензента') .option('--subject-matter', 'подтвердить содержательную валидность') .option('--instructional', 'подтвердить учебную валидность') .option('--construct', 'подтвердить конструктную валидность') .option('--debrief', 'подтвердить валидность дебрифа') .action(wrapAction(runCasesReview)); casesCmd .command('prune') .description('Очистить LLM-кэш, сохраняя лучшие по оценке и свежести кейсы') .option('--keep <n>', 'сколько лучших LLM-кейсов сохранить', '20') .option('--with-attempts', 'разрешить удаление кейсов вместе с историей попыток') .option('--yes', 'выполнить удаление; без флага показывается предварительный просмотр') .action(wrapAction(runCasesPrune)); const dataCmd = program .command('data') .description('Экспорт/импорт локальных данных (кэшированные кейсы и история попыток)'); dataCmd .command('export <file>') .description('Выгрузить локальные данные в JSON или JSONL (формат определяется по .jsonl)') .option('--case <caseId...>', 'выгрузить только указанные case id и их попытки') .option('--theme <themeId...>', 'выгрузить только данные указанных тем') .option('--format <format>', 'явно задать формат: json | jsonl') .action(wrapAction(runDataExport)); dataCmd .command('import <file>') .description('Загрузить JSON/JSONL backup (кейсы по id, v5 без дубликатов попыток)') .option('--dry-run', 'проверить backup тем же путём импорта, но откатить все изменения') .action(wrapAction(runDataImport)); program .command('doctor') .description('Проверить локальную БД, конфигурацию и встроенные кейсы без изменения данных') .action(wrapAction(runDoctor)); const configCmd = program .command('config') .description('Настройка LLM-провайдера и параметров по умолчанию') .addHelpText( 'after', ` Переменные окружения (приоритет над config.json): NORD_LLM_BASE_URL базовый URL OpenAI-совместимого API NORD_LLM_API_KEY ключ API (либо стандартный OPENAI_API_KEY как fallback) NORD_LLM_MODEL имя модели NORD_LLM_TIMEOUT_MS таймаут одного запроса к LLM в миллисекундах Эти имена также можно использовать как alias в config get/set. Хранить ключ в переменной окружения предпочтительнее: config.json держит ключ в открытом виде (файл создаётся с правами 0600 и ужесточается при каждой записи).`, ); configCmd .command('path') .description('Показать путь к файлу конфигурации') .action(wrapAction(runConfigPath)); configCmd .command('get [key]') .description( 'Показать эффективное значение конфигурации (llm.baseUrl, llm.apiKey, llm.model, llm.timeoutMs, defaults.timeLimitSeconds)', ) .action(wrapAction(runConfigGet)); configCmd .command('set <key> <value>') .description('Установить значение конфигурации') .action(wrapAction(runConfigSet)); configCmd .command('test') .description('Проверить соединение с настроенным LLM-провайдером') .action(wrapAction(runConfigTest)); program.parseAsync(process.argv);