@iola_adm/iola-cli 0.2.2 → 0.2.4
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
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import readline from "node:readline/promises";
|
|
|
10
10
|
import { Readable } from "node:stream";
|
|
11
11
|
import { stdin as input, stdout as output } from "node:process";
|
|
12
12
|
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
import tls from "node:tls";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { inflateRawSync, inflateSync } from "node:zlib";
|
|
15
16
|
|
|
@@ -700,6 +701,7 @@ async function startAgent() {
|
|
|
700
701
|
setTerminalTitle(`iola - ${path.basename(process.cwd()) || process.cwd()}`);
|
|
701
702
|
await showBanner();
|
|
702
703
|
await ensureAgentAiReady();
|
|
704
|
+
await printActiveAiModelLine();
|
|
703
705
|
console.log("Интерактивный режим. Введите /help для списка команд, /master чтобы запустить мастер настройки, /exit для выхода.");
|
|
704
706
|
await runHooks("SessionStart", { mode: "agent" });
|
|
705
707
|
|
|
@@ -713,6 +715,16 @@ async function startAgent() {
|
|
|
713
715
|
await runHooks("SessionEnd", { mode: "agent" });
|
|
714
716
|
}
|
|
715
717
|
|
|
718
|
+
async function printActiveAiModelLine() {
|
|
719
|
+
const config = await loadConfig();
|
|
720
|
+
const name = getActiveProfileName(config);
|
|
721
|
+
const profile = config.ai.profiles?.[name] || {
|
|
722
|
+
provider: config.ai.provider,
|
|
723
|
+
model: config.ai.model,
|
|
724
|
+
};
|
|
725
|
+
console.log(`Активная модель: ${name} (${profile.provider || "-"}, ${profile.model || "-"})`);
|
|
726
|
+
}
|
|
727
|
+
|
|
716
728
|
async function ensureAgentAiReady() {
|
|
717
729
|
const readiness = await getAiReadiness();
|
|
718
730
|
if (readiness.ready) return readiness;
|
|
@@ -4851,17 +4863,38 @@ function normalizeModelMenuTarget(value = "") {
|
|
|
4851
4863
|
}
|
|
4852
4864
|
|
|
4853
4865
|
async function chooseModelTarget() {
|
|
4866
|
+
const active = await getActiveAiSummary();
|
|
4854
4867
|
console.log("Выберите AI-подключение:");
|
|
4855
|
-
console.log(
|
|
4856
|
-
console.log(
|
|
4857
|
-
console.log(
|
|
4858
|
-
console.log(
|
|
4868
|
+
console.log(` 1. Локальные модели${active.group === "local" ? " [выбрано]" : ""}`);
|
|
4869
|
+
console.log(` 2. Российские AI (YandexGPT/GigaChat)${active.group === "russian" ? " [выбрано]" : ""}`);
|
|
4870
|
+
console.log(` 3. API (OpenAI/OpenRouter)${active.group === "api" ? " [выбрано]" : ""}`);
|
|
4871
|
+
console.log(` 4. Codex CLI${active.group === "codex" ? " [выбрано]" : ""}`);
|
|
4859
4872
|
console.log(" 0. Отмена");
|
|
4860
4873
|
|
|
4861
4874
|
const answer = await askText("Номер: ");
|
|
4862
4875
|
return { 1: "local", 2: "russian", 3: "api", 4: "codex" }[answer.trim()] || "";
|
|
4863
4876
|
}
|
|
4864
4877
|
|
|
4878
|
+
async function getActiveAiSummary() {
|
|
4879
|
+
const config = await loadConfig();
|
|
4880
|
+
const name = getActiveProfileName(config);
|
|
4881
|
+
const profile = config.ai.profiles?.[name] || {
|
|
4882
|
+
provider: config.ai.provider,
|
|
4883
|
+
model: config.ai.model,
|
|
4884
|
+
};
|
|
4885
|
+
const provider = profile.provider || "";
|
|
4886
|
+
const group = provider === "iola" || provider === "ollama"
|
|
4887
|
+
? "local"
|
|
4888
|
+
: provider === "yandexgpt" || provider === "gigachat"
|
|
4889
|
+
? "russian"
|
|
4890
|
+
: provider === "openai" || provider === "openrouter"
|
|
4891
|
+
? "api"
|
|
4892
|
+
: provider === "codex"
|
|
4893
|
+
? "codex"
|
|
4894
|
+
: "";
|
|
4895
|
+
return { name, provider, model: profile.model || "", group };
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4865
4898
|
async function openModelTargetMenu(target) {
|
|
4866
4899
|
if (target === "local") {
|
|
4867
4900
|
const selection = await chooseLocalModel();
|
|
@@ -4903,9 +4936,10 @@ async function openModelTargetMenu(target) {
|
|
|
4903
4936
|
|
|
4904
4937
|
async function chooseApiProvider() {
|
|
4905
4938
|
const config = await loadConfig();
|
|
4939
|
+
const active = await getActiveAiSummary();
|
|
4906
4940
|
const apiProfiles = Object.entries(config.ai.profiles || {})
|
|
4907
4941
|
.filter(([, profile]) => profile.provider === "openai" || profile.provider === "openrouter")
|
|
4908
|
-
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
|
|
4942
|
+
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})${active.provider === profile.provider ? " [выбрано]" : ""}` }));
|
|
4909
4943
|
const choices = [
|
|
4910
4944
|
...apiProfiles,
|
|
4911
4945
|
{ id: "openai", label: "OpenAI API" },
|
|
@@ -4922,9 +4956,10 @@ async function chooseApiProvider() {
|
|
|
4922
4956
|
|
|
4923
4957
|
async function chooseRussianProvider() {
|
|
4924
4958
|
const config = await loadConfig();
|
|
4959
|
+
const active = await getActiveAiSummary();
|
|
4925
4960
|
const russianProfiles = Object.entries(config.ai.profiles || {})
|
|
4926
4961
|
.filter(([, profile]) => profile.provider === "yandexgpt" || profile.provider === "gigachat")
|
|
4927
|
-
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
|
|
4962
|
+
.map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})${active.provider === profile.provider ? " [выбрано]" : ""}` }));
|
|
4928
4963
|
const choices = [
|
|
4929
4964
|
...russianProfiles,
|
|
4930
4965
|
{ id: "yandexgpt", label: "YandexGPT API" },
|
|
@@ -4948,15 +4983,16 @@ async function getDefaultApiProviderForModelSwitch() {
|
|
|
4948
4983
|
}
|
|
4949
4984
|
|
|
4950
4985
|
async function chooseLocalModel() {
|
|
4986
|
+
const active = await getActiveAiSummary();
|
|
4951
4987
|
const models = await listAiModels("ollama");
|
|
4952
4988
|
const choices = [
|
|
4953
|
-
{ id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router` },
|
|
4989
|
+
{ id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router${active.provider === "iola" && active.model === IOLA_LOCAL_MODEL ? " [выбрано]" : ""}` },
|
|
4954
4990
|
...models
|
|
4955
4991
|
.filter((model) => model.id !== IOLA_LOCAL_MODEL)
|
|
4956
4992
|
.map((model) => ({
|
|
4957
4993
|
id: model.id,
|
|
4958
4994
|
provider: "ollama",
|
|
4959
|
-
label: `${model.id}${model.note ? ` - ${model.note}` : ""}`,
|
|
4995
|
+
label: `${model.id}${model.note ? ` - ${model.note}` : ""}${active.provider === "ollama" && active.model === model.id ? " [выбрано]" : ""}`,
|
|
4960
4996
|
})),
|
|
4961
4997
|
{ id: "__manual__", provider: "ollama", label: "Другая Ollama-модель: ввести имя вручную" },
|
|
4962
4998
|
].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
|
|
@@ -4979,6 +5015,7 @@ async function chooseLocalModel() {
|
|
|
4979
5015
|
}
|
|
4980
5016
|
|
|
4981
5017
|
async function chooseAiModel(provider) {
|
|
5018
|
+
const active = await getActiveAiSummary();
|
|
4982
5019
|
if (provider === "openrouter") {
|
|
4983
5020
|
return chooseOpenRouterModel();
|
|
4984
5021
|
}
|
|
@@ -5024,7 +5061,8 @@ async function chooseAiModel(provider) {
|
|
|
5024
5061
|
console.log("Выберите модель:");
|
|
5025
5062
|
filtered.forEach((model, index) => {
|
|
5026
5063
|
const date = model.releaseDate ? ` (${model.releaseDate})` : "";
|
|
5027
|
-
|
|
5064
|
+
const selected = active.provider === provider && active.model === model.id ? " [выбрано]" : "";
|
|
5065
|
+
console.log(` ${index + 1}. ${model.id}${date}${model.note ? ` - ${model.note}` : ""}${selected}`);
|
|
5028
5066
|
});
|
|
5029
5067
|
console.log(" 0. Отмена");
|
|
5030
5068
|
|
|
@@ -5033,6 +5071,7 @@ async function chooseAiModel(provider) {
|
|
|
5033
5071
|
}
|
|
5034
5072
|
|
|
5035
5073
|
async function chooseOpenRouterModel() {
|
|
5074
|
+
const active = await getActiveAiSummary();
|
|
5036
5075
|
const ready = await ensureApiKeyForModelSelection("openrouter");
|
|
5037
5076
|
if (!ready) return "";
|
|
5038
5077
|
|
|
@@ -5077,7 +5116,8 @@ async function chooseOpenRouterModel() {
|
|
|
5077
5116
|
filtered.forEach((model, index) => {
|
|
5078
5117
|
const date = model.releaseDate || "дата неизвестна";
|
|
5079
5118
|
const context = model.contextLength ? `, ctx ${formatCompactNumber(model.contextLength)}` : "";
|
|
5080
|
-
|
|
5119
|
+
const selected = active.provider === "openrouter" && active.model === model.id ? " [выбрано]" : "";
|
|
5120
|
+
console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}${selected}`);
|
|
5081
5121
|
});
|
|
5082
5122
|
console.log(" 0. Назад");
|
|
5083
5123
|
|
|
@@ -8803,18 +8843,23 @@ async function callGigaChat(config, messages) {
|
|
|
8803
8843
|
}
|
|
8804
8844
|
|
|
8805
8845
|
const token = await getGigaChatAccessToken(config, authKey);
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8846
|
+
let response;
|
|
8847
|
+
try {
|
|
8848
|
+
response = await fetch(`${String(config.baseUrl || "https://gigachat.devices.sberbank.ru/api/v1").replace(/\/+$/, "")}/chat/completions`, {
|
|
8849
|
+
method: "POST",
|
|
8850
|
+
headers: {
|
|
8851
|
+
authorization: `Bearer ${token}`,
|
|
8852
|
+
"content-type": "application/json",
|
|
8853
|
+
},
|
|
8854
|
+
body: JSON.stringify({
|
|
8855
|
+
model: config.model || "GigaChat-2",
|
|
8856
|
+
messages,
|
|
8857
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
8858
|
+
}),
|
|
8859
|
+
});
|
|
8860
|
+
} catch (error) {
|
|
8861
|
+
throw new Error(formatProviderFetchError("GigaChat", error));
|
|
8862
|
+
}
|
|
8818
8863
|
|
|
8819
8864
|
if (!response.ok) {
|
|
8820
8865
|
const text = await response.text();
|
|
@@ -8826,17 +8871,23 @@ async function callGigaChat(config, messages) {
|
|
|
8826
8871
|
}
|
|
8827
8872
|
|
|
8828
8873
|
async function getGigaChatAccessToken(config, authKey) {
|
|
8874
|
+
enableSystemCaForGigaChat();
|
|
8829
8875
|
const secrets = await loadSecrets();
|
|
8830
8876
|
const scope = process.env.GIGACHAT_SCOPE || secrets.gigachat?.scope || config.scope || "GIGACHAT_API_PERS";
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8877
|
+
let response;
|
|
8878
|
+
try {
|
|
8879
|
+
response = await fetch(config.authUrl || "https://ngw.devices.sberbank.ru:9443/api/v2/oauth", {
|
|
8880
|
+
method: "POST",
|
|
8881
|
+
headers: {
|
|
8882
|
+
authorization: `Basic ${authKey}`,
|
|
8883
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
8884
|
+
RqUID: randomUUID(),
|
|
8885
|
+
},
|
|
8886
|
+
body: new URLSearchParams({ scope }).toString(),
|
|
8887
|
+
});
|
|
8888
|
+
} catch (error) {
|
|
8889
|
+
throw new Error(formatProviderFetchError("GigaChat OAuth", error));
|
|
8890
|
+
}
|
|
8840
8891
|
|
|
8841
8892
|
if (!response.ok) {
|
|
8842
8893
|
const text = await response.text();
|
|
@@ -8848,6 +8899,37 @@ async function getGigaChatAccessToken(config, authKey) {
|
|
|
8848
8899
|
return payload.access_token;
|
|
8849
8900
|
}
|
|
8850
8901
|
|
|
8902
|
+
let systemCaForGigaChatEnabled = false;
|
|
8903
|
+
|
|
8904
|
+
function enableSystemCaForGigaChat() {
|
|
8905
|
+
if (systemCaForGigaChatEnabled || process.env.GIGACHAT_DISABLE_SYSTEM_CA === "1") return;
|
|
8906
|
+
systemCaForGigaChatEnabled = true;
|
|
8907
|
+
|
|
8908
|
+
try {
|
|
8909
|
+
if (typeof tls.getCACertificates !== "function" || typeof tls.setDefaultCACertificates !== "function") return;
|
|
8910
|
+
const certificates = [
|
|
8911
|
+
...tls.getCACertificates("system"),
|
|
8912
|
+
...tls.getCACertificates("bundled"),
|
|
8913
|
+
...tls.getCACertificates("extra"),
|
|
8914
|
+
];
|
|
8915
|
+
if (certificates.length > 0) tls.setDefaultCACertificates([...new Set(certificates)]);
|
|
8916
|
+
} catch {
|
|
8917
|
+
// Older Node builds may not expose system CA management. The fetch error below
|
|
8918
|
+
// will include the concrete TLS/network cause and the manual workaround.
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
|
|
8922
|
+
function formatProviderFetchError(provider, error) {
|
|
8923
|
+
const cause = error?.cause;
|
|
8924
|
+
const causeCode = cause?.code ? `${cause.code}: ` : "";
|
|
8925
|
+
const causeMessage = cause?.message || "";
|
|
8926
|
+
const details = `${error?.message || "fetch failed"}${causeMessage ? ` (${causeCode}${causeMessage})` : ""}`;
|
|
8927
|
+
if (/SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_GET_ISSUER_CERT|CERT_/i.test(`${cause?.code || ""} ${causeMessage}`)) {
|
|
8928
|
+
return `${provider} network error: ${details}\nNode не доверяет цепочке сертификатов провайдера. CLI пробует использовать системные сертификаты ОС автоматически; если ошибка повторяется, обновите Node.js или запустите CLI с NODE_OPTIONS=--use-system-ca.`;
|
|
8929
|
+
}
|
|
8930
|
+
return `${provider} network error: ${details}`;
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8851
8933
|
function getAiNetworkMode(config = {}) {
|
|
8852
8934
|
return validateAiNetworkMode(AI_NETWORK_MODE || config.networkMode || "gateway");
|
|
8853
8935
|
}
|
|
@@ -99,12 +99,23 @@ CLI также понимает env-переменные `YANDEXGPT_API_KEY` и
|
|
|
99
99
|
Пошаговая настройка:
|
|
100
100
|
|
|
101
101
|
1. Перейдите на портал разработчиков Сбера: `https://developers.sber.ru/`.
|
|
102
|
-
2. Войдите в аккаунт.
|
|
103
|
-
3. Откройте
|
|
104
|
-
4.
|
|
105
|
-
5.
|
|
106
|
-
6.
|
|
107
|
-
7.
|
|
102
|
+
2. Войдите в аккаунт. Если вход еще не выполнен, портал откроет страницу авторизации: `https://developers.sber.ru/studio/login`.
|
|
103
|
+
3. Откройте страницу GigaChat API. Прямой путь: `https://developers.sber.ru/studio/workspaces/my-space/get/gigachat-api`.
|
|
104
|
+
4. Нажмите кнопку подключения или `Попробовать`. После входа откроется форма создания проекта.
|
|
105
|
+
5. В форме `Почти готово` заполните `Название проекта`. Можно оставить предложенное название `Мой GigaChat API` или указать `iola-cli`.
|
|
106
|
+
6. В блоке `Группа проекта` нажмите `Выбрать группу`.
|
|
107
|
+
7. Если есть вариант `Без группы`, выберите его. Если создание проекта после этого недоступно, выберите `Создать группу`, задайте понятное имя, например `iola-cli`, и сохраните группу.
|
|
108
|
+
8. Нажмите `Создать проект`.
|
|
109
|
+
9. После создания проекта откроется страница GigaChat API. В верхней части будет название проекта, например `Мой GigaChat API`.
|
|
110
|
+
10. На странице проекта нажмите `Настроить API`. Если кнопки не видно, откройте раздел настроек проекта GigaChat API в левом меню.
|
|
111
|
+
11. На странице `Настройка API` найдите блок `Данные для авторизации запросов к API`.
|
|
112
|
+
12. Проверьте поле `Scope`. Для личного Freemium-доступа обычно указан `GIGACHAT_API_PERS`. Это значение понадобится CLI.
|
|
113
|
+
13. В блоке `Ключ авторизации` нажмите `Получить ключ`.
|
|
114
|
+
14. Откроется окно `Сохраните ваш Authorization Key`. Не закрывайте его, пока не сохраните данные.
|
|
115
|
+
15. Скопируйте поле `Authorization Key`. Это и есть значение `GIGACHAT_AUTH_KEY` для CLI.
|
|
116
|
+
16. При желании также сохраните `Client ID` и `Client Secret`, но CLI они не нужны, если вы уже скопировали `Authorization Key`.
|
|
117
|
+
17. Нажмите `Готово` только после сохранения ключа.
|
|
118
|
+
18. Сохраните ключ локально в CLI:
|
|
108
119
|
|
|
109
120
|
```bash
|
|
110
121
|
iola ai key set gigachat
|
|
@@ -115,6 +126,8 @@ CLI попросит ввести:
|
|
|
115
126
|
- `GIGACHAT_AUTH_KEY` - authorization key;
|
|
116
127
|
- `scope` - по умолчанию `GIGACHAT_API_PERS` для персонального доступа.
|
|
117
128
|
|
|
129
|
+
Важно: `Authorization Key` показывается один раз. Если закрыть окно и не сохранить ключ, его нельзя посмотреть повторно. В этом случае нажмите `Получить новый ключ` и сохраните новое значение. `Authorization Key` - это уже готовая строка для заголовка `Authorization: Basic ...`; вручную кодировать `Client ID:Client Secret` в base64 не нужно.
|
|
130
|
+
|
|
118
131
|
После сохранения ключа выберите профиль и модель:
|
|
119
132
|
|
|
120
133
|
```bash
|