@iola_adm/iola-cli 0.1.36 → 0.1.38

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +146 -87
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -5,6 +5,7 @@ import { createServer } from "node:http";
5
5
  import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
+ import { emitKeypressEvents } from "node:readline";
8
9
  import readline from "node:readline/promises";
9
10
  import { stdin as input, stdout as output } from "node:process";
10
11
  import { DatabaseSync } from "node:sqlite";
@@ -264,15 +265,74 @@ const DATASETS = {
264
265
  endpoint: "kindergartens",
265
266
  },
266
267
  };
268
+ const SLASH_COMMANDS = [
269
+ { command: "/help", description: "список slash-команд" },
270
+ { command: "/health", description: "проверка публичного API/MCP" },
271
+ { command: "/doctor", description: "диагностика CLI" },
272
+ { command: "/wizard", description: "мастер настройки" },
273
+ { command: "/db status", description: "статус локальной SQLite-БД" },
274
+ { command: "/sessions", description: "AI-сессии" },
275
+ { command: "/resume SESSION_ID", description: "продолжить сессию" },
276
+ { command: "/features list", description: "feature flags" },
277
+ { command: "/gosuslugi status", description: "личное подключение Госуслуг" },
278
+ { command: "/wiki", description: "ссылки на документацию" },
279
+ { command: "/context list", description: "локальный контекст проекта" },
280
+ { command: "/skills list", description: "skills" },
281
+ { command: "/permissions", description: "разрешения" },
282
+ { command: "/tools", description: "tools и toolsets" },
283
+ { command: "/files status", description: "локальные файловые операции" },
284
+ { command: "/archive doctor", description: "архиватор" },
285
+ { command: "/changes list", description: "подготовленные изменения" },
286
+ { command: "/index status", description: "индекс документов" },
287
+ { command: "/reports list", description: "пакеты отчетов" },
288
+ { command: "/plugins list", description: "plugins" },
289
+ { command: "/workspace status", description: "workspace" },
290
+ { command: "/tasks list", description: "задачи" },
291
+ { command: "/artifacts list", description: "artifacts" },
292
+ { command: "/trace last", description: "последние tools trace" },
293
+ { command: "/policy use safe", description: "переключить policy" },
294
+ { command: "/cron list", description: "cron-задачи" },
295
+ { command: "/daemon status", description: "локальный daemon" },
296
+ { command: "/rpc call status", description: "RPC status" },
297
+ { command: "/memory show", description: "память агента" },
298
+ { command: "/hooks list", description: "hooks" },
299
+ { command: "/agents list", description: "agents" },
300
+ { command: "/mcp status", description: "MCP" },
301
+ { command: "/cache status", description: "cache" },
302
+ { command: "/sync", description: "обновить локальные данные" },
303
+ { command: "/diff", description: "изменения данных" },
304
+ { command: "/card школа 29", description: "карточка объекта" },
305
+ { command: "/quality", description: "качество данных" },
306
+ { command: "/views", description: "saved views" },
307
+ { command: "/config get", description: "конфигурация" },
308
+ { command: "/layers", description: "слои данных" },
309
+ { command: "/data schools --limit 10", description: "данные слоя" },
310
+ { command: "/schools --limit 10", description: "школы" },
311
+ { command: "/kindergartens --search 29", description: "детские сады" },
312
+ { command: "/search лицей --limit 3", description: "поиск" },
313
+ { command: "/mcp-info", description: "публичный MCP" },
314
+ { command: "/profiles", description: "AI-профили" },
315
+ { command: "/models openrouter --search qwen", description: "модели" },
316
+ { command: "/ai doctor", description: "AI diagnostics" },
317
+ { command: "/ai setup ollama", description: "настройка Ollama" },
318
+ { command: "/use openai", description: "выбрать OpenAI" },
319
+ { command: "/use ollama", description: "выбрать Ollama" },
320
+ { command: "/key status", description: "API-ключи" },
321
+ { command: "/history", description: "история текущей сессии" },
322
+ { command: "/new", description: "новая agent-сессия" },
323
+ { command: "/retry", description: "повторить последний вопрос" },
324
+ { command: "/undo", description: "удалить последний обмен" },
325
+ { command: "/compact", description: "сжать контекст" },
326
+ { command: "/usage", description: "использование контекста" },
327
+ { command: "/clear", description: "очистить историю agent-сессии" },
328
+ { command: "/banner", description: "показать баннер" },
329
+ { command: "/update", description: "проверить обновления" },
330
+ { command: "/init", description: "проверить окружение" },
331
+ { command: "/exit", description: "выйти" },
332
+ ];
267
333
  const BANNER = `\x1b[38;5;45m┌────────────────────────────────────────────────────────────────────────────┐
268
334
  │ │
269
- │\x1b[38;5;51m ____ _ ___ __ ______ ____ _ _ _ __ _ ____ \x1b[38;5;45m│
270
- │\x1b[38;5;51m / ___| | |_ _| \\ \\ / / ___|| _ \\| | | | |/ / / \\ | _ \\ \x1b[38;5;45m│
271
- │\x1b[38;5;51m | | | | | | _____ \\ V /\\___ \\| | | | |_| | ' / / _ \\ | |_) | \x1b[38;5;45m│
272
- │\x1b[38;5;51m | |___| |___ | | |_____| | | ___) | |_| | _ | . \\ / ___ \\| _ < \x1b[38;5;45m│
273
- │\x1b[38;5;51m \\____|_____|___| |_| |____/|____/|_| |_|_|\\_\\/_/ \\_\\_| \\_\\ \x1b[38;5;45m│
274
- │ │
275
- │\x1b[38;5;213m CLI-ЙОШКАР-ОЛА \x1b[38;5;45m│
335
+ │\x1b[38;5;213m CLI-Йошкар-Ола \x1b[38;5;45m│
276
336
  │ │
277
337
  │\x1b[38;5;250m открытые данные • MCP • локальный AI \x1b[38;5;45m│
278
338
  │ │
@@ -580,6 +640,7 @@ async function startAgent() {
580
640
  rl.on("close", () => {
581
641
  closed = true;
582
642
  });
643
+ const detachSlashSuggestions = attachSlashSuggestions(rl);
583
644
  safePrompt(rl);
584
645
 
585
646
  for await (const rawLine of rl) {
@@ -605,6 +666,7 @@ async function startAgent() {
605
666
  if (!closed) {
606
667
  rl.close();
607
668
  }
669
+ detachSlashSuggestions();
608
670
  await runHooks("SessionEnd", { mode: "agent" });
609
671
  }
610
672
 
@@ -616,9 +678,19 @@ async function handleAgentLine(line, state) {
616
678
  return false;
617
679
  }
618
680
 
681
+ if (line === "/") {
682
+ printSlashMenu("");
683
+ return false;
684
+ }
685
+
619
686
  const [command, ...args] = splitCommandLine(line.slice(1));
620
687
  state.lastCommand = { command, args };
621
688
 
689
+ if (!command) {
690
+ printSlashMenu("");
691
+ return false;
692
+ }
693
+
622
694
  if (command === "exit" || command === "quit") {
623
695
  return true;
624
696
  }
@@ -968,8 +1040,13 @@ async function handleAgentLine(line, state) {
968
1040
  }[command];
969
1041
 
970
1042
  if (!mapped) {
971
- console.log(`Неизвестная slash-команда: /${command}`);
972
- printAgentHelp();
1043
+ const matches = getSlashCommandMatches(command);
1044
+ if (matches.length > 0) {
1045
+ printSlashMenu(command);
1046
+ } else {
1047
+ console.log(`Неизвестная slash-команда: /${command}`);
1048
+ printSlashMenu(command);
1049
+ }
973
1050
  return false;
974
1051
  }
975
1052
 
@@ -979,83 +1056,33 @@ async function handleAgentLine(line, state) {
979
1056
  }
980
1057
 
981
1058
  function printAgentHelp() {
982
- console.log(`Slash-команды:
983
- /help
984
- /health
985
- /doctor
986
- /wizard
987
- /db status
988
- /sessions
989
- /resume SESSION_ID
990
- /features list
991
- /gosuslugi status
992
- /wiki
993
- /context list
994
- /skills list
995
- /permissions
996
- /tools
997
- /files status
998
- /archive doctor
999
- /changes list
1000
- /index status
1001
- /reports list
1002
- /plugins list
1003
- /workspace status
1004
- /tasks list
1005
- /artifacts list
1006
- /trace last
1007
- /policy use safe
1008
- /cron list
1009
- /daemon status
1010
- /rpc call status
1011
- /memory show
1012
- /hooks list
1013
- /agents list
1014
- /mcp status
1015
- /cache status
1016
- /sync
1017
- /diff
1018
- /card школа 29
1019
- /quality
1020
- /views
1021
- /config get
1022
- /config set api.baseUrl URL
1023
- /layers
1024
- /data schools --limit 10
1025
- /schools --limit 10
1026
- /schools get --inn 1215067180
1027
- /kindergartens --search 29
1028
- /kindergartens get --inn 1215077421
1029
- /search лицей --limit 3
1030
- /mcp-info
1031
- /context школа 29
1032
- /profiles
1033
- /profile use local
1034
- /models openrouter --search qwen
1035
- /ai doctor
1036
- /ai setup ollama
1037
- /use openai
1038
- /use ollama
1039
- /key status
1040
- /key set openai
1041
- /config
1042
- /provider
1043
- /model
1044
- /history
1045
- /history --limit 20
1046
- /new
1047
- /reset
1048
- /retry
1049
- /undo
1050
- /compact
1051
- /usage
1052
- /clear
1053
- /banner
1054
- /update
1055
- /init
1056
- /exit
1057
-
1058
- Обычный текст без slash-команды отправляется в настроенный AI-провайдер.`);
1059
+ printSlashMenu("");
1060
+ console.log("");
1061
+ console.log("Обычный текст без slash-команды отправляется в настроенный AI-провайдер.");
1062
+ }
1063
+
1064
+ function printSlashMenu(filter = "", options = {}) {
1065
+ const normalized = String(filter || "").replace(/^\//, "");
1066
+ const rows = getSlashCommandMatches(normalized)
1067
+ .slice(0, Number(options.limit || 30))
1068
+ .map((item) => ({ command: item.command, description: item.description }));
1069
+ if (rows.length === 0) {
1070
+ console.log(`Нет slash-команд по фильтру: ${filter}`);
1071
+ console.log("Введите / для списка команд.");
1072
+ return;
1073
+ }
1074
+ console.log(normalized ? `Slash-команды по фильтру "${filter}":` : "Slash-команды:");
1075
+ printTable(rows, [["command", "Команда"], ["description", "Описание"]]);
1076
+ if (!options.compact && SLASH_COMMANDS.length > rows.length && !normalized) {
1077
+ console.log(`Показано ${rows.length} из ${SLASH_COMMANDS.length}. Введите /текст для фильтра.`);
1078
+ }
1079
+ }
1080
+
1081
+ function getSlashCommandMatches(filter = "") {
1082
+ const normalized = String(filter || "").replace(/^\//, "").toLocaleLowerCase("ru-RU");
1083
+ return SLASH_COMMANDS.filter((item) => !normalized
1084
+ || item.command.toLocaleLowerCase("ru-RU").includes(normalized)
1085
+ || item.description.toLocaleLowerCase("ru-RU").includes(normalized));
1059
1086
  }
1060
1087
 
1061
1088
  function printAgentHistory(history) {
@@ -1096,12 +1123,44 @@ function safePrompt(rl, closed = false) {
1096
1123
  }
1097
1124
 
1098
1125
  try {
1126
+ writePromptBottomPadding();
1099
1127
  rl.prompt();
1100
1128
  } catch {
1101
1129
  // The input stream can close while an async slash-command is still running.
1102
1130
  }
1103
1131
  }
1104
1132
 
1133
+ function attachSlashSuggestions(rl) {
1134
+ if (!input.isTTY) return () => {};
1135
+ emitKeypressEvents(input, rl);
1136
+ let lastFilter = null;
1137
+ const onKeypress = () => {
1138
+ setTimeout(() => {
1139
+ const line = rl.line || "";
1140
+ if (!line.startsWith("/")) {
1141
+ lastFilter = null;
1142
+ return;
1143
+ }
1144
+ const filter = line.slice(1);
1145
+ if (filter === lastFilter) return;
1146
+ lastFilter = filter;
1147
+ output.write("\n");
1148
+ printSlashMenu(filter, { compact: true, limit: 10 });
1149
+ writePromptBottomPadding();
1150
+ rl.prompt(true);
1151
+ }, 0);
1152
+ };
1153
+ input.on("keypress", onKeypress);
1154
+ return () => input.off("keypress", onKeypress);
1155
+ }
1156
+
1157
+ function writePromptBottomPadding() {
1158
+ if (!output.isTTY) return;
1159
+ const padding = Math.max(0, Math.min(5, Number(process.env.IOLA_PROMPT_BOTTOM_PADDING || 2)));
1160
+ if (padding === 0) return;
1161
+ output.write(`${"\n".repeat(padding)}\x1b[${padding}A`);
1162
+ }
1163
+
1105
1164
  async function showBanner(options = {}) {
1106
1165
  const version = getPackageVersion();
1107
1166
  const latest = options.skipUpdate ? null : await getLatestNpmVersion("@iola_adm/iola-cli");
@@ -1116,7 +1175,7 @@ async function showBanner(options = {}) {
1116
1175
  return;
1117
1176
  }
1118
1177
 
1119
- console.log(`CLI-ЙОШКАР-ОЛА ${updateAvailable ? `v${version} -> v${latest}` : `v${version}`}`);
1178
+ console.log(`CLI-Йошкар-Ола ${updateAvailable ? `v${version} -> v${latest}` : `v${version}`}`);
1120
1179
  console.log("открытые данные • MCP • локальный AI");
1121
1180
  if (updateAvailable) console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
1122
1181
  }