@iola_adm/iola-cli 0.1.10 → 0.1.11
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 +43 -0
- package/package.json +1 -1
- package/src/cli.js +516 -24
package/README.md
CHANGED
|
@@ -93,6 +93,12 @@ iola data schools --where address=Петрова --columns name,address,phone
|
|
|
93
93
|
iola data schools --format csv
|
|
94
94
|
iola ai doctor
|
|
95
95
|
iola ai setup ollama
|
|
96
|
+
iola ai setup codex --model gpt-5.5
|
|
97
|
+
iola ai profiles
|
|
98
|
+
iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b
|
|
99
|
+
iola ai profile use router-qwen
|
|
100
|
+
iola ai models openrouter --search qwen
|
|
101
|
+
iola ai models codex
|
|
96
102
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
97
103
|
iola ai context "школа 29"
|
|
98
104
|
iola ai key set openai
|
|
@@ -137,6 +143,9 @@ iola agent
|
|
|
137
143
|
/mcp-info
|
|
138
144
|
/ai doctor
|
|
139
145
|
/context школа 29
|
|
146
|
+
/profiles
|
|
147
|
+
/profile use local
|
|
148
|
+
/models openrouter --search qwen
|
|
140
149
|
/use ollama
|
|
141
150
|
/use openai
|
|
142
151
|
/key status
|
|
@@ -180,6 +189,40 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
180
189
|
iola ai ask "Покажи контакты лицея"
|
|
181
190
|
```
|
|
182
191
|
|
|
192
|
+
Codex CLI:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
codex login
|
|
196
|
+
iola ai setup codex --model gpt-5.5
|
|
197
|
+
iola setup codex
|
|
198
|
+
iola ask "Назови ИНН школы 29"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
AI-профили позволяют держать локальную модель, OpenAI, OpenRouter и Codex
|
|
202
|
+
одновременно:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
iola ai profiles
|
|
206
|
+
iola ai profile add local-small --provider ollama --model llama3.2:1b
|
|
207
|
+
iola ai profile add gpt --provider openai --model gpt-4.1-mini
|
|
208
|
+
iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b
|
|
209
|
+
iola ai profile add codex-read --provider codex --model gpt-5.5 --sandbox read-only
|
|
210
|
+
iola ai profile use router-qwen
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Списки моделей:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
iola ai models ollama
|
|
217
|
+
iola ai models openai
|
|
218
|
+
iola ai models openrouter --search qwen
|
|
219
|
+
iola ai models codex
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Для OpenAI список моделей требует сохраненный ключ. OpenRouter берется из
|
|
223
|
+
публичного API OpenRouter. Ollama читает локальные модели через `api/tags`, а
|
|
224
|
+
если Ollama не запущен, показывает рекомендуемые локальные модели.
|
|
225
|
+
|
|
183
226
|
Проверить, какие данные попадут в AI-контекст:
|
|
184
227
|
|
|
185
228
|
```bash
|
package/package.json
CHANGED
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";
|
|
@@ -16,9 +16,34 @@ const DEFAULT_AI_CONFIG = {
|
|
|
16
16
|
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
17
17
|
},
|
|
18
18
|
ai: {
|
|
19
|
+
activeProfile: "local",
|
|
19
20
|
provider: "ollama",
|
|
20
21
|
model: "llama3.2:1b",
|
|
21
22
|
baseUrl: "http://127.0.0.1:11434",
|
|
23
|
+
profiles: {
|
|
24
|
+
local: {
|
|
25
|
+
provider: "ollama",
|
|
26
|
+
model: "llama3.2:1b",
|
|
27
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
28
|
+
},
|
|
29
|
+
openai: {
|
|
30
|
+
provider: "openai",
|
|
31
|
+
model: "gpt-4.1-mini",
|
|
32
|
+
baseUrl: "https://api.openai.com/v1",
|
|
33
|
+
},
|
|
34
|
+
openrouter: {
|
|
35
|
+
provider: "openrouter",
|
|
36
|
+
model: "openai/gpt-4.1-mini",
|
|
37
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
38
|
+
},
|
|
39
|
+
codex: {
|
|
40
|
+
provider: "codex",
|
|
41
|
+
model: "gpt-5.5",
|
|
42
|
+
sandbox: "read-only",
|
|
43
|
+
approval: "never",
|
|
44
|
+
cwd: ".",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
22
47
|
},
|
|
23
48
|
};
|
|
24
49
|
const DATASETS = {
|
|
@@ -101,6 +126,11 @@ Usage:
|
|
|
101
126
|
iola ai key set openrouter
|
|
102
127
|
iola ai key status
|
|
103
128
|
iola ai key delete openai|openrouter
|
|
129
|
+
iola ai profiles
|
|
130
|
+
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
131
|
+
iola ai profile use NAME
|
|
132
|
+
iola ai profile delete NAME
|
|
133
|
+
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
104
134
|
iola ai doctor [--json]
|
|
105
135
|
iola ai setup
|
|
106
136
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -210,6 +240,21 @@ async function handleAgentLine(line, state) {
|
|
|
210
240
|
return false;
|
|
211
241
|
}
|
|
212
242
|
|
|
243
|
+
if (command === "profiles") {
|
|
244
|
+
await handleAiProfile(["list", ...args]);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (command === "profile") {
|
|
249
|
+
await handleAiProfile(args);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (command === "models") {
|
|
254
|
+
await aiModels(args);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
213
258
|
if (command === "use") {
|
|
214
259
|
await useAiProvider(args);
|
|
215
260
|
return false;
|
|
@@ -290,6 +335,9 @@ function printAgentHelp() {
|
|
|
290
335
|
/search лицей --limit 3
|
|
291
336
|
/mcp-info
|
|
292
337
|
/context школа 29
|
|
338
|
+
/profiles
|
|
339
|
+
/profile use local
|
|
340
|
+
/models openrouter --search qwen
|
|
293
341
|
/ai doctor
|
|
294
342
|
/ai setup ollama
|
|
295
343
|
/use openai
|
|
@@ -402,6 +450,7 @@ async function doctor(args = []) {
|
|
|
402
450
|
const options = parseOptions(args);
|
|
403
451
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
404
452
|
const config = await loadConfig();
|
|
453
|
+
const activeAiProfile = resolveAiProfile(config);
|
|
405
454
|
const secrets = await loadSecrets();
|
|
406
455
|
const diagnostics = await getLocalDiagnostics();
|
|
407
456
|
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
@@ -419,9 +468,10 @@ async function doctor(args = []) {
|
|
|
419
468
|
health: await probeEndpoint(`${mcpBaseUrl}/mcp-health`),
|
|
420
469
|
},
|
|
421
470
|
ai: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
471
|
+
activeProfile: getActiveProfileName(config),
|
|
472
|
+
provider: activeAiProfile.provider,
|
|
473
|
+
model: activeAiProfile.model,
|
|
474
|
+
modelAvailable: await checkConfiguredModel({ ai: activeAiProfile }),
|
|
425
475
|
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
426
476
|
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
427
477
|
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
@@ -531,6 +581,11 @@ async function handleAi(args) {
|
|
|
531
581
|
iola ai key set openrouter
|
|
532
582
|
iola ai key status
|
|
533
583
|
iola ai key delete openai|openrouter
|
|
584
|
+
iola ai profiles
|
|
585
|
+
iola ai profile add NAME --provider ollama|openai|openrouter|codex --model MODEL
|
|
586
|
+
iola ai profile use NAME
|
|
587
|
+
iola ai profile delete NAME
|
|
588
|
+
iola ai models ollama|openai|openrouter|codex [--search TEXT]
|
|
534
589
|
iola ai doctor [--json]
|
|
535
590
|
iola ai setup
|
|
536
591
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -556,6 +611,21 @@ async function handleAi(args) {
|
|
|
556
611
|
return;
|
|
557
612
|
}
|
|
558
613
|
|
|
614
|
+
if (subcommand === "profiles") {
|
|
615
|
+
await handleAiProfile(["list", ...rest]);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (subcommand === "profile") {
|
|
620
|
+
await handleAiProfile(rest);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (subcommand === "models") {
|
|
625
|
+
await aiModels(rest);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
559
629
|
if (subcommand === "doctor") {
|
|
560
630
|
await aiDoctor(rest);
|
|
561
631
|
return;
|
|
@@ -641,21 +711,50 @@ async function aiSetup(args) {
|
|
|
641
711
|
if (provider === "openai" || provider === "openrouter") {
|
|
642
712
|
const options = parseOptions(args.slice(1));
|
|
643
713
|
const model = options.model || (provider === "openai" ? "gpt-4.1-mini" : "openai/gpt-4.1-mini");
|
|
714
|
+
const profileName = options.name || provider;
|
|
715
|
+
const profile = buildProfileFromOptions(provider, { ...options, model });
|
|
716
|
+
const config = await loadConfig();
|
|
644
717
|
await saveConfig({
|
|
645
718
|
ai: {
|
|
719
|
+
...config.ai,
|
|
720
|
+
activeProfile: profileName,
|
|
646
721
|
provider,
|
|
647
722
|
model,
|
|
648
|
-
baseUrl:
|
|
723
|
+
baseUrl: profile.baseUrl,
|
|
724
|
+
profiles: {
|
|
725
|
+
...(config.ai.profiles || {}),
|
|
726
|
+
[profileName]: profile,
|
|
727
|
+
},
|
|
649
728
|
},
|
|
650
729
|
});
|
|
651
|
-
console.log(`AI-профиль ${
|
|
730
|
+
console.log(`AI-профиль ${profileName} сохранен и выбран в ${CONFIG_FILE}`);
|
|
652
731
|
console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
|
|
653
732
|
console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
654
733
|
return;
|
|
655
734
|
}
|
|
656
735
|
|
|
657
736
|
if (provider === "codex") {
|
|
658
|
-
|
|
737
|
+
const options = parseOptions(args.slice(1));
|
|
738
|
+
const profileName = options.name || "codex";
|
|
739
|
+
const profile = buildProfileFromOptions("codex", options);
|
|
740
|
+
const config = await loadConfig();
|
|
741
|
+
await saveConfig({
|
|
742
|
+
ai: {
|
|
743
|
+
...config.ai,
|
|
744
|
+
activeProfile: profileName,
|
|
745
|
+
provider: "codex",
|
|
746
|
+
model: profile.model,
|
|
747
|
+
profiles: {
|
|
748
|
+
...(config.ai.profiles || {}),
|
|
749
|
+
[profileName]: profile,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
console.log(`AI-профиль ${profileName} сохранен и выбран.`);
|
|
754
|
+
console.log("Проверка Codex CLI:");
|
|
755
|
+
console.log(` ${await getCommandVersion("codex", ["--version"])}`);
|
|
756
|
+
console.log("MCP подключается отдельно командой:");
|
|
757
|
+
console.log(" iola setup codex");
|
|
659
758
|
return;
|
|
660
759
|
}
|
|
661
760
|
|
|
@@ -687,33 +786,318 @@ async function handleAiKey(args) {
|
|
|
687
786
|
iola ai key delete openai|openrouter`);
|
|
688
787
|
}
|
|
689
788
|
|
|
690
|
-
async function
|
|
789
|
+
async function handleAiProfile(args) {
|
|
790
|
+
const [action = "list", name, ...rest] = args;
|
|
791
|
+
|
|
792
|
+
if (action === "list" || action === "ls") {
|
|
793
|
+
await printAiProfiles();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (action === "show") {
|
|
798
|
+
await showAiProfile(name);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (action === "use") {
|
|
803
|
+
await useAiProfile(name);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (action === "add" || action === "set") {
|
|
808
|
+
await addAiProfile(name, rest);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (action === "delete" || action === "remove" || action === "rm") {
|
|
813
|
+
await deleteAiProfile(name);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
throw new Error(`Unknown profile command. Use:
|
|
818
|
+
iola ai profiles
|
|
819
|
+
iola ai profile add NAME --provider PROVIDER --model MODEL
|
|
820
|
+
iola ai profile use NAME
|
|
821
|
+
iola ai profile delete NAME`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function aiModels(args) {
|
|
691
825
|
const [provider] = args;
|
|
826
|
+
const options = parseOptions(args.slice(1));
|
|
827
|
+
|
|
828
|
+
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
829
|
+
throw new Error("Провайдер обязателен: iola ai models ollama|openai|openrouter|codex");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const models = await listAiModels(provider);
|
|
833
|
+
const filtered = options.search
|
|
834
|
+
? models.filter((model) => model.id.toLocaleLowerCase("ru-RU").includes(options.search.toLocaleLowerCase("ru-RU")))
|
|
835
|
+
: models;
|
|
836
|
+
|
|
837
|
+
if (options.json) {
|
|
838
|
+
printJson(filtered);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
printTable(filtered, [
|
|
843
|
+
["id", "Модель"],
|
|
844
|
+
["provider", "Провайдер"],
|
|
845
|
+
["note", "Примечание"],
|
|
846
|
+
]);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function listAiModels(provider) {
|
|
850
|
+
if (provider === "ollama") {
|
|
851
|
+
try {
|
|
852
|
+
const config = await loadConfig();
|
|
853
|
+
const response = await fetch(`${config.ai.profiles?.local?.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
|
|
854
|
+
|
|
855
|
+
if (!response.ok) {
|
|
856
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const payload = await response.json();
|
|
860
|
+
return (payload.models || []).map((model) => ({
|
|
861
|
+
id: model.name,
|
|
862
|
+
provider: "ollama",
|
|
863
|
+
note: model.modified_at ? `updated ${model.modified_at}` : "local",
|
|
864
|
+
}));
|
|
865
|
+
} catch {
|
|
866
|
+
return [
|
|
867
|
+
{ id: "llama3.2:1b", provider: "ollama", note: "recommended low RAM" },
|
|
868
|
+
{ id: "llama3.2:3b", provider: "ollama", note: "recommended standard" },
|
|
869
|
+
{ id: "qwen3:4b", provider: "ollama", note: "recommended balanced" },
|
|
870
|
+
{ id: "qwen3:8b", provider: "ollama", note: "recommended good GPU" },
|
|
871
|
+
];
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (provider === "openai") {
|
|
876
|
+
const apiKey = await getApiKey("openai");
|
|
877
|
+
if (!apiKey) {
|
|
878
|
+
throw new Error("OpenAI API key не найден. Выполните iola ai key set openai.");
|
|
879
|
+
}
|
|
880
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
881
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
if (!response.ok) {
|
|
885
|
+
throw new Error(`OpenAI models request failed: ${response.status} ${response.statusText}`);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const payload = await response.json();
|
|
889
|
+
return (payload.data || [])
|
|
890
|
+
.map((model) => ({ id: model.id, provider: "openai", note: model.owned_by || "" }))
|
|
891
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (provider === "openrouter") {
|
|
895
|
+
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
896
|
+
headers: { accept: "application/json" },
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
if (!response.ok) {
|
|
900
|
+
throw new Error(`OpenRouter models request failed: ${response.status} ${response.statusText}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const payload = await response.json();
|
|
904
|
+
return (payload.data || [])
|
|
905
|
+
.map((model) => ({
|
|
906
|
+
id: model.id,
|
|
907
|
+
provider: "openrouter",
|
|
908
|
+
note: model.name || "",
|
|
909
|
+
}))
|
|
910
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const version = await getCommandVersion("codex", ["--version"]);
|
|
914
|
+
return [
|
|
915
|
+
{ id: "gpt-5.5", provider: "codex", note: version },
|
|
916
|
+
{ id: "gpt-5", provider: "codex", note: version },
|
|
917
|
+
{ id: "gpt-5-codex", provider: "codex", note: version },
|
|
918
|
+
{ id: "gpt-5-mini", provider: "codex", note: version },
|
|
919
|
+
];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async function printAiProfiles() {
|
|
923
|
+
const config = await loadConfig();
|
|
924
|
+
const active = getActiveProfileName(config);
|
|
925
|
+
const rows = Object.entries(config.ai.profiles || {}).map(([name, profile]) => ({
|
|
926
|
+
active: name === active ? "*" : "",
|
|
927
|
+
name,
|
|
928
|
+
provider: profile.provider,
|
|
929
|
+
model: profile.model || "-",
|
|
930
|
+
baseUrl: profile.baseUrl || "-",
|
|
931
|
+
mode: profile.provider === "codex" ? `sandbox=${profile.sandbox || "read-only"}, approval=${profile.approval || "never"}` : "-",
|
|
932
|
+
}));
|
|
933
|
+
|
|
934
|
+
printTable(rows, [
|
|
935
|
+
["active", ""],
|
|
936
|
+
["name", "Профиль"],
|
|
937
|
+
["provider", "Провайдер"],
|
|
938
|
+
["model", "Модель"],
|
|
939
|
+
["baseUrl", "Base URL"],
|
|
940
|
+
["mode", "Режим"],
|
|
941
|
+
]);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function showAiProfile(name) {
|
|
945
|
+
const config = await loadConfig();
|
|
946
|
+
const profileName = name || getActiveProfileName(config);
|
|
947
|
+
const profile = config.ai.profiles?.[profileName];
|
|
948
|
+
|
|
949
|
+
if (!profile) {
|
|
950
|
+
throw new Error(`AI-профиль не найден: ${profileName}`);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
printJson({ name: profileName, active: profileName === getActiveProfileName(config), ...profile });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function addAiProfile(name, args) {
|
|
957
|
+
if (!name) {
|
|
958
|
+
throw new Error("Имя профиля обязательно. Пример: iola ai profile add router-qwen --provider openrouter --model qwen/qwen3-32b");
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const options = parseOptions(args);
|
|
962
|
+
const provider = options.provider;
|
|
963
|
+
|
|
964
|
+
if (!["ollama", "openai", "openrouter", "codex"].includes(provider)) {
|
|
965
|
+
throw new Error("Провайдер должен быть ollama, openai, openrouter или codex.");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const profile = buildProfileFromOptions(provider, options);
|
|
969
|
+
const config = await loadConfig();
|
|
970
|
+
await saveConfig({
|
|
971
|
+
ai: {
|
|
972
|
+
...config.ai,
|
|
973
|
+
profiles: {
|
|
974
|
+
...(config.ai.profiles || {}),
|
|
975
|
+
[name]: profile,
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
});
|
|
692
979
|
|
|
693
|
-
|
|
694
|
-
|
|
980
|
+
console.log(`AI-профиль сохранен: ${name}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function useAiProfile(name) {
|
|
984
|
+
if (!name) {
|
|
985
|
+
throw new Error("Имя профиля обязательно. Пример: iola ai profile use local");
|
|
695
986
|
}
|
|
696
987
|
|
|
697
988
|
const config = await loadConfig();
|
|
989
|
+
const profile = config.ai.profiles?.[name];
|
|
990
|
+
|
|
991
|
+
if (!profile) {
|
|
992
|
+
throw new Error(`AI-профиль не найден: ${name}`);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
await saveConfig({
|
|
996
|
+
ai: {
|
|
997
|
+
...config.ai,
|
|
998
|
+
activeProfile: name,
|
|
999
|
+
provider: profile.provider,
|
|
1000
|
+
model: profile.model,
|
|
1001
|
+
baseUrl: profile.baseUrl || config.ai.baseUrl,
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
console.log(`Активный AI-профиль: ${name} (${profile.provider}, ${profile.model || "-"})`);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function deleteAiProfile(name) {
|
|
1009
|
+
if (!name) {
|
|
1010
|
+
throw new Error("Имя профиля обязательно.");
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const config = await loadConfig();
|
|
1014
|
+
const profiles = { ...(config.ai.profiles || {}) };
|
|
1015
|
+
|
|
1016
|
+
if (!profiles[name]) {
|
|
1017
|
+
throw new Error(`AI-профиль не найден: ${name}`);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
delete profiles[name];
|
|
1021
|
+
const nextActive = config.ai.activeProfile === name ? Object.keys(profiles)[0] : config.ai.activeProfile;
|
|
1022
|
+
const activeProfile = profiles[nextActive] || DEFAULT_AI_CONFIG.ai.profiles.local;
|
|
1023
|
+
|
|
1024
|
+
await saveConfig({
|
|
1025
|
+
ai: {
|
|
1026
|
+
...config.ai,
|
|
1027
|
+
profiles,
|
|
1028
|
+
activeProfile: nextActive || "local",
|
|
1029
|
+
provider: activeProfile.provider,
|
|
1030
|
+
model: activeProfile.model,
|
|
1031
|
+
baseUrl: activeProfile.baseUrl || config.ai.baseUrl,
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
console.log(`AI-профиль удален: ${name}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function buildProfileFromOptions(provider, options) {
|
|
1039
|
+
const defaults = DEFAULT_AI_CONFIG.ai.profiles[provider === "ollama" ? "local" : provider];
|
|
1040
|
+
const profile = {
|
|
1041
|
+
...defaults,
|
|
1042
|
+
provider,
|
|
1043
|
+
model: options.model || defaults.model,
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
if (options["base-url"]) {
|
|
1047
|
+
profile.baseUrl = options["base-url"];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (provider === "codex") {
|
|
1051
|
+
profile.sandbox = options.sandbox || defaults.sandbox || "read-only";
|
|
1052
|
+
profile.approval = options.approval || defaults.approval || "never";
|
|
1053
|
+
profile.cwd = options.cwd || defaults.cwd || ".";
|
|
1054
|
+
if (options["codex-profile"]) {
|
|
1055
|
+
profile.codexProfile = options["codex-profile"];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return profile;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function useAiProvider(args) {
|
|
1063
|
+
const [providerOrProfile] = args;
|
|
1064
|
+
const config = await loadConfig();
|
|
1065
|
+
|
|
1066
|
+
if (config.ai.profiles?.[providerOrProfile]) {
|
|
1067
|
+
await useAiProfile(providerOrProfile);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const provider = providerOrProfile;
|
|
1072
|
+
|
|
1073
|
+
if (provider !== "ollama" && provider !== "openai" && provider !== "openrouter" && provider !== "codex") {
|
|
1074
|
+
throw new Error("Провайдер должен быть ollama, openai, openrouter, codex или именем AI-профиля.");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
698
1077
|
const defaultModel = {
|
|
699
1078
|
ollama: config.ai.provider === "ollama" ? config.ai.model : "llama3.2:1b",
|
|
700
1079
|
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
701
1080
|
openrouter: config.ai.provider === "openrouter" ? config.ai.model : "openai/gpt-4.1-mini",
|
|
1081
|
+
codex: config.ai.provider === "codex" ? config.ai.model : "gpt-5.5",
|
|
702
1082
|
}[provider];
|
|
1083
|
+
const profileName = provider === "ollama" ? "local" : provider;
|
|
1084
|
+
const profile = buildProfileFromOptions(provider, { model: defaultModel });
|
|
703
1085
|
|
|
704
1086
|
await saveConfig({
|
|
705
1087
|
ai: {
|
|
1088
|
+
...config.ai,
|
|
1089
|
+
activeProfile: profileName,
|
|
706
1090
|
provider,
|
|
707
1091
|
model: defaultModel,
|
|
708
|
-
baseUrl:
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1092
|
+
baseUrl: profile.baseUrl,
|
|
1093
|
+
profiles: {
|
|
1094
|
+
...(config.ai.profiles || {}),
|
|
1095
|
+
[profileName]: profile,
|
|
1096
|
+
},
|
|
713
1097
|
},
|
|
714
1098
|
});
|
|
715
1099
|
|
|
716
|
-
console.log(`AI-провайдер переключен: ${provider}, модель: ${defaultModel}`);
|
|
1100
|
+
console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
|
|
717
1101
|
}
|
|
718
1102
|
|
|
719
1103
|
async function aiContext(args) {
|
|
@@ -838,11 +1222,23 @@ async function setupOllama(args) {
|
|
|
838
1222
|
await runCommand("ollama", ["pull", model], { inherit: true });
|
|
839
1223
|
}
|
|
840
1224
|
|
|
1225
|
+
const config = await loadConfig();
|
|
1226
|
+
const profileName = options.name || "local";
|
|
841
1227
|
await saveConfig({
|
|
842
1228
|
ai: {
|
|
1229
|
+
...config.ai,
|
|
1230
|
+
activeProfile: profileName,
|
|
843
1231
|
provider: "ollama",
|
|
844
1232
|
model,
|
|
845
1233
|
baseUrl: "http://127.0.0.1:11434",
|
|
1234
|
+
profiles: {
|
|
1235
|
+
...(config.ai.profiles || {}),
|
|
1236
|
+
[profileName]: {
|
|
1237
|
+
provider: "ollama",
|
|
1238
|
+
model,
|
|
1239
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
846
1242
|
},
|
|
847
1243
|
});
|
|
848
1244
|
|
|
@@ -859,13 +1255,7 @@ async function aiAsk(args, context = {}) {
|
|
|
859
1255
|
}
|
|
860
1256
|
|
|
861
1257
|
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
|
-
};
|
|
1258
|
+
const providerConfig = resolveAiProfile(config, options);
|
|
869
1259
|
const dataContext = await buildDataContext(question);
|
|
870
1260
|
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
871
1261
|
const answer = await callAiProvider(providerConfig, messages);
|
|
@@ -874,6 +1264,26 @@ async function aiAsk(args, context = {}) {
|
|
|
874
1264
|
return answer;
|
|
875
1265
|
}
|
|
876
1266
|
|
|
1267
|
+
function resolveAiProfile(config, options = {}) {
|
|
1268
|
+
const profileName = options.profile || (options.provider && config.ai.profiles?.[options.provider]
|
|
1269
|
+
? options.provider
|
|
1270
|
+
: getActiveProfileName(config));
|
|
1271
|
+
const activeProfile = config.ai.profiles?.[profileName] || {
|
|
1272
|
+
provider: config.ai.provider,
|
|
1273
|
+
model: config.ai.model,
|
|
1274
|
+
baseUrl: config.ai.baseUrl,
|
|
1275
|
+
};
|
|
1276
|
+
const provider = options.provider && !config.ai.profiles?.[options.provider] ? options.provider : activeProfile.provider;
|
|
1277
|
+
|
|
1278
|
+
return {
|
|
1279
|
+
name: profileName,
|
|
1280
|
+
...activeProfile,
|
|
1281
|
+
provider,
|
|
1282
|
+
model: options.model || activeProfile.model || config.ai.model,
|
|
1283
|
+
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
877
1287
|
async function buildDataContext(question) {
|
|
878
1288
|
const apiBaseUrl = await getApiBaseUrl();
|
|
879
1289
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
@@ -1048,9 +1458,49 @@ async function callAiProvider(config, messages) {
|
|
|
1048
1458
|
return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
|
|
1049
1459
|
}
|
|
1050
1460
|
|
|
1461
|
+
if (config.provider === "codex") {
|
|
1462
|
+
return callCodex(config, messages);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1051
1465
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
1052
1466
|
}
|
|
1053
1467
|
|
|
1468
|
+
async function callCodex(config, messages) {
|
|
1469
|
+
const prompt = messages.map((message) => `${message.role.toUpperCase()}:\n${message.content}`).join("\n\n");
|
|
1470
|
+
const outputFile = path.join(os.tmpdir(), `iola-codex-${process.pid}-${Date.now()}.txt`);
|
|
1471
|
+
const args = [
|
|
1472
|
+
"exec",
|
|
1473
|
+
"--skip-git-repo-check",
|
|
1474
|
+
"--output-last-message",
|
|
1475
|
+
outputFile,
|
|
1476
|
+
"--cd",
|
|
1477
|
+
path.resolve(process.cwd(), config.cwd || "."),
|
|
1478
|
+
"--model",
|
|
1479
|
+
config.model || "gpt-5.5",
|
|
1480
|
+
"--sandbox",
|
|
1481
|
+
config.sandbox || "read-only",
|
|
1482
|
+
];
|
|
1483
|
+
|
|
1484
|
+
if (config.codexProfile) {
|
|
1485
|
+
args.push("--profile", config.codexProfile);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
args.push("-");
|
|
1489
|
+
|
|
1490
|
+
try {
|
|
1491
|
+
const { stdout, stderr } = await runCommand("codex", args, { input: prompt });
|
|
1492
|
+
const answer = (await readFile(outputFile, "utf8")).trim();
|
|
1493
|
+
if (answer) {
|
|
1494
|
+
return answer;
|
|
1495
|
+
}
|
|
1496
|
+
return stdout.trim() || stderr.trim();
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
throw new Error(`Codex CLI недоступен или не авторизован. Проверьте "codex doctor" и "codex login".\n${error.message}`);
|
|
1499
|
+
} finally {
|
|
1500
|
+
await rm(outputFile, { force: true });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1054
1504
|
async function callOllama(config, messages) {
|
|
1055
1505
|
let response;
|
|
1056
1506
|
|
|
@@ -1296,7 +1746,7 @@ function parseOptions(args) {
|
|
|
1296
1746
|
result[arg.slice(2)] = true;
|
|
1297
1747
|
} else if (arg === "--check") {
|
|
1298
1748
|
result.check = true;
|
|
1299
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--format") {
|
|
1749
|
+
} 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
1750
|
result[arg.slice(2)] = args[index + 1];
|
|
1301
1751
|
index += 1;
|
|
1302
1752
|
} else {
|
|
@@ -1617,6 +2067,9 @@ async function confirm(question) {
|
|
|
1617
2067
|
async function saveConfig(value) {
|
|
1618
2068
|
const current = await loadConfig();
|
|
1619
2069
|
const merged = mergeConfig(current, value);
|
|
2070
|
+
if (value.ai?.profiles) {
|
|
2071
|
+
merged.ai.profiles = value.ai.profiles;
|
|
2072
|
+
}
|
|
1620
2073
|
await writeConfig(merged);
|
|
1621
2074
|
}
|
|
1622
2075
|
|
|
@@ -1645,10 +2098,27 @@ function mergeConfig(base, override) {
|
|
|
1645
2098
|
ai: {
|
|
1646
2099
|
...base.ai,
|
|
1647
2100
|
...(override.ai || {}),
|
|
2101
|
+
profiles: {
|
|
2102
|
+
...(base.ai.profiles || {}),
|
|
2103
|
+
...(override.ai?.profiles || {}),
|
|
2104
|
+
},
|
|
1648
2105
|
},
|
|
1649
2106
|
};
|
|
1650
2107
|
}
|
|
1651
2108
|
|
|
2109
|
+
function getActiveProfileName(config) {
|
|
2110
|
+
if (config.ai.activeProfile && config.ai.profiles?.[config.ai.activeProfile]) {
|
|
2111
|
+
return config.ai.activeProfile;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const provider = config.ai.provider === "ollama" ? "local" : config.ai.provider;
|
|
2115
|
+
if (provider && config.ai.profiles?.[provider]) {
|
|
2116
|
+
return provider;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
return Object.keys(config.ai.profiles || {})[0] || "local";
|
|
2120
|
+
}
|
|
2121
|
+
|
|
1652
2122
|
async function getApiBaseUrl() {
|
|
1653
2123
|
if (process.env.IOLA_API_BASE_URL) {
|
|
1654
2124
|
return process.env.IOLA_API_BASE_URL;
|
|
@@ -1755,6 +2225,14 @@ function runCommand(command, args, options = {}) {
|
|
|
1755
2225
|
maxBuffer: 1024 * 1024 * 5,
|
|
1756
2226
|
}, (error, stdout, stderr) => {
|
|
1757
2227
|
if (error) {
|
|
2228
|
+
if (process.platform === "win32" && (error.code === "ENOENT" || error.code === "EINVAL") && !options.cmdFallback) {
|
|
2229
|
+
runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", quoteWindowsCommand(command, args)], {
|
|
2230
|
+
...options,
|
|
2231
|
+
cmdFallback: true,
|
|
2232
|
+
}).then(resolve, reject);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
1758
2236
|
reject(error);
|
|
1759
2237
|
return;
|
|
1760
2238
|
}
|
|
@@ -1766,9 +2244,23 @@ function runCommand(command, args, options = {}) {
|
|
|
1766
2244
|
child.stdout?.pipe(process.stdout);
|
|
1767
2245
|
child.stderr?.pipe(process.stderr);
|
|
1768
2246
|
}
|
|
2247
|
+
|
|
2248
|
+
if (options.input) {
|
|
2249
|
+
child.stdin?.end(options.input);
|
|
2250
|
+
}
|
|
1769
2251
|
});
|
|
1770
2252
|
}
|
|
1771
2253
|
|
|
2254
|
+
function quoteWindowsCommand(command, args) {
|
|
2255
|
+
return [command, ...args].map((value) => {
|
|
2256
|
+
const text = String(value);
|
|
2257
|
+
if (/^[A-Za-z0-9_./:=\\-]+$/.test(text)) {
|
|
2258
|
+
return text;
|
|
2259
|
+
}
|
|
2260
|
+
return `"${text.replace(/"/g, "\\\"")}"`;
|
|
2261
|
+
}).join(" ");
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1772
2264
|
function roundGb(bytes) {
|
|
1773
2265
|
return Math.round((bytes / 1024 / 1024 / 1024) * 10) / 10;
|
|
1774
2266
|
}
|