@iola_adm/iola-cli 0.1.38 → 0.1.39

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 +177 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
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
@@ -330,14 +330,7 @@ const SLASH_COMMANDS = [
330
330
  { command: "/init", description: "проверить окружение" },
331
331
  { command: "/exit", description: "выйти" },
332
332
  ];
333
- const BANNER = `\x1b[38;5;45m┌────────────────────────────────────────────────────────────────────────────┐
334
- │ │
335
- │\x1b[38;5;213m CLI-Йошкар-Ола \x1b[38;5;45m│
336
- │ │
337
- │\x1b[38;5;250m открытые данные • MCP • локальный AI \x1b[38;5;45m│
338
- │ │
339
- │\x1b[38;5;82m VERSION_LINE \x1b[38;5;45m│
340
- └────────────────────────────────────────────────────────────────────────────┘\x1b[0m`;
333
+ const BANNER_WIDTH = 76;
341
334
 
342
335
  const COMMANDS = new Map([
343
336
  ["help", showHelp],
@@ -632,6 +625,17 @@ async function startAgent() {
632
625
  console.log("Интерактивный режим. Введите /help для списка команд, /exit для выхода.");
633
626
  await runHooks("SessionStart", { mode: "agent" });
634
627
 
628
+ if (input.isTTY && output.isTTY) {
629
+ await startAgentRawInput();
630
+ await runHooks("SessionEnd", { mode: "agent" });
631
+ return;
632
+ }
633
+
634
+ await startAgentReadline();
635
+ await runHooks("SessionEnd", { mode: "agent" });
636
+ }
637
+
638
+ async function startAgentReadline() {
635
639
  const rl = readline.createInterface({ input, output, prompt: "iola> " });
636
640
  const state = {
637
641
  history: [],
@@ -667,7 +671,86 @@ async function startAgent() {
667
671
  rl.close();
668
672
  }
669
673
  detachSlashSuggestions();
670
- await runHooks("SessionEnd", { mode: "agent" });
674
+ }
675
+
676
+ async function startAgentRawInput() {
677
+ const state = { history: [], buffer: "", selected: 0, slashOpen: false, running: false };
678
+ emitKeypressEvents(input);
679
+ const wasRaw = input.isRaw;
680
+ input.setRawMode(true);
681
+ input.resume();
682
+
683
+ const render = () => renderAgentInput(state);
684
+ render();
685
+
686
+ try {
687
+ while (true) {
688
+ const { str, key } = await readKeypress();
689
+ if (key?.ctrl && key.name === "c") break;
690
+ if (key?.name === "escape") {
691
+ state.slashOpen = false;
692
+ render();
693
+ continue;
694
+ }
695
+ if (key?.name === "backspace") {
696
+ state.buffer = [...state.buffer].slice(0, -1).join("");
697
+ updateSlashState(state);
698
+ render();
699
+ continue;
700
+ }
701
+ if (key?.name === "up" && state.slashOpen) {
702
+ const matches = currentSlashMatches(state);
703
+ state.selected = Math.max(0, Math.min(matches.length - 1, state.selected - 1));
704
+ render();
705
+ continue;
706
+ }
707
+ if (key?.name === "down" && state.slashOpen) {
708
+ const matches = currentSlashMatches(state);
709
+ state.selected = Math.max(0, Math.min(matches.length - 1, state.selected + 1));
710
+ render();
711
+ continue;
712
+ }
713
+ if (isShiftEnter(str, key)) {
714
+ state.buffer += "\n";
715
+ state.slashOpen = false;
716
+ render();
717
+ continue;
718
+ }
719
+ if (key?.name === "return" || key?.name === "enter") {
720
+ const matches = currentSlashMatches(state);
721
+ const selected = matches[state.selected];
722
+ if (state.slashOpen && selected && state.buffer.trim() !== selected.command) {
723
+ state.buffer = selected.command;
724
+ state.slashOpen = false;
725
+ render();
726
+ continue;
727
+ }
728
+ const line = state.buffer.trim();
729
+ state.buffer = "";
730
+ state.slashOpen = false;
731
+ clearAgentInputArea();
732
+ if (!line) {
733
+ render();
734
+ continue;
735
+ }
736
+ try {
737
+ const shouldExit = await handleAgentLine(line, state);
738
+ if (shouldExit) break;
739
+ } catch (error) {
740
+ console.error(error instanceof Error ? error.message : String(error));
741
+ }
742
+ render();
743
+ continue;
744
+ }
745
+ if (str && !key?.ctrl && !key?.meta) {
746
+ state.buffer += str;
747
+ updateSlashState(state);
748
+ render();
749
+ }
750
+ }
751
+ } finally {
752
+ if (!wasRaw) input.setRawMode(false);
753
+ }
671
754
  }
672
755
 
673
756
  async function handleAgentLine(line, state) {
@@ -1085,6 +1168,59 @@ function getSlashCommandMatches(filter = "") {
1085
1168
  || item.description.toLocaleLowerCase("ru-RU").includes(normalized));
1086
1169
  }
1087
1170
 
1171
+ function updateSlashState(state) {
1172
+ state.slashOpen = state.buffer.startsWith("/");
1173
+ state.selected = 0;
1174
+ }
1175
+
1176
+ function currentSlashMatches(state) {
1177
+ if (!state.buffer.startsWith("/")) return [];
1178
+ return getSlashCommandMatches(state.buffer.slice(1)).slice(0, 10);
1179
+ }
1180
+
1181
+ function renderAgentInput(state) {
1182
+ clearAgentInputArea();
1183
+ const prompt = "iola> ";
1184
+ const lines = state.buffer.split("\n");
1185
+ output.write(`${prompt}${lines[0] || ""}\n`);
1186
+ for (const line of lines.slice(1)) output.write(` ${line}\n`);
1187
+ if (state.slashOpen) {
1188
+ const matches = currentSlashMatches(state);
1189
+ if (matches.length === 0) {
1190
+ output.write(" нет команд\n");
1191
+ } else {
1192
+ for (let index = 0; index < matches.length; index += 1) {
1193
+ const marker = index === state.selected ? ">" : " ";
1194
+ output.write(`${marker} ${matches[index].command.padEnd(24)} ${matches[index].description}\n`);
1195
+ }
1196
+ output.write(" ↑/↓ выбрать • Enter вставить/выполнить • Esc закрыть\n");
1197
+ }
1198
+ }
1199
+ writePromptBottomPadding();
1200
+ }
1201
+
1202
+ function clearAgentInputArea() {
1203
+ if (!output.isTTY) return;
1204
+ output.write("\x1b[2K\r");
1205
+ }
1206
+
1207
+ function readKeypress() {
1208
+ return new Promise((resolve) => {
1209
+ const handler = (str, key) => {
1210
+ input.off("keypress", handler);
1211
+ resolve({ str, key });
1212
+ };
1213
+ input.on("keypress", handler);
1214
+ });
1215
+ }
1216
+
1217
+ function isShiftEnter(str, key) {
1218
+ return (key?.name === "return" && key.shift)
1219
+ || (key?.name === "enter" && key.shift)
1220
+ || String(str || "").includes("[13;2")
1221
+ || String(str || "").includes("[27;2;13");
1222
+ }
1223
+
1088
1224
  function printAgentHistory(history) {
1089
1225
  if (history.length === 0) {
1090
1226
  console.log("История пуста.");
@@ -1167,7 +1303,7 @@ async function showBanner(options = {}) {
1167
1303
  const updateAvailable = latest && compareVersions(latest, version) > 0;
1168
1304
  const versionLine = updateAvailable ? `v${version} -> v${latest} • npm install -g @iola_adm/iola-cli@latest` : `v${version} • iola help`;
1169
1305
  if (process.stdout.isTTY && process.env.NO_COLOR !== "1") {
1170
- console.log(BANNER.replace("VERSION_LINE", padBannerLine(versionLine)));
1306
+ console.log(renderBanner(versionLine, true));
1171
1307
  if (updateAvailable) {
1172
1308
  console.log(`Доступно обновление: v${version} -> v${latest}`);
1173
1309
  console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
@@ -1180,9 +1316,37 @@ async function showBanner(options = {}) {
1180
1316
  if (updateAvailable) console.log("Обновить: npm install -g @iola_adm/iola-cli@latest");
1181
1317
  }
1182
1318
 
1183
- function padBannerLine(value) {
1184
- const text = String(value).slice(0, 62);
1185
- return `${text}${" ".repeat(Math.max(0, 62 - bannerVisibleLength(text)))}`;
1319
+ function renderBanner(versionLine, color = false) {
1320
+ const c = color ? {
1321
+ border: "\x1b[38;5;45m",
1322
+ title: "\x1b[38;5;213m",
1323
+ muted: "\x1b[38;5;250m",
1324
+ version: "\x1b[38;5;82m",
1325
+ reset: "\x1b[0m",
1326
+ } : { border: "", title: "", muted: "", version: "", reset: "" };
1327
+ const line = (text = "", style = "") => {
1328
+ const value = centerBannerText(text);
1329
+ return `${c.border}│${style}${value}${c.border}│`;
1330
+ };
1331
+ return [
1332
+ `${c.border}┌${"─".repeat(BANNER_WIDTH)}┐`,
1333
+ line(),
1334
+ line("CLI-Йошкар-Ола", c.title),
1335
+ line(),
1336
+ line("открытые данные • MCP • локальный AI", c.muted),
1337
+ line(),
1338
+ line(versionLine, c.version),
1339
+ `${c.border}└${"─".repeat(BANNER_WIDTH)}┘${c.reset}`,
1340
+ ].join("\n");
1341
+ }
1342
+
1343
+ function centerBannerText(value) {
1344
+ const text = String(value || "");
1345
+ const length = bannerVisibleLength(text);
1346
+ if (length >= BANNER_WIDTH) return [...text].slice(0, BANNER_WIDTH).join("");
1347
+ const left = Math.floor((BANNER_WIDTH - length) / 2);
1348
+ const right = BANNER_WIDTH - length - left;
1349
+ return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
1186
1350
  }
1187
1351
 
1188
1352
  function bannerVisibleLength(value) {