@iola_adm/iola-cli 0.1.10 → 0.1.12
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 +50 -1
- package/package.json +2 -2
- package/src/cli.js +601 -25
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@ node --version
|
|
|
18
18
|
npm --version
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Нужен Node.js `
|
|
21
|
+
Нужен Node.js `22.5.0` или новее. Это нужно для встроенной SQLite-БД
|
|
22
|
+
`node:sqlite`, которую CLI будет использовать для локальной истории, кеша и
|
|
23
|
+
сессий без дополнительных нативных зависимостей.
|
|
24
|
+
|
|
25
|
+
Если Node.js не установлен или версия ниже `22.5.0`:
|
|
22
26
|
|
|
23
27
|
```bash
|
|
24
28
|
# Windows
|
|
@@ -54,6 +58,8 @@ curl -fsSL https://ollama.com/install.sh | sh
|
|
|
54
58
|
Диагностика ПК и подбор локальной модели:
|
|
55
59
|
|
|
56
60
|
```bash
|
|
61
|
+
npx -y @iola_adm/iola-cli init
|
|
62
|
+
npx -y @iola_adm/iola-cli init --upgrade-node
|
|
57
63
|
npx -y @iola_adm/iola-cli ai doctor
|
|
58
64
|
npx -y @iola_adm/iola-cli ai setup ollama
|
|
59
65
|
```
|
|
@@ -93,6 +99,12 @@ iola data schools --where address=Петрова --columns name,address,phone
|
|
|
93
99
|
iola data schools --format csv
|
|
94
100
|
iola ai doctor
|
|
95
101
|
iola ai setup ollama
|
|
102
|
+
iola ai setup codex --model gpt-5.5
|
|
103
|
+
iola ai profiles
|
|
104
|
+
iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b
|
|
105
|
+
iola ai profile use router-qwen
|
|
106
|
+
iola ai models openrouter --search qwen
|
|
107
|
+
iola ai models codex
|
|
96
108
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
97
109
|
iola ai context "школа 29"
|
|
98
110
|
iola ai key set openai
|
|
@@ -137,6 +149,9 @@ iola agent
|
|
|
137
149
|
/mcp-info
|
|
138
150
|
/ai doctor
|
|
139
151
|
/context школа 29
|
|
152
|
+
/profiles
|
|
153
|
+
/profile use local
|
|
154
|
+
/models openrouter --search qwen
|
|
140
155
|
/use ollama
|
|
141
156
|
/use openai
|
|
142
157
|
/key status
|
|
@@ -180,6 +195,40 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
180
195
|
iola ai ask "Покажи контакты лицея"
|
|
181
196
|
```
|
|
182
197
|
|
|
198
|
+
Codex CLI:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
codex login
|
|
202
|
+
iola ai setup codex --model gpt-5.5
|
|
203
|
+
iola setup codex
|
|
204
|
+
iola ask "Назови ИНН школы 29"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
AI-профили позволяют держать локальную модель, OpenAI, OpenRouter и Codex
|
|
208
|
+
одновременно:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
iola ai profiles
|
|
212
|
+
iola ai profile add local-small --provider ollama --model llama3.2:1b
|
|
213
|
+
iola ai profile add gpt --provider openai --model gpt-4.1-mini
|
|
214
|
+
iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b
|
|
215
|
+
iola ai profile add codex-read --provider codex --model gpt-5.5 --sandbox read-only
|
|
216
|
+
iola ai profile use router-qwen
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Списки моделей:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
iola ai models ollama
|
|
223
|
+
iola ai models openai
|
|
224
|
+
iola ai models openrouter --search qwen
|
|
225
|
+
iola ai models codex
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Для OpenAI список моделей требует сохраненный ключ. OpenRouter берется из
|
|
229
|
+
публичного API OpenRouter. Ollama читает локальные модели через `api/tags`, а
|
|
230
|
+
если Ollama не запущен, показывает рекомендуемые локальные модели.
|
|
231
|
+
|
|
183
232
|
Проверить, какие данные попадут в AI-контекст:
|
|
184
233
|
|
|
185
234
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iola_adm/iola-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/adm-iola/iola-cli#readme",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"cli"
|
|
37
37
|
],
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
39
|
+
"node": ">=22.5.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline/promises";
|
|
@@ -7,6 +7,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
7
7
|
|
|
8
8
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
9
9
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
10
|
+
const MIN_NODE_VERSION = "22.5.0";
|
|
10
11
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
12
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
13
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
@@ -16,9 +17,34 @@ const DEFAULT_AI_CONFIG = {
|
|
|
16
17
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
17
18
|
},
|
|
18
19
|
ai: {
|
|
20
|
+
activeProfile: "local",
|
|
19
21
|
provider: "ollama",
|
|
20
22
|
model: "llama3.2:1b",
|
|
21
23
|
baseUrl: "http://127.0.0.1:11434",
|
|
24
|
+
profiles: {
|
|
25
|
+
local: {
|
|
26
|
+
provider: "ollama",
|
|
27
|
+
model: "llama3.2:1b",
|
|
28
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
29
|
+
},
|
|
30
|
+
openai: {
|
|
31
|
+
provider: "openai",
|
|
32
|
+
model: "gpt-4.1-mini",
|
|
33
|
+
baseUrl: "https://api.openai.com/v1",
|
|
34
|
+
},
|
|
35
|
+
openrouter: {
|
|
36
|
+
provider: "openrouter",
|
|
37
|
+
model: "openai/gpt-4.1-mini",
|
|
38
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
39
|
+
},
|
|
40
|
+
codex: {
|
|
41
|
+
provider: "codex",
|
|
42
|
+
model: "gpt-5.5",
|
|
43
|
+
sandbox: "read-only",
|
|
44
|
+
approval: "never",
|
|
45
|
+
cwd: ".",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
22
48
|
},
|
|
23
49
|
};
|
|
24
50
|
const DATASETS = {
|
|
@@ -69,6 +95,11 @@ const COMMANDS = new Map([
|
|
|
69
95
|
|
|
70
96
|
export async function main(argv) {
|
|
71
97
|
const [command = "help", ...args] = argv;
|
|
98
|
+
const nodeStatus = getNodeRequirementStatus();
|
|
99
|
+
if (!nodeStatus.ok && !["help", "version", "doctor", "init"].includes(command)) {
|
|
100
|
+
throw new Error(`Нужен Node.js ${MIN_NODE_VERSION} или новее. Сейчас: ${nodeStatus.current}. Запустите: iola init --upgrade-node`);
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
const handler = COMMANDS.get(command);
|
|
73
104
|
|
|
74
105
|
if (!handler) {
|
|
@@ -101,6 +132,11 @@ Usage:
|
|
|
101
132
|
iola ai key set openrouter
|
|
102
133
|
iola ai key status
|
|
103
134
|
iola ai key delete openai|openrouter
|
|
135
|
+
iola ai profiles
|
|
136
|
+
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
137
|
+
iola ai profile use NAME
|
|
138
|
+
iola ai profile delete NAME
|
|
139
|
+
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
104
140
|
iola ai doctor [--json]
|
|
105
141
|
iola ai setup
|
|
106
142
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -118,6 +154,9 @@ Usage:
|
|
|
118
154
|
Environment:
|
|
119
155
|
IOLA_API_BASE_URL default: ${API_BASE_URL}
|
|
120
156
|
IOLA_MCP_BASE_URL default: ${MCP_BASE_URL}
|
|
157
|
+
|
|
158
|
+
Requirements:
|
|
159
|
+
Node.js >= ${MIN_NODE_VERSION}
|
|
121
160
|
`);
|
|
122
161
|
}
|
|
123
162
|
|
|
@@ -210,6 +249,21 @@ async function handleAgentLine(line, state) {
|
|
|
210
249
|
return false;
|
|
211
250
|
}
|
|
212
251
|
|
|
252
|
+
if (command === "profiles") {
|
|
253
|
+
await handleAiProfile(["list", ...args]);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (command === "profile") {
|
|
258
|
+
await handleAiProfile(args);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (command === "models") {
|
|
263
|
+
await aiModels(args);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
213
267
|
if (command === "use") {
|
|
214
268
|
await useAiProvider(args);
|
|
215
269
|
return false;
|
|
@@ -290,6 +344,9 @@ function printAgentHelp() {
|
|
|
290
344
|
/search лицей --limit 3
|
|
291
345
|
/mcp-info
|
|
292
346
|
/context школа 29
|
|
347
|
+
/profiles
|
|
348
|
+
/profile use local
|
|
349
|
+
/models openrouter --search qwen
|
|
293
350
|
/ai doctor
|
|
294
351
|
/ai setup ollama
|
|
295
352
|
/use openai
|
|
@@ -402,6 +459,7 @@ async function doctor(args = []) {
|
|
|
402
459
|
const options = parseOptions(args);
|
|
403
460
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
404
461
|
const config = await loadConfig();
|
|
462
|
+
const activeAiProfile = resolveAiProfile(config);
|
|
405
463
|
const secrets = await loadSecrets();
|
|
406
464
|
const diagnostics = await getLocalDiagnostics();
|
|
407
465
|
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
@@ -412,6 +470,9 @@ async function doctor(args = []) {
|
|
|
412
470
|
version: packageJson.default.version,
|
|
413
471
|
npmLatest: latest || "-",
|
|
414
472
|
update: getUpdateStatus(packageJson.default.version, latest),
|
|
473
|
+
node: process.version,
|
|
474
|
+
nodeRequired: `>=${MIN_NODE_VERSION}`,
|
|
475
|
+
nodeStatus: getNodeRequirementStatus().ok ? "ok" : "upgrade-required",
|
|
415
476
|
},
|
|
416
477
|
api: {
|
|
417
478
|
baseUrl: apiBaseUrl,
|
|
@@ -419,9 +480,10 @@ async function doctor(args = []) {
|
|
|
419
480
|
health: await probeEndpoint(`${mcpBaseUrl}/mcp-health`),
|
|
420
481
|
},
|
|
421
482
|
ai: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
483
|
+
activeProfile: getActiveProfileName(config),
|
|
484
|
+
provider: activeAiProfile.provider,
|
|
485
|
+
model: activeAiProfile.model,
|
|
486
|
+
modelAvailable: await checkConfiguredModel({ ai: activeAiProfile }),
|
|
425
487
|
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
426
488
|
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
427
489
|
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
@@ -464,6 +526,69 @@ function getUpdateStatus(current, latest) {
|
|
|
464
526
|
return "ok";
|
|
465
527
|
}
|
|
466
528
|
|
|
529
|
+
function getNodeRequirementStatus() {
|
|
530
|
+
const current = process.versions.node;
|
|
531
|
+
return {
|
|
532
|
+
current,
|
|
533
|
+
required: MIN_NODE_VERSION,
|
|
534
|
+
ok: compareVersions(current, MIN_NODE_VERSION) >= 0,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function offerNodeUpgrade(options, status) {
|
|
539
|
+
console.log(`Текущая версия Node.js: ${status.current}. Нужна ${MIN_NODE_VERSION} или новее.`);
|
|
540
|
+
|
|
541
|
+
if (!process.stdin.isTTY && !options["upgrade-node"]) {
|
|
542
|
+
printNodeUpgradeInstructions();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const shouldUpgrade = options["upgrade-node"] || (await confirm("Обновить Node.js установщиком сейчас? [y/N] "));
|
|
547
|
+
|
|
548
|
+
if (!shouldUpgrade) {
|
|
549
|
+
printNodeUpgradeInstructions();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
await upgradeNodeWithInstaller();
|
|
554
|
+
console.log("");
|
|
555
|
+
console.log("После обновления перезапустите терминал и проверьте:");
|
|
556
|
+
console.log(" node --version");
|
|
557
|
+
console.log(" iola init");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function printNodeUpgradeInstructions() {
|
|
561
|
+
console.log("Обновите Node.js:");
|
|
562
|
+
console.log(" Windows: winget install OpenJS.NodeJS.LTS");
|
|
563
|
+
console.log(" macOS: brew install node");
|
|
564
|
+
console.log(" Linux: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function upgradeNodeWithInstaller() {
|
|
568
|
+
if (process.platform === "win32") {
|
|
569
|
+
try {
|
|
570
|
+
await runCommand("winget", ["upgrade", "OpenJS.NodeJS.LTS", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
571
|
+
} catch {
|
|
572
|
+
await runCommand("winget", ["install", "OpenJS.NodeJS.LTS", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (process.platform === "darwin") {
|
|
578
|
+
try {
|
|
579
|
+
await runCommand("brew", ["upgrade", "node"], { inherit: true });
|
|
580
|
+
} catch {
|
|
581
|
+
await runCommand("brew", ["install", "node"], { inherit: true });
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await runCommand("sh", [
|
|
587
|
+
"-c",
|
|
588
|
+
"curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs",
|
|
589
|
+
], { inherit: true });
|
|
590
|
+
}
|
|
591
|
+
|
|
467
592
|
async function checkConfiguredModel(config) {
|
|
468
593
|
if (config.ai.provider !== "ollama") {
|
|
469
594
|
return "external-api";
|
|
@@ -486,17 +611,25 @@ async function checkConfiguredModel(config) {
|
|
|
486
611
|
|
|
487
612
|
async function initCli(args = []) {
|
|
488
613
|
const options = parseOptions(args);
|
|
614
|
+
const nodeStatus = getNodeRequirementStatus();
|
|
489
615
|
|
|
490
616
|
showBanner();
|
|
491
617
|
console.log("Проверка окружения");
|
|
492
618
|
printKeyValue({
|
|
493
619
|
node: process.version,
|
|
620
|
+
node_required: `>=${MIN_NODE_VERSION}`,
|
|
621
|
+
node_status: nodeStatus.ok ? "ok" : "нужно обновить",
|
|
494
622
|
npm: await getCommandVersion("npm", ["--version"]),
|
|
495
623
|
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
496
624
|
mcp: await getMcpBaseUrl(),
|
|
497
625
|
});
|
|
498
626
|
console.log("");
|
|
499
627
|
|
|
628
|
+
if (!nodeStatus.ok) {
|
|
629
|
+
await offerNodeUpgrade(options, nodeStatus);
|
|
630
|
+
console.log("");
|
|
631
|
+
}
|
|
632
|
+
|
|
500
633
|
await aiDoctor(options.json ? ["--json"] : []);
|
|
501
634
|
|
|
502
635
|
if (!process.stdin.isTTY || options.yes) {
|
|
@@ -531,6 +664,11 @@ async function handleAi(args) {
|
|
|
531
664
|
iola ai key set openrouter
|
|
532
665
|
iola ai key status
|
|
533
666
|
iola ai key delete openai|openrouter
|
|
667
|
+
iola ai profiles
|
|
668
|
+
iola ai profile add NAME --provider ollama|openai|openrouter|codex --model MODEL
|
|
669
|
+
iola ai profile use NAME
|
|
670
|
+
iola ai profile delete NAME
|
|
671
|
+
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
534
672
|
iola ai doctor [--json]
|
|
535
673
|
iola ai setup
|
|
536
674
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -556,6 +694,21 @@ async function handleAi(args) {
|
|
|
556
694
|
return;
|
|
557
695
|
}
|
|
558
696
|
|
|
697
|
+
if (subcommand === "profiles") {
|
|
698
|
+
await handleAiProfile(["list", ...rest]);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (subcommand === "profile") {
|
|
703
|
+
await handleAiProfile(rest);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (subcommand === "models") {
|
|
708
|
+
await aiModels(rest);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
559
712
|
if (subcommand === "doctor") {
|
|
560
713
|
await aiDoctor(rest);
|
|
561
714
|
return;
|
|
@@ -641,21 +794,50 @@ async function aiSetup(args) {
|
|
|
641
794
|
if (provider === "openai" || provider === "openrouter") {
|
|
642
795
|
const options = parseOptions(args.slice(1));
|
|
643
796
|
const model = options.model || (provider === "openai" ? "gpt-4.1-mini" : "openai/gpt-4.1-mini");
|
|
797
|
+
const profileName = options.name || provider;
|
|
798
|
+
const profile = buildProfileFromOptions(provider, { ...options, model });
|
|
799
|
+
const config = await loadConfig();
|
|
644
800
|
await saveConfig({
|
|
645
801
|
ai: {
|
|
802
|
+
...config.ai,
|
|
803
|
+
activeProfile: profileName,
|
|
646
804
|
provider,
|
|
647
805
|
model,
|
|
648
|
-
baseUrl:
|
|
806
|
+
baseUrl: profile.baseUrl,
|
|
807
|
+
profiles: {
|
|
808
|
+
...(config.ai.profiles || {}),
|
|
809
|
+
[profileName]: profile,
|
|
810
|
+
},
|
|
649
811
|
},
|
|
650
812
|
});
|
|
651
|
-
console.log(`AI-профиль ${
|
|
813
|
+
console.log(`AI-профиль ${profileName} сохранен и выбран в ${CONFIG_FILE}`);
|
|
652
814
|
console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
|
|
653
815
|
console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
654
816
|
return;
|
|
655
817
|
}
|
|
656
818
|
|
|
657
819
|
if (provider === "codex") {
|
|
658
|
-
|
|
820
|
+
const options = parseOptions(args.slice(1));
|
|
821
|
+
const profileName = options.name || "codex";
|
|
822
|
+
const profile = buildProfileFromOptions("codex", options);
|
|
823
|
+
const config = await loadConfig();
|
|
824
|
+
await saveConfig({
|
|
825
|
+
ai: {
|
|
826
|
+
...config.ai,
|
|
827
|
+
activeProfile: profileName,
|
|
828
|
+
provider: "codex",
|
|
829
|
+
model: profile.model,
|
|
830
|
+
profiles: {
|
|
831
|
+
...(config.ai.profiles || {}),
|
|
832
|
+
[profileName]: profile,
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
console.log(`AI-профиль ${profileName} сохранен и выбран.`);
|
|
837
|
+
console.log("Проверка Codex CLI:");
|
|
838
|
+
console.log(` ${await getCommandVersion("codex", ["--version"])}`);
|
|
839
|
+
console.log("MCP подключается отдельно командой:");
|
|
840
|
+
console.log(" iola setup codex");
|
|
659
841
|
return;
|
|
660
842
|
}
|
|
661
843
|
|
|
@@ -687,33 +869,318 @@ async function handleAiKey(args) {
|
|
|
687
869
|
iola ai key delete openai|openrouter`);
|
|
688
870
|
}
|
|
689
871
|
|
|
690
|
-
async function
|
|
872
|
+
async function handleAiProfile(args) {
|
|
873
|
+
const [action = "list", name, ...rest] = args;
|
|
874
|
+
|
|
875
|
+
if (action === "list" || action === "ls") {
|
|
876
|
+
await printAiProfiles();
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (action === "show") {
|
|
881
|
+
await showAiProfile(name);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (action === "use") {
|
|
886
|
+
await useAiProfile(name);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (action === "add" || action === "set") {
|
|
891
|
+
await addAiProfile(name, rest);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (action === "delete" || action === "remove" || action === "rm") {
|
|
896
|
+
await deleteAiProfile(name);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
throw new Error(`Unknown profile command. Use:
|
|
901
|
+
iola ai profiles
|
|
902
|
+
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
903
|
+
iola ai profile use NAME
|
|
904
|
+
iola ai profile delete NAME`);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
async function aiModels(args) {
|
|
691
908
|
const [provider] = args;
|
|
909
|
+
const options = parseOptions(args.slice(1));
|
|
910
|
+
|
|
911
|
+
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
912
|
+
throw new Error("Провайдер обязателен: iola ai models ollama|openai|openrouter|codex");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const models = await listAiModels(provider);
|
|
916
|
+
const filtered = options.search
|
|
917
|
+
? models.filter((model) => model.id.toLocaleLowerCase("ru-RU").includes(options.search.toLocaleLowerCase("ru-RU")))
|
|
918
|
+
: models;
|
|
919
|
+
|
|
920
|
+
if (options.json) {
|
|
921
|
+
printJson(filtered);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
printTable(filtered, [
|
|
926
|
+
["id", "Модель"],
|
|
927
|
+
["provider", "Провайдер"],
|
|
928
|
+
["note", "Примечание"],
|
|
929
|
+
]);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function listAiModels(provider) {
|
|
933
|
+
if (provider === "ollama") {
|
|
934
|
+
try {
|
|
935
|
+
const config = await loadConfig();
|
|
936
|
+
const response = await fetch(`${config.ai.profiles?.local?.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
|
|
937
|
+
|
|
938
|
+
if (!response.ok) {
|
|
939
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const payload = await response.json();
|
|
943
|
+
return (payload.models || []).map((model) => ({
|
|
944
|
+
id: model.name,
|
|
945
|
+
provider: "ollama",
|
|
946
|
+
note: model.modified_at ? `updated ${model.modified_at}` : "local",
|
|
947
|
+
}));
|
|
948
|
+
} catch {
|
|
949
|
+
return [
|
|
950
|
+
{ id: "llama3.2:1b", provider: "ollama", note: "recommended low RAM" },
|
|
951
|
+
{ id: "llama3.2:3b", provider: "ollama", note: "recommended standard" },
|
|
952
|
+
{ id: "qwen3:4b", provider: "ollama", note: "recommended balanced" },
|
|
953
|
+
{ id: "qwen3:8b", provider: "ollama", note: "recommended good GPU" },
|
|
954
|
+
];
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (provider === "openai") {
|
|
959
|
+
const apiKey = await getApiKey("openai");
|
|
960
|
+
if (!apiKey) {
|
|
961
|
+
throw new Error("OpenAI API key не найден. Выполните iola ai key set openai.");
|
|
962
|
+
}
|
|
963
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
964
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
if (!response.ok) {
|
|
968
|
+
throw new Error(`OpenAI models request failed: ${response.status} ${response.statusText}`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const payload = await response.json();
|
|
972
|
+
return (payload.data || [])
|
|
973
|
+
.map((model) => ({ id: model.id, provider: "openai", note: model.owned_by || "" }))
|
|
974
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (provider === "openrouter") {
|
|
978
|
+
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
979
|
+
headers: { accept: "application/json" },
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (!response.ok) {
|
|
983
|
+
throw new Error(`OpenRouter models request failed: ${response.status} ${response.statusText}`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const payload = await response.json();
|
|
987
|
+
return (payload.data || [])
|
|
988
|
+
.map((model) => ({
|
|
989
|
+
id: model.id,
|
|
990
|
+
provider: "openrouter",
|
|
991
|
+
note: model.name || "",
|
|
992
|
+
}))
|
|
993
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const version = await getCommandVersion("codex", ["--version"]);
|
|
997
|
+
return [
|
|
998
|
+
{ id: "gpt-5.5", provider: "codex", note: version },
|
|
999
|
+
{ id: "gpt-5", provider: "codex", note: version },
|
|
1000
|
+
{ id: "gpt-5-codex", provider: "codex", note: version },
|
|
1001
|
+
{ id: "gpt-5-mini", provider: "codex", note: version },
|
|
1002
|
+
];
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function printAiProfiles() {
|
|
1006
|
+
const config = await loadConfig();
|
|
1007
|
+
const active = getActiveProfileName(config);
|
|
1008
|
+
const rows = Object.entries(config.ai.profiles || {}).map(([name, profile]) => ({
|
|
1009
|
+
active: name === active ? "*" : "",
|
|
1010
|
+
name,
|
|
1011
|
+
provider: profile.provider,
|
|
1012
|
+
model: profile.model || "-",
|
|
1013
|
+
baseUrl: profile.baseUrl || "-",
|
|
1014
|
+
mode: profile.provider === "codex" ? `sandbox=${profile.sandbox || "read-only"}, approval=${profile.approval || "never"}` : "-",
|
|
1015
|
+
}));
|
|
1016
|
+
|
|
1017
|
+
printTable(rows, [
|
|
1018
|
+
["active", ""],
|
|
1019
|
+
["name", "Профиль"],
|
|
1020
|
+
["provider", "Провайдер"],
|
|
1021
|
+
["model", "Модель"],
|
|
1022
|
+
["baseUrl", "Base URL"],
|
|
1023
|
+
["mode", "Режим"],
|
|
1024
|
+
]);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function showAiProfile(name) {
|
|
1028
|
+
const config = await loadConfig();
|
|
1029
|
+
const profileName = name || getActiveProfileName(config);
|
|
1030
|
+
const profile = config.ai.profiles?.[profileName];
|
|
1031
|
+
|
|
1032
|
+
if (!profile) {
|
|
1033
|
+
throw new Error(`AI-профиль не найден: ${profileName}`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
printJson({ name: profileName, active: profileName === getActiveProfileName(config), ...profile });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function addAiProfile(name, args) {
|
|
1040
|
+
if (!name) {
|
|
1041
|
+
throw new Error("Имя профиля обязательно. Пример: iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const options = parseOptions(args);
|
|
1045
|
+
const provider = options.provider;
|
|
1046
|
+
|
|
1047
|
+
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
1048
|
+
throw new Error("Провайдер должен быть ollama, openai, openrouter или codex.");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const profile = buildProfileFromOptions(provider, options);
|
|
1052
|
+
const config = await loadConfig();
|
|
1053
|
+
await saveConfig({
|
|
1054
|
+
ai: {
|
|
1055
|
+
...config.ai,
|
|
1056
|
+
profiles: {
|
|
1057
|
+
...(config.ai.profiles || {}),
|
|
1058
|
+
[name]: profile,
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
console.log(`AI-профиль сохранен: ${name}`);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function useAiProfile(name) {
|
|
1067
|
+
if (!name) {
|
|
1068
|
+
throw new Error("Имя профиля обязательно. Пример: iola ai profile use local");
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const config = await loadConfig();
|
|
1072
|
+
const profile = config.ai.profiles?.[name];
|
|
1073
|
+
|
|
1074
|
+
if (!profile) {
|
|
1075
|
+
throw new Error(`AI-профиль не найден: ${name}`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
await saveConfig({
|
|
1079
|
+
ai: {
|
|
1080
|
+
...config.ai,
|
|
1081
|
+
activeProfile: name,
|
|
1082
|
+
provider: profile.provider,
|
|
1083
|
+
model: profile.model,
|
|
1084
|
+
baseUrl: profile.baseUrl || config.ai.baseUrl,
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
console.log(`Активный AI-профиль: ${name} (${profile.provider}, ${profile.model || "-"})`);
|
|
1089
|
+
}
|
|
692
1090
|
|
|
693
|
-
|
|
694
|
-
|
|
1091
|
+
async function deleteAiProfile(name) {
|
|
1092
|
+
if (!name) {
|
|
1093
|
+
throw new Error("Имя профиля обязательно.");
|
|
695
1094
|
}
|
|
696
1095
|
|
|
697
1096
|
const config = await loadConfig();
|
|
1097
|
+
const profiles = { ...(config.ai.profiles || {}) };
|
|
1098
|
+
|
|
1099
|
+
if (!profiles[name]) {
|
|
1100
|
+
throw new Error(`AI-профиль не найден: ${name}`);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
delete profiles[name];
|
|
1104
|
+
const nextActive = config.ai.activeProfile === name ? Object.keys(profiles)[0] : config.ai.activeProfile;
|
|
1105
|
+
const activeProfile = profiles[nextActive] || DEFAULT_AI_CONFIG.ai.profiles.local;
|
|
1106
|
+
|
|
1107
|
+
await saveConfig({
|
|
1108
|
+
ai: {
|
|
1109
|
+
...config.ai,
|
|
1110
|
+
profiles,
|
|
1111
|
+
activeProfile: nextActive || "local",
|
|
1112
|
+
provider: activeProfile.provider,
|
|
1113
|
+
model: activeProfile.model,
|
|
1114
|
+
baseUrl: activeProfile.baseUrl || config.ai.baseUrl,
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
console.log(`AI-профиль удален: ${name}`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function buildProfileFromOptions(provider, options) {
|
|
1122
|
+
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" ? "local" : provider];
|
|
1123
|
+
const profile = {
|
|
1124
|
+
...defaults,
|
|
1125
|
+
provider,
|
|
1126
|
+
model: options.model || defaults.model,
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
if (options["base-url"]) {
|
|
1130
|
+
profile.baseUrl = options["base-url"];
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (provider === "codex") {
|
|
1134
|
+
profile.sandbox = options.sandbox || defaults.sandbox || "read-only";
|
|
1135
|
+
profile.approval = options.approval || defaults.approval || "never";
|
|
1136
|
+
profile.cwd = options.cwd || defaults.cwd || ".";
|
|
1137
|
+
if (options["codex-profile"]) {
|
|
1138
|
+
profile.codexProfile = options["codex-profile"];
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return profile;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async function useAiProvider(args) {
|
|
1146
|
+
const [providerOrProfile] = args;
|
|
1147
|
+
const config = await loadConfig();
|
|
1148
|
+
|
|
1149
|
+
if (config.ai.profiles?.[providerOrProfile]) {
|
|
1150
|
+
await useAiProfile(providerOrProfile);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const provider = providerOrProfile;
|
|
1155
|
+
|
|
1156
|
+
if (provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
1157
|
+
throw new Error("Провайдер должен быть ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
698
1160
|
const defaultModel = {
|
|
699
1161
|
ollama: config.ai.provider === "ollama" ? config.ai.model : "llama3.2:1b",
|
|
700
1162
|
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
701
1163
|
openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
|
|
1164
|
+
codex: config.ai.provider === "codex" ? config.ai.model : "gpt-5.5",
|
|
702
1165
|
}[provider];
|
|
1166
|
+
const profileName = provider === "ollama" ? "local" : provider;
|
|
1167
|
+
const profile = buildProfileFromOptions(provider, { model: defaultModel });
|
|
703
1168
|
|
|
704
1169
|
await saveConfig({
|
|
705
1170
|
ai: {
|
|
1171
|
+
...config.ai,
|
|
1172
|
+
activeProfile: profileName,
|
|
706
1173
|
provider,
|
|
707
1174
|
model: defaultModel,
|
|
708
|
-
baseUrl:
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1175
|
+
baseUrl: profile.baseUrl,
|
|
1176
|
+
profiles: {
|
|
1177
|
+
...(config.ai.profiles || {}),
|
|
1178
|
+
[profileName]: profile,
|
|
1179
|
+
},
|
|
713
1180
|
},
|
|
714
1181
|
});
|
|
715
1182
|
|
|
716
|
-
console.log(`AI-провайдер переключен: ${provider}, модель: ${defaultModel}`);
|
|
1183
|
+
console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
|
|
717
1184
|
}
|
|
718
1185
|
|
|
719
1186
|
async function aiContext(args) {
|
|
@@ -838,11 +1305,23 @@ async function setupOllama(args) {
|
|
|
838
1305
|
await runCommand("ollama", ["pull", model], { inherit: true });
|
|
839
1306
|
}
|
|
840
1307
|
|
|
1308
|
+
const config = await loadConfig();
|
|
1309
|
+
const profileName = options.name || "local";
|
|
841
1310
|
await saveConfig({
|
|
842
1311
|
ai: {
|
|
1312
|
+
...config.ai,
|
|
1313
|
+
activeProfile: profileName,
|
|
843
1314
|
provider: "ollama",
|
|
844
1315
|
model,
|
|
845
1316
|
baseUrl: "http://127.0.0.1:11434",
|
|
1317
|
+
profiles: {
|
|
1318
|
+
...(config.ai.profiles || {}),
|
|
1319
|
+
[profileName]: {
|
|
1320
|
+
provider: "ollama",
|
|
1321
|
+
model,
|
|
1322
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
846
1325
|
},
|
|
847
1326
|
});
|
|
848
1327
|
|
|
@@ -859,13 +1338,7 @@ async function aiAsk(args, context = {}) {
|
|
|
859
1338
|
}
|
|
860
1339
|
|
|
861
1340
|
const config = await loadConfig();
|
|
862
|
-
const
|
|
863
|
-
const model = options.model || config.ai.model;
|
|
864
|
-
const providerConfig = {
|
|
865
|
-
...config.ai,
|
|
866
|
-
provider,
|
|
867
|
-
model,
|
|
868
|
-
};
|
|
1341
|
+
const providerConfig = resolveAiProfile(config, options);
|
|
869
1342
|
const dataContext = await buildDataContext(question);
|
|
870
1343
|
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
871
1344
|
const answer = await callAiProvider(providerConfig, messages);
|
|
@@ -874,6 +1347,26 @@ async function aiAsk(args, context = {}) {
|
|
|
874
1347
|
return answer;
|
|
875
1348
|
}
|
|
876
1349
|
|
|
1350
|
+
function resolveAiProfile(config, options = {}) {
|
|
1351
|
+
const profileName = options.profile || (options.provider && config.ai.profiles?.[options.provider]
|
|
1352
|
+
? options.provider
|
|
1353
|
+
: getActiveProfileName(config));
|
|
1354
|
+
const activeProfile = config.ai.profiles?.[profileName] || {
|
|
1355
|
+
provider: config.ai.provider,
|
|
1356
|
+
model: config.ai.model,
|
|
1357
|
+
baseUrl: config.ai.baseUrl,
|
|
1358
|
+
};
|
|
1359
|
+
const provider = options.provider && !config.ai.profiles?.[options.provider] ? options.provider : activeProfile.provider;
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1362
|
+
name: profileName,
|
|
1363
|
+
...activeProfile,
|
|
1364
|
+
provider,
|
|
1365
|
+
model: options.model || activeProfile.model || config.ai.model,
|
|
1366
|
+
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
|
|
877
1370
|
async function buildDataContext(question) {
|
|
878
1371
|
const apiBaseUrl = await getApiBaseUrl();
|
|
879
1372
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
@@ -1048,9 +1541,49 @@ async function callAiProvider(config, messages) {
|
|
|
1048
1541
|
return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
|
|
1049
1542
|
}
|
|
1050
1543
|
|
|
1544
|
+
if (config.provider === "codex") {
|
|
1545
|
+
return callCodex(config, messages);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1051
1548
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
1052
1549
|
}
|
|
1053
1550
|
|
|
1551
|
+
async function callCodex(config, messages) {
|
|
1552
|
+
const prompt = messages.map((message) => `${message.role.toUpperCase()}:\n${message.content}`).join("\n\n");
|
|
1553
|
+
const outputFile = path.join(os.tmpdir(), `iola-codex-${process.pid}-${Date.now()}.txt`);
|
|
1554
|
+
const args = [
|
|
1555
|
+
"exec",
|
|
1556
|
+
"--skip-git-repo-check",
|
|
1557
|
+
"--output-last-message",
|
|
1558
|
+
outputFile,
|
|
1559
|
+
"--cd",
|
|
1560
|
+
path.resolve(process.cwd(), config.cwd || "."),
|
|
1561
|
+
"--model",
|
|
1562
|
+
config.model || "gpt-5.5",
|
|
1563
|
+
"--sandbox",
|
|
1564
|
+
config.sandbox || "read-only",
|
|
1565
|
+
];
|
|
1566
|
+
|
|
1567
|
+
if (config.codexProfile) {
|
|
1568
|
+
args.push("--profile", config.codexProfile);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
args.push("-");
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
const { stdout, stderr } = await runCommand("codex", args, { input: prompt });
|
|
1575
|
+
const answer = (await readFile(outputFile, "utf8")).trim();
|
|
1576
|
+
if (answer) {
|
|
1577
|
+
return answer;
|
|
1578
|
+
}
|
|
1579
|
+
return stdout.trim() || stderr.trim();
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
throw new Error(`Codex CLI недоступен или не авторизован. Проверьте "codex doctor" и "codex login".\n${error.message}`);
|
|
1582
|
+
} finally {
|
|
1583
|
+
await rm(outputFile, { force: true });
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1054
1587
|
async function callOllama(config, messages) {
|
|
1055
1588
|
let response;
|
|
1056
1589
|
|
|
@@ -1294,9 +1827,10 @@ function parseOptions(args) {
|
|
|
1294
1827
|
const arg = args[index];
|
|
1295
1828
|
if (arg === "--json" || arg === "--yes") {
|
|
1296
1829
|
result[arg.slice(2)] = true;
|
|
1297
|
-
} else if (arg === "--check") {
|
|
1830
|
+
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
1298
1831
|
result.check = true;
|
|
1299
|
-
|
|
1832
|
+
result[arg.slice(2)] = true;
|
|
1833
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format") {
|
|
1300
1834
|
result[arg.slice(2)] = args[index + 1];
|
|
1301
1835
|
index += 1;
|
|
1302
1836
|
} else {
|
|
@@ -1617,6 +2151,9 @@ async function confirm(question) {
|
|
|
1617
2151
|
async function saveConfig(value) {
|
|
1618
2152
|
const current = await loadConfig();
|
|
1619
2153
|
const merged = mergeConfig(current, value);
|
|
2154
|
+
if (value.ai?.profiles) {
|
|
2155
|
+
merged.ai.profiles = value.ai.profiles;
|
|
2156
|
+
}
|
|
1620
2157
|
await writeConfig(merged);
|
|
1621
2158
|
}
|
|
1622
2159
|
|
|
@@ -1645,10 +2182,27 @@ function mergeConfig(base, override) {
|
|
|
1645
2182
|
ai: {
|
|
1646
2183
|
...base.ai,
|
|
1647
2184
|
...(override.ai || {}),
|
|
2185
|
+
profiles: {
|
|
2186
|
+
...(base.ai.profiles || {}),
|
|
2187
|
+
...(override.ai?.profiles || {}),
|
|
2188
|
+
},
|
|
1648
2189
|
},
|
|
1649
2190
|
};
|
|
1650
2191
|
}
|
|
1651
2192
|
|
|
2193
|
+
function getActiveProfileName(config) {
|
|
2194
|
+
if (config.ai.activeProfile && config.ai.profiles?.[config.ai.activeProfile]) {
|
|
2195
|
+
return config.ai.activeProfile;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
const provider = config.ai.provider === "ollama" ? "local" : config.ai.provider;
|
|
2199
|
+
if (provider && config.ai.profiles?.[provider]) {
|
|
2200
|
+
return provider;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
return Object.keys(config.ai.profiles || {})[0] || "local";
|
|
2204
|
+
}
|
|
2205
|
+
|
|
1652
2206
|
async function getApiBaseUrl() {
|
|
1653
2207
|
if (process.env.IOLA_API_BASE_URL) {
|
|
1654
2208
|
return process.env.IOLA_API_BASE_URL;
|
|
@@ -1755,6 +2309,14 @@ function runCommand(command, args, options = {}) {
|
|
|
1755
2309
|
maxBuffer: 1024 * 1024 * 5,
|
|
1756
2310
|
}, (error, stdout, stderr) => {
|
|
1757
2311
|
if (error) {
|
|
2312
|
+
if (process.platform === "win32" && (error.code === "ENOENT" || error.code === "EINVAL") && !options.cmdFallback) {
|
|
2313
|
+
runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", quoteWindowsCommand(command, args)], {
|
|
2314
|
+
...options,
|
|
2315
|
+
cmdFallback: true,
|
|
2316
|
+
}).then(resolve, reject);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
1758
2320
|
reject(error);
|
|
1759
2321
|
return;
|
|
1760
2322
|
}
|
|
@@ -1766,9 +2328,23 @@ function runCommand(command, args, options = {}) {
|
|
|
1766
2328
|
child.stdout?.pipe(process.stdout);
|
|
1767
2329
|
child.stderr?.pipe(process.stderr);
|
|
1768
2330
|
}
|
|
2331
|
+
|
|
2332
|
+
if (options.input) {
|
|
2333
|
+
child.stdin?.end(options.input);
|
|
2334
|
+
}
|
|
1769
2335
|
});
|
|
1770
2336
|
}
|
|
1771
2337
|
|
|
2338
|
+
function quoteWindowsCommand(command, args) {
|
|
2339
|
+
return [command, ...args].map((value) => {
|
|
2340
|
+
const text = String(value);
|
|
2341
|
+
if (/^[A-Za-z0-9_./:=\\-]+$/.test(text)) {
|
|
2342
|
+
return text;
|
|
2343
|
+
}
|
|
2344
|
+
return `"${text.replace(/"/g, "\\\"")}"`;
|
|
2345
|
+
}).join(" ");
|
|
2346
|
+
}
|
|
2347
|
+
|
|
1772
2348
|
function roundGb(bytes) {
|
|
1773
2349
|
return Math.round((bytes / 1024 / 1024 / 1024) * 10) / 10;
|
|
1774
2350
|
}
|