@iola_adm/iola-cli 0.1.107 → 0.1.109
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 +163 -14
- package/test/smoke-test.js +5 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -101,6 +101,19 @@ 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 = [
|
|
105
|
+
["openai", "OpenAI"],
|
|
106
|
+
["anthropic", "Anthropic"],
|
|
107
|
+
["google", "Google"],
|
|
108
|
+
["qwen", "Qwen / Alibaba"],
|
|
109
|
+
["deepseek", "DeepSeek"],
|
|
110
|
+
["meta-llama", "Meta / Llama"],
|
|
111
|
+
["mistralai", "Mistral AI"],
|
|
112
|
+
["x-ai", "xAI"],
|
|
113
|
+
["cohere", "Cohere"],
|
|
114
|
+
["microsoft", "Microsoft"],
|
|
115
|
+
["perplexity", "Perplexity"],
|
|
116
|
+
];
|
|
104
117
|
const SKILL_BUNDLES = {
|
|
105
118
|
analyst: {
|
|
106
119
|
description: "Аналитик открытых данных: поиск, карточки, отчеты и память.",
|
|
@@ -611,7 +624,7 @@ Usage:
|
|
|
611
624
|
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
612
625
|
iola ai doctor [--json]
|
|
613
626
|
iola ai setup
|
|
614
|
-
iola ai setup iola [--yes]
|
|
627
|
+
iola ai setup iola [--yes] [--force]
|
|
615
628
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
616
629
|
iola health [--json]
|
|
617
630
|
iola layers [--json]
|
|
@@ -2053,7 +2066,7 @@ async function handleAi(args) {
|
|
|
2053
2066
|
iola ai models iola|ollama|openai|openrouter|codex [--search TEXT]
|
|
2054
2067
|
iola ai doctor [--json]
|
|
2055
2068
|
iola ai setup
|
|
2056
|
-
iola ai setup iola [--yes]
|
|
2069
|
+
iola ai setup iola [--yes] [--force]
|
|
2057
2070
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
2058
2071
|
iola ai setup openai [--model MODEL]
|
|
2059
2072
|
iola ai setup openrouter [--model MODEL]
|
|
@@ -4288,6 +4301,7 @@ async function aiModels(args) {
|
|
|
4288
4301
|
printTable(filtered, [
|
|
4289
4302
|
["id", "Модель"],
|
|
4290
4303
|
["provider", "Провайдер"],
|
|
4304
|
+
["releaseDate", "Дата"],
|
|
4291
4305
|
["note", "Примечание"],
|
|
4292
4306
|
]);
|
|
4293
4307
|
}
|
|
@@ -4361,11 +4375,7 @@ async function listAiModels(provider) {
|
|
|
4361
4375
|
if (apiKey && getAiNetworkMode(relayConfig) === "gateway") {
|
|
4362
4376
|
const payload = await callAiRelayModels(relayConfig, apiKey, "OpenRouter");
|
|
4363
4377
|
return (payload.data || [])
|
|
4364
|
-
.map(
|
|
4365
|
-
id: model.id,
|
|
4366
|
-
provider: "openrouter",
|
|
4367
|
-
note: model.name || "",
|
|
4368
|
-
}))
|
|
4378
|
+
.map(mapOpenRouterModel)
|
|
4369
4379
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
4370
4380
|
}
|
|
4371
4381
|
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
@@ -4378,11 +4388,7 @@ async function listAiModels(provider) {
|
|
|
4378
4388
|
|
|
4379
4389
|
const payload = await response.json();
|
|
4380
4390
|
return (payload.data || [])
|
|
4381
|
-
.map(
|
|
4382
|
-
id: model.id,
|
|
4383
|
-
provider: "openrouter",
|
|
4384
|
-
note: model.name || "",
|
|
4385
|
-
}))
|
|
4391
|
+
.map(mapOpenRouterModel)
|
|
4386
4392
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
4387
4393
|
}
|
|
4388
4394
|
|
|
@@ -4405,6 +4411,85 @@ async function getApiProviderNetworkConfig(provider) {
|
|
|
4405
4411
|
};
|
|
4406
4412
|
}
|
|
4407
4413
|
|
|
4414
|
+
function mapOpenRouterModel(model) {
|
|
4415
|
+
const id = String(model.id || "");
|
|
4416
|
+
const architecture = model.architecture || {};
|
|
4417
|
+
const created = Number(model.created || 0);
|
|
4418
|
+
return {
|
|
4419
|
+
id,
|
|
4420
|
+
provider: "openrouter",
|
|
4421
|
+
note: model.name || "",
|
|
4422
|
+
author: id.includes("/") ? id.split("/")[0] : "",
|
|
4423
|
+
created,
|
|
4424
|
+
releaseDate: formatUnixDate(created),
|
|
4425
|
+
modality: architecture.modality || "",
|
|
4426
|
+
inputModalities: Array.isArray(architecture.input_modalities) ? architecture.input_modalities : [],
|
|
4427
|
+
outputModalities: Array.isArray(architecture.output_modalities) ? architecture.output_modalities : [],
|
|
4428
|
+
contextLength: Number(model.context_length || model.top_provider?.context_length || 0),
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
function isOpenRouterTextModel(model) {
|
|
4433
|
+
const inputs = model.inputModalities || [];
|
|
4434
|
+
const outputs = model.outputModalities || [];
|
|
4435
|
+
if (inputs.length > 0 && !inputs.includes("text")) return false;
|
|
4436
|
+
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;
|
|
4439
|
+
const id = String(model.id || "").toLocaleLowerCase("en-US");
|
|
4440
|
+
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}`);
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function buildOpenRouterAuthorChoices(models) {
|
|
4445
|
+
const byAuthor = new Map();
|
|
4446
|
+
for (const model of models) {
|
|
4447
|
+
if (!model.author) continue;
|
|
4448
|
+
const current = byAuthor.get(model.author) || { count: 0, latestCreated: 0 };
|
|
4449
|
+
current.count += 1;
|
|
4450
|
+
current.latestCreated = Math.max(current.latestCreated, Number(model.created || 0));
|
|
4451
|
+
byAuthor.set(model.author, current);
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
return MAIN_OPENROUTER_AUTHORS
|
|
4455
|
+
.map(([id, label]) => {
|
|
4456
|
+
const stat = byAuthor.get(id);
|
|
4457
|
+
if (!stat) return null;
|
|
4458
|
+
return {
|
|
4459
|
+
id,
|
|
4460
|
+
label,
|
|
4461
|
+
count: stat.count,
|
|
4462
|
+
latestReleaseDate: formatUnixDate(stat.latestCreated),
|
|
4463
|
+
};
|
|
4464
|
+
})
|
|
4465
|
+
.filter(Boolean);
|
|
4466
|
+
}
|
|
4467
|
+
|
|
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
|
+
function sortModelsByFreshness(left, right) {
|
|
4475
|
+
return Number(right.created || 0) - Number(left.created || 0)
|
|
4476
|
+
|| String(left.id).localeCompare(String(right.id));
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
function formatUnixDate(value) {
|
|
4480
|
+
const seconds = Number(value || 0);
|
|
4481
|
+
if (!seconds) return "";
|
|
4482
|
+
return new Date(seconds * 1000).toISOString().slice(0, 10);
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
function formatCompactNumber(value) {
|
|
4486
|
+
const number = Number(value || 0);
|
|
4487
|
+
if (!number) return "";
|
|
4488
|
+
if (number >= 1_000_000) return `${Math.round(number / 100_000) / 10}M`;
|
|
4489
|
+
if (number >= 1_000) return `${Math.round(number / 100) / 10}K`;
|
|
4490
|
+
return String(number);
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4408
4493
|
function getRecommendedOllamaModels(notePrefix = "recommended") {
|
|
4409
4494
|
return [
|
|
4410
4495
|
{ id: IOLA_LOCAL_OLLAMA_MODEL, provider: "ollama", note: `${notePrefix} IOLA default low RAM` },
|
|
@@ -4711,8 +4796,12 @@ async function getDefaultApiProviderForModelSwitch() {
|
|
|
4711
4796
|
}
|
|
4712
4797
|
|
|
4713
4798
|
async function chooseAiModel(provider) {
|
|
4799
|
+
if (provider === "openrouter") {
|
|
4800
|
+
return chooseOpenRouterModel();
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4714
4803
|
let search = "";
|
|
4715
|
-
if (provider === "
|
|
4804
|
+
if (provider === "openai") {
|
|
4716
4805
|
search = (await askText("Фильтр моделей (Enter - без фильтра): ")).trim();
|
|
4717
4806
|
}
|
|
4718
4807
|
|
|
@@ -4747,6 +4836,66 @@ async function chooseAiModel(provider) {
|
|
|
4747
4836
|
return filtered[answer - 1]?.id || "";
|
|
4748
4837
|
}
|
|
4749
4838
|
|
|
4839
|
+
async function chooseOpenRouterModel() {
|
|
4840
|
+
let models;
|
|
4841
|
+
try {
|
|
4842
|
+
models = await listAiModels("openrouter");
|
|
4843
|
+
} catch (error) {
|
|
4844
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
4845
|
+
return "";
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
const textModels = models.filter(isOpenRouterTextModel);
|
|
4849
|
+
if (textModels.length === 0) {
|
|
4850
|
+
console.log("Текстовые модели OpenRouter не найдены.");
|
|
4851
|
+
return "";
|
|
4852
|
+
}
|
|
4853
|
+
|
|
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. Отмена");
|
|
4863
|
+
|
|
4864
|
+
const authorAnswer = Number(await askText("Номер: "));
|
|
4865
|
+
if (!authorAnswer) return "";
|
|
4866
|
+
|
|
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);
|
|
4881
|
+
|
|
4882
|
+
if (filtered.length === 0) {
|
|
4883
|
+
console.log("Модели не найдены.");
|
|
4884
|
+
return "";
|
|
4885
|
+
}
|
|
4886
|
+
|
|
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. Отмена");
|
|
4894
|
+
|
|
4895
|
+
const modelAnswer = Number(await askText("Номер: "));
|
|
4896
|
+
return filtered[modelAnswer - 1]?.id || "";
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4750
4899
|
async function chooseAndSaveApiModel(provider) {
|
|
4751
4900
|
const model = await chooseAiModel(provider);
|
|
4752
4901
|
if (!model) {
|
|
@@ -6239,7 +6388,7 @@ async function setupIolaLocal(args) {
|
|
|
6239
6388
|
ggufFile,
|
|
6240
6389
|
modelDir,
|
|
6241
6390
|
model,
|
|
6242
|
-
force:
|
|
6391
|
+
force: Boolean(options.force),
|
|
6243
6392
|
quiet: Boolean(options.quiet),
|
|
6244
6393
|
});
|
|
6245
6394
|
} catch (error) {
|
package/test/smoke-test.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
7
|
const binPath = resolve(rootDir, "bin", "iola.js");
|
|
8
8
|
const packageJson = JSON.parse(await readFile(resolve(rootDir, "package.json"), "utf8"));
|
|
9
|
+
const cliSource = await readFile(resolve(rootDir, "src", "cli.js"), "utf8");
|
|
9
10
|
|
|
10
11
|
function runCli(args) {
|
|
11
12
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -45,6 +46,10 @@ const help = await runCli(["--help"]);
|
|
|
45
46
|
assertIncludes(help, "iola master", "help");
|
|
46
47
|
assertIncludes(help, "iola ask", "help");
|
|
47
48
|
|
|
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");
|
|
52
|
+
|
|
48
53
|
const commands = await runCli(["commands"]);
|
|
49
54
|
assertIncludes(commands, "iola browser status|install|open|text|html|screenshot|pdf|click|type|eval", "commands");
|
|
50
55
|
assertIncludes(commands, "iola mcp list|status|install|remove|serve [--stdio]", "commands");
|