@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 +1 -1
- package/src/cli.js +67 -63
- package/test/smoke-test.js +5 -2
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
4461
|
+
return !/\b(image|video|audio|tts|embed|embedding|rerank|moderation|safeguard)\b/.test(`${id} ${note}`);
|
|
4442
4462
|
}
|
|
4443
4463
|
|
|
4444
|
-
function
|
|
4445
|
-
const
|
|
4464
|
+
function buildOpenRouterDeveloperChoices(models) {
|
|
4465
|
+
const byDeveloper = new Map();
|
|
4446
4466
|
for (const model of models) {
|
|
4447
|
-
if (!model.
|
|
4448
|
-
const current =
|
|
4467
|
+
if (!model.developer) continue;
|
|
4468
|
+
const current = byDeveloper.get(model.developer) || { count: 0 };
|
|
4449
4469
|
current.count += 1;
|
|
4450
|
-
|
|
4451
|
-
byAuthor.set(model.author, current);
|
|
4470
|
+
byDeveloper.set(model.developer, current);
|
|
4452
4471
|
}
|
|
4453
4472
|
|
|
4454
|
-
return
|
|
4473
|
+
return MAIN_OPENROUTER_DEVELOPERS
|
|
4455
4474
|
.map(([id, label]) => {
|
|
4456
|
-
const stat =
|
|
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(
|
|
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
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
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
|
-
|
|
4865
|
-
|
|
4874
|
+
const developerAnswer = Number(await askText("Номер: "));
|
|
4875
|
+
if (!developerAnswer) return "";
|
|
4866
4876
|
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
const
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
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
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4884
|
+
if (filtered.length === 0) {
|
|
4885
|
+
console.log("Модели не найдены.");
|
|
4886
|
+
continue;
|
|
4887
|
+
}
|
|
4886
4888
|
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
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
|
-
|
|
4896
|
-
|
|
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)
|
|
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)
|
|
6746
|
+
if (!options.quiet) printAiAnswer(answer);
|
|
6743
6747
|
}
|
|
6744
6748
|
return answer;
|
|
6745
6749
|
}
|
package/test/smoke-test.js
CHANGED
|
@@ -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, "
|
|
51
|
-
assertIncludes(cliSource, "
|
|
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");
|