@iola_adm/iola-cli 0.1.109 → 0.1.111

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
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
@@ -101,7 +101,7 @@ const FEATURES = {
101
101
  "mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
102
102
  "web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
103
103
  };
104
- const MAIN_OPENROUTER_AUTHORS = [
104
+ const MAIN_OPENROUTER_DEVELOPERS = [
105
105
  ["openai", "OpenAI"],
106
106
  ["anthropic", "Anthropic"],
107
107
  ["google", "Google"],
@@ -1524,7 +1524,7 @@ function flushPendingAgentOutput(state) {
1524
1524
  const text = state.pendingOutput;
1525
1525
  state.pendingOutput = "";
1526
1526
  if (!text) return;
1527
- console.log(text);
1527
+ printAiAnswer(text);
1528
1528
  }
1529
1529
 
1530
1530
  function colorSlashSelection(row) {
@@ -1537,6 +1537,26 @@ function colorMuted(row) {
1537
1537
  return `\x1b[38;5;245m${row}\x1b[0m`;
1538
1538
  }
1539
1539
 
1540
+ function printAiAnswer(text) {
1541
+ output.write(`${renderTerminalMarkdown(text)}\n`);
1542
+ }
1543
+
1544
+ function renderTerminalMarkdown(text) {
1545
+ const source = String(text || "");
1546
+ if (!output.isTTY || process.env.NO_COLOR === "1") return source;
1547
+ return source
1548
+ .split(/(```[\s\S]*?```)/g)
1549
+ .map((part) => part.startsWith("```") ? part : renderInlineMarkdown(part))
1550
+ .join("");
1551
+ }
1552
+
1553
+ function renderInlineMarkdown(text) {
1554
+ return String(text || "")
1555
+ .replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "\x1b[1m$1\x1b[22m")
1556
+ .replace(/__([^_\n][\s\S]*?[^_\n])__/g, "\x1b[1m$1\x1b[22m")
1557
+ .replace(/`([^`\n]+)`/g, "\x1b[36m$1\x1b[39m");
1558
+ }
1559
+
1540
1560
  function setTerminalTitle(title) {
1541
1561
  if (!output.isTTY) return;
1542
1562
  output.write(`\x1b]0;${String(title).replace(/[\x00-\x1f\x7f]/g, "")}\x07`);
@@ -4415,11 +4435,12 @@ function mapOpenRouterModel(model) {
4415
4435
  const id = String(model.id || "");
4416
4436
  const architecture = model.architecture || {};
4417
4437
  const created = Number(model.created || 0);
4438
+ const developer = id.includes("/") ? id.split("/")[0] : "";
4418
4439
  return {
4419
4440
  id,
4420
4441
  provider: "openrouter",
4421
4442
  note: model.name || "",
4422
- author: id.includes("/") ? id.split("/")[0] : "",
4443
+ developer,
4423
4444
  created,
4424
4445
  releaseDate: formatUnixDate(created),
4425
4446
  modality: architecture.modality || "",
@@ -4429,48 +4450,39 @@ function mapOpenRouterModel(model) {
4429
4450
  };
4430
4451
  }
4431
4452
 
4432
- function isOpenRouterTextModel(model) {
4453
+ function isOpenRouterTextGenerationModel(model) {
4433
4454
  const inputs = model.inputModalities || [];
4434
4455
  const outputs = model.outputModalities || [];
4435
4456
  if (inputs.length > 0 && !inputs.includes("text")) return false;
4436
4457
  if (outputs.length > 0 && !outputs.includes("text")) return false;
4437
- const modality = String(model.modality || "").toLocaleLowerCase("en-US");
4438
- if (modality && modality !== "text->text") return false;
4458
+ if (outputs.includes("image") || outputs.includes("audio") || outputs.includes("video")) return false;
4439
4459
  const id = String(model.id || "").toLocaleLowerCase("en-US");
4440
4460
  const note = String(model.note || "").toLocaleLowerCase("en-US");
4441
- return !/\b(vl|vision|image|video|audio|tts|embed|embedding|rerank|moderation)\b/.test(`${id} ${note}`);
4461
+ return !/\b(image|video|audio|tts|embed|embedding|rerank|moderation|safeguard)\b/.test(`${id} ${note}`);
4442
4462
  }
4443
4463
 
4444
- function buildOpenRouterAuthorChoices(models) {
4445
- const byAuthor = new Map();
4464
+ function buildOpenRouterDeveloperChoices(models) {
4465
+ const byDeveloper = new Map();
4446
4466
  for (const model of models) {
4447
- if (!model.author) continue;
4448
- const current = byAuthor.get(model.author) || { count: 0, latestCreated: 0 };
4467
+ if (!model.developer) continue;
4468
+ const current = byDeveloper.get(model.developer) || { count: 0 };
4449
4469
  current.count += 1;
4450
- current.latestCreated = Math.max(current.latestCreated, Number(model.created || 0));
4451
- byAuthor.set(model.author, current);
4470
+ byDeveloper.set(model.developer, current);
4452
4471
  }
4453
4472
 
4454
- return MAIN_OPENROUTER_AUTHORS
4473
+ return MAIN_OPENROUTER_DEVELOPERS
4455
4474
  .map(([id, label]) => {
4456
- const stat = byAuthor.get(id);
4475
+ const stat = byDeveloper.get(id);
4457
4476
  if (!stat) return null;
4458
4477
  return {
4459
4478
  id,
4460
4479
  label,
4461
4480
  count: stat.count,
4462
- latestReleaseDate: formatUnixDate(stat.latestCreated),
4463
4481
  };
4464
4482
  })
4465
4483
  .filter(Boolean);
4466
4484
  }
4467
4485
 
4468
- function modelMatchesSearch(model, search) {
4469
- const needle = search.toLocaleLowerCase("ru-RU");
4470
- return [model.id, model.note, model.author]
4471
- .some((value) => String(value || "").toLocaleLowerCase("ru-RU").includes(needle));
4472
- }
4473
-
4474
4486
  function sortModelsByFreshness(left, right) {
4475
4487
  return Number(right.created || 0) - Number(left.created || 0)
4476
4488
  || String(left.id).localeCompare(String(right.id));
@@ -4845,55 +4857,47 @@ async function chooseOpenRouterModel() {
4845
4857
  return "";
4846
4858
  }
4847
4859
 
4848
- const textModels = models.filter(isOpenRouterTextModel);
4860
+ const textModels = models.filter(isOpenRouterTextGenerationModel);
4849
4861
  if (textModels.length === 0) {
4850
4862
  console.log("Текстовые модели OpenRouter не найдены.");
4851
4863
  return "";
4852
4864
  }
4853
4865
 
4854
- const authorChoices = buildOpenRouterAuthorChoices(textModels);
4855
- console.log("Выберите автора моделей OpenRouter:");
4856
- authorChoices.forEach((choice, index) => {
4857
- const date = choice.latestReleaseDate ? `, свежая: ${choice.latestReleaseDate}` : "";
4858
- console.log(` ${index + 1}. ${choice.label} (${choice.count}${date})`);
4859
- });
4860
- const searchIndex = authorChoices.length + 1;
4861
- console.log(` ${searchIndex}. Поиск по всем текстовым моделям`);
4862
- console.log(" 0. Отмена");
4866
+ while (true) {
4867
+ const developerChoices = buildOpenRouterDeveloperChoices(textModels);
4868
+ console.log("Выберите разработчика моделей OpenRouter:");
4869
+ developerChoices.forEach((choice, index) => {
4870
+ console.log(` ${index + 1}. ${choice.label} (${choice.count})`);
4871
+ });
4872
+ console.log(" 0. Отмена");
4863
4873
 
4864
- const authorAnswer = Number(await askText("Номер: "));
4865
- if (!authorAnswer) return "";
4874
+ const developerAnswer = Number(await askText("Номер: "));
4875
+ if (!developerAnswer) return "";
4866
4876
 
4867
- let filtered;
4868
- if (authorAnswer === searchIndex) {
4869
- const search = (await askText("Фильтр моделей: ")).trim();
4870
- if (!search) return "";
4871
- filtered = textModels.filter((model) => modelMatchesSearch(model, search));
4872
- } else {
4873
- const selectedAuthor = authorChoices[authorAnswer - 1];
4874
- if (!selectedAuthor) return "";
4875
- filtered = textModels.filter((model) => model.author === selectedAuthor.id);
4876
- }
4877
-
4878
- filtered = filtered
4879
- .sort(sortModelsByFreshness)
4880
- .slice(0, 30);
4877
+ const selectedDeveloper = developerChoices[developerAnswer - 1];
4878
+ if (!selectedDeveloper) continue;
4879
+ const filtered = textModels
4880
+ .filter((model) => model.developer === selectedDeveloper.id)
4881
+ .sort(sortModelsByFreshness)
4882
+ .slice(0, 30);
4881
4883
 
4882
- if (filtered.length === 0) {
4883
- console.log("Модели не найдены.");
4884
- return "";
4885
- }
4884
+ if (filtered.length === 0) {
4885
+ console.log("Модели не найдены.");
4886
+ continue;
4887
+ }
4886
4888
 
4887
- console.log("Выберите текстовую модель:");
4888
- filtered.forEach((model, index) => {
4889
- const date = model.releaseDate || "дата неизвестна";
4890
- const context = model.contextLength ? `, ctx ${formatCompactNumber(model.contextLength)}` : "";
4891
- console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}`);
4892
- });
4893
- console.log(" 0. Отмена");
4889
+ console.log("Выберите текстовую модель:");
4890
+ filtered.forEach((model, index) => {
4891
+ const date = model.releaseDate || "дата неизвестна";
4892
+ const context = model.contextLength ? `, ctx ${formatCompactNumber(model.contextLength)}` : "";
4893
+ console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}`);
4894
+ });
4895
+ console.log(" 0. Назад");
4894
4896
 
4895
- const modelAnswer = Number(await askText("Номер: "));
4896
- return filtered[modelAnswer - 1]?.id || "";
4897
+ const modelAnswer = Number(await askText("Номер: "));
4898
+ if (!modelAnswer) continue;
4899
+ return filtered[modelAnswer - 1]?.id || "";
4900
+ }
4897
4901
  }
4898
4902
 
4899
4903
  async function chooseAndSaveApiModel(provider) {
@@ -6512,7 +6516,7 @@ async function aiAsk(args, context = {}) {
6512
6516
  return answer;
6513
6517
  }
6514
6518
 
6515
- if (!options.quiet) console.log(answer);
6519
+ if (!options.quiet) printAiAnswer(answer);
6516
6520
  return answer;
6517
6521
  }
6518
6522
 
@@ -6739,7 +6743,7 @@ async function localToolAsk(question, providerConfig, options) {
6739
6743
  if (options.format === "json" || options.schema === "json") {
6740
6744
  printJson({ answer, plan: validated, result });
6741
6745
  } else {
6742
- if (!options.quiet) console.log(answer);
6746
+ if (!options.quiet) printAiAnswer(answer);
6743
6747
  }
6744
6748
  return answer;
6745
6749
  }
@@ -47,8 +47,11 @@ assertIncludes(help, "iola master", "help");
47
47
  assertIncludes(help, "iola ask", "help");
48
48
 
49
49
  assertIncludes(cliSource, "force: Boolean(options.force)", "IOLA setup should not force model reinstall by default");
50
- assertIncludes(cliSource, "MAIN_OPENROUTER_AUTHORS", "OpenRouter model selection should group models by author");
51
- assertIncludes(cliSource, "modality !== \"text->text\"", "OpenRouter model selection should prefer text models");
50
+ assertIncludes(cliSource, "MAIN_OPENROUTER_DEVELOPERS", "OpenRouter model selection should group models by developer");
51
+ assertIncludes(cliSource, "isOpenRouterTextGenerationModel", "OpenRouter model selection should prefer text-generation models");
52
+ assertIncludes(cliSource, "console.log(\" 0. Назад\")", "OpenRouter model selection should return to developer menu");
53
+ assertIncludes(cliSource, "renderTerminalMarkdown", "AI answers should render inline markdown in the terminal");
54
+ assertIncludes(cliSource, "\\x1b[1m$1\\x1b[22m", "AI answer renderer should support bold markdown");
52
55
 
53
56
  const commands = await runCli(["commands"]);
54
57
  assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");