@iola_adm/iola-cli 0.1.6 → 0.1.7
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/README.md +23 -2
- package/package.json +1 -1
- package/src/cli.js +122 -2
package/README.md
CHANGED
|
@@ -79,6 +79,8 @@ iola agent
|
|
|
79
79
|
iola ai doctor
|
|
80
80
|
iola ai setup ollama
|
|
81
81
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
82
|
+
iola ai key set openai
|
|
83
|
+
iola ai key status
|
|
82
84
|
iola ai setup openai --model gpt-4.1-mini
|
|
83
85
|
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
84
86
|
iola health
|
|
@@ -135,7 +137,7 @@ iola ai ask "Какие школы есть на улице Петрова?"
|
|
|
135
137
|
OpenAI:
|
|
136
138
|
|
|
137
139
|
```bash
|
|
138
|
-
set
|
|
140
|
+
iola ai key set openai
|
|
139
141
|
iola ai setup openai --model gpt-4.1-mini
|
|
140
142
|
iola ai ask "Найди школу 29"
|
|
141
143
|
```
|
|
@@ -143,11 +145,30 @@ iola ai ask "Найди школу 29"
|
|
|
143
145
|
OpenRouter:
|
|
144
146
|
|
|
145
147
|
```bash
|
|
146
|
-
set
|
|
148
|
+
iola ai key set openrouter
|
|
147
149
|
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
148
150
|
iola ai ask "Покажи контакты лицея"
|
|
149
151
|
```
|
|
150
152
|
|
|
153
|
+
Ключи OpenAI/OpenRouter сохраняются локально на компьютере пользователя:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
%USERPROFILE%\.iola\secrets.json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Управление ключами:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
iola ai key set openai
|
|
163
|
+
iola ai key set openrouter
|
|
164
|
+
iola ai key status
|
|
165
|
+
iola ai key delete openai
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Если одновременно задана переменная окружения (`OPENAI_API_KEY` или
|
|
169
|
+
`OPENROUTER_API_KEY`) и сохранен локальный ключ, CLI использует переменную
|
|
170
|
+
окружения как более приоритетную.
|
|
171
|
+
|
|
151
172
|
AI-ответ строится по контексту из публичного API. Если данных в контексте
|
|
152
173
|
недостаточно, ассистент должен сообщить об этом, а не выдумывать сведения.
|
|
153
174
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/a
|
|
|
9
9
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
10
10
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
11
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
|
+
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
12
13
|
const DEFAULT_AI_CONFIG = {
|
|
13
14
|
ai: {
|
|
14
15
|
provider: "ollama",
|
|
@@ -64,6 +65,10 @@ Usage:
|
|
|
64
65
|
iola banner
|
|
65
66
|
iola agent
|
|
66
67
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
68
|
+
iola ai key set openai
|
|
69
|
+
iola ai key set openrouter
|
|
70
|
+
iola ai key status
|
|
71
|
+
iola ai key delete openai|openrouter
|
|
67
72
|
iola ai doctor [--json]
|
|
68
73
|
iola ai setup
|
|
69
74
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -285,6 +290,10 @@ async function handleAi(args) {
|
|
|
285
290
|
showBanner();
|
|
286
291
|
console.log(`AI-команды:
|
|
287
292
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
293
|
+
iola ai key set openai
|
|
294
|
+
iola ai key set openrouter
|
|
295
|
+
iola ai key status
|
|
296
|
+
iola ai key delete openai|openrouter
|
|
288
297
|
iola ai doctor [--json]
|
|
289
298
|
iola ai setup
|
|
290
299
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -300,6 +309,11 @@ async function handleAi(args) {
|
|
|
300
309
|
return;
|
|
301
310
|
}
|
|
302
311
|
|
|
312
|
+
if (subcommand === "key") {
|
|
313
|
+
await handleAiKey(rest);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
303
317
|
if (subcommand === "doctor") {
|
|
304
318
|
await aiDoctor(rest);
|
|
305
319
|
return;
|
|
@@ -364,6 +378,86 @@ async function aiSetup(args) {
|
|
|
364
378
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
365
379
|
}
|
|
366
380
|
|
|
381
|
+
async function handleAiKey(args) {
|
|
382
|
+
const [action, provider] = args;
|
|
383
|
+
|
|
384
|
+
if (action === "set") {
|
|
385
|
+
await setAiKey(provider);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (action === "status") {
|
|
390
|
+
await printAiKeyStatus();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (action === "delete") {
|
|
395
|
+
await deleteAiKey(provider);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
throw new Error(`Unknown key command. Use:
|
|
400
|
+
iola ai key set openai
|
|
401
|
+
iola ai key set openrouter
|
|
402
|
+
iola ai key status
|
|
403
|
+
iola ai key delete openai|openrouter`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function setAiKey(provider) {
|
|
407
|
+
assertKeyProvider(provider);
|
|
408
|
+
|
|
409
|
+
if (!process.stdin.isTTY) {
|
|
410
|
+
throw new Error("Для сохранения ключа запустите команду в интерактивном терминале.");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const envName = provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY";
|
|
414
|
+
const rl = readline.createInterface({ input, output });
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const key = (await rl.question(`Введите ${envName}: `)).trim();
|
|
418
|
+
|
|
419
|
+
if (!key) {
|
|
420
|
+
throw new Error("Ключ пустой, сохранение отменено.");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const secrets = await loadSecrets();
|
|
424
|
+
secrets[provider] = { apiKey: key };
|
|
425
|
+
await saveSecrets(secrets);
|
|
426
|
+
console.log(`Ключ ${provider} сохранен локально: ${SECRETS_FILE}`);
|
|
427
|
+
} finally {
|
|
428
|
+
rl.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function printAiKeyStatus() {
|
|
433
|
+
const secrets = await loadSecrets();
|
|
434
|
+
const rows = ["openai", "openrouter"].map((provider) => ({
|
|
435
|
+
provider,
|
|
436
|
+
env: provider === "openai" ? (process.env.OPENAI_API_KEY ? "yes" : "no") : (process.env.OPENROUTER_API_KEY ? "yes" : "no"),
|
|
437
|
+
local: secrets[provider]?.apiKey ? "yes" : "no",
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
printTable(rows, [
|
|
441
|
+
["provider", "Провайдер"],
|
|
442
|
+
["env", "Env"],
|
|
443
|
+
["local", "Локально"],
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function deleteAiKey(provider) {
|
|
448
|
+
assertKeyProvider(provider);
|
|
449
|
+
const secrets = await loadSecrets();
|
|
450
|
+
delete secrets[provider];
|
|
451
|
+
await saveSecrets(secrets);
|
|
452
|
+
console.log(`Локальный ключ ${provider} удален.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function assertKeyProvider(provider) {
|
|
456
|
+
if (provider !== "openai" && provider !== "openrouter") {
|
|
457
|
+
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
367
461
|
async function chooseAiProvider() {
|
|
368
462
|
console.log("Выберите режим AI:");
|
|
369
463
|
console.log("1. Локальная модель через Ollama");
|
|
@@ -522,11 +616,11 @@ async function callAiProvider(config, messages) {
|
|
|
522
616
|
}
|
|
523
617
|
|
|
524
618
|
if (config.provider === "openai") {
|
|
525
|
-
return callOpenAiCompatible(config, messages,
|
|
619
|
+
return callOpenAiCompatible(config, messages, await getApiKey("openai"), "OpenAI");
|
|
526
620
|
}
|
|
527
621
|
|
|
528
622
|
if (config.provider === "openrouter") {
|
|
529
|
-
return callOpenAiCompatible(config, messages,
|
|
623
|
+
return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
|
|
530
624
|
}
|
|
531
625
|
|
|
532
626
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
@@ -586,6 +680,19 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
586
680
|
return payload.choices?.[0]?.message?.content || "";
|
|
587
681
|
}
|
|
588
682
|
|
|
683
|
+
async function getApiKey(provider) {
|
|
684
|
+
if (provider === "openai" && process.env.OPENAI_API_KEY) {
|
|
685
|
+
return process.env.OPENAI_API_KEY;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (provider === "openrouter" && process.env.OPENROUTER_API_KEY) {
|
|
689
|
+
return process.env.OPENROUTER_API_KEY;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const secrets = await loadSecrets();
|
|
693
|
+
return secrets[provider]?.apiKey || "";
|
|
694
|
+
}
|
|
695
|
+
|
|
589
696
|
async function listLayers(args) {
|
|
590
697
|
const options = parseOptions(args);
|
|
591
698
|
const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
|
|
@@ -972,6 +1079,19 @@ function mergeConfig(base, override) {
|
|
|
972
1079
|
};
|
|
973
1080
|
}
|
|
974
1081
|
|
|
1082
|
+
async function loadSecrets() {
|
|
1083
|
+
try {
|
|
1084
|
+
return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
|
|
1085
|
+
} catch {
|
|
1086
|
+
return {};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function saveSecrets(value) {
|
|
1091
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
1092
|
+
await writeFile(SECRETS_FILE, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1093
|
+
}
|
|
1094
|
+
|
|
975
1095
|
async function printAiConfig() {
|
|
976
1096
|
const config = await loadConfig();
|
|
977
1097
|
printJson({
|