@iola_adm/iola-cli 0.1.9 → 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 +61 -0
- package/package.json +1 -1
- package/src/cli.js +790 -51
package/README.md
CHANGED
|
@@ -80,13 +80,25 @@ iola banner
|
|
|
80
80
|
iola agent
|
|
81
81
|
iola chat
|
|
82
82
|
iola init
|
|
83
|
+
iola doctor
|
|
84
|
+
iola config get
|
|
85
|
+
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
86
|
+
iola config reset
|
|
83
87
|
iola update
|
|
84
88
|
iola version --check
|
|
89
|
+
iola ask "Найди школу 29"
|
|
85
90
|
iola data schools --limit 10
|
|
86
91
|
iola data kindergartens --search "29"
|
|
92
|
+
iola data schools --where address=Петрова --columns name,address,phone
|
|
87
93
|
iola data schools --format csv
|
|
88
94
|
iola ai doctor
|
|
89
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
|
|
90
102
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
91
103
|
iola ai context "школа 29"
|
|
92
104
|
iola ai key set openai
|
|
@@ -120,6 +132,8 @@ iola agent
|
|
|
120
132
|
```text
|
|
121
133
|
/help
|
|
122
134
|
/health
|
|
135
|
+
/doctor
|
|
136
|
+
/config get
|
|
123
137
|
/layers
|
|
124
138
|
/data schools --limit 10
|
|
125
139
|
/schools --limit 10
|
|
@@ -129,6 +143,9 @@ iola agent
|
|
|
129
143
|
/mcp-info
|
|
130
144
|
/ai doctor
|
|
131
145
|
/context школа 29
|
|
146
|
+
/profiles
|
|
147
|
+
/profile use local
|
|
148
|
+
/models openrouter --search qwen
|
|
132
149
|
/use ollama
|
|
133
150
|
/use openai
|
|
134
151
|
/key status
|
|
@@ -153,6 +170,7 @@ iola agent
|
|
|
153
170
|
```bash
|
|
154
171
|
iola ai setup ollama
|
|
155
172
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
173
|
+
iola ask "Какие школы есть на улице Петрова?"
|
|
156
174
|
```
|
|
157
175
|
|
|
158
176
|
OpenAI:
|
|
@@ -171,6 +189,40 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
171
189
|
iola ai ask "Покажи контакты лицея"
|
|
172
190
|
```
|
|
173
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
|
+
|
|
174
226
|
Проверить, какие данные попадут в AI-контекст:
|
|
175
227
|
|
|
176
228
|
```bash
|
|
@@ -219,3 +271,12 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
219
271
|
IOLA_API_BASE_URL=https://apiiola.yasg.ru/api/v1
|
|
220
272
|
IOLA_MCP_BASE_URL=https://apiiola.yasg.ru
|
|
221
273
|
```
|
|
274
|
+
|
|
275
|
+
Переменные окружения имеют приоритет над локальной конфигурацией. Локальные
|
|
276
|
+
endpoints можно настроить так:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
280
|
+
iola config set api.mcpBaseUrl https://apiiola.yasg.ru
|
|
281
|
+
iola config get
|
|
282
|
+
```
|
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";
|
|
@@ -11,10 +11,39 @@ const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
|
11
11
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
12
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
13
13
|
const DEFAULT_AI_CONFIG = {
|
|
14
|
+
api: {
|
|
15
|
+
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
16
|
+
mcpBaseUrl: "https://apiiola.yasg.ru",
|
|
17
|
+
},
|
|
14
18
|
ai: {
|
|
19
|
+
activeProfile: "local",
|
|
15
20
|
provider: "ollama",
|
|
16
21
|
model: "llama3.2:1b",
|
|
17
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
|
+
},
|
|
18
47
|
},
|
|
19
48
|
};
|
|
20
49
|
const DATASETS = {
|
|
@@ -45,9 +74,12 @@ const COMMANDS = new Map([
|
|
|
45
74
|
["help", showHelp],
|
|
46
75
|
["version", showVersion],
|
|
47
76
|
["update", checkUpdate],
|
|
77
|
+
["doctor", doctor],
|
|
78
|
+
["config", handleConfig],
|
|
48
79
|
["banner", showBanner],
|
|
49
80
|
["agent", startAgent],
|
|
50
81
|
["chat", startAgent],
|
|
82
|
+
["ask", aiAsk],
|
|
51
83
|
["ai", handleAi],
|
|
52
84
|
["init", initCli],
|
|
53
85
|
["health", checkHealth],
|
|
@@ -80,22 +112,33 @@ Usage:
|
|
|
80
112
|
iola agent
|
|
81
113
|
iola chat
|
|
82
114
|
iola init
|
|
115
|
+
iola doctor
|
|
116
|
+
iola config get
|
|
117
|
+
iola config set api.baseUrl URL
|
|
118
|
+
iola config set api.mcpBaseUrl URL
|
|
119
|
+
iola config reset
|
|
83
120
|
iola update
|
|
84
|
-
iola
|
|
121
|
+
iola ask TEXT
|
|
122
|
+
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
85
123
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
86
124
|
iola ai context TEXT [--json]
|
|
87
125
|
iola ai key set openai
|
|
88
126
|
iola ai key set openrouter
|
|
89
127
|
iola ai key status
|
|
90
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]
|
|
91
134
|
iola ai doctor [--json]
|
|
92
135
|
iola ai setup
|
|
93
136
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
94
137
|
iola health [--json]
|
|
95
138
|
iola layers [--json]
|
|
96
|
-
iola schools [--limit 10] [--search TEXT] [--format table|json|csv]
|
|
139
|
+
iola schools [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
97
140
|
iola schools get --inn INN [--json]
|
|
98
|
-
iola kindergartens [--limit 10] [--search TEXT] [--format table|json|csv]
|
|
141
|
+
iola kindergartens [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
99
142
|
iola kindergartens get --inn INN [--json]
|
|
100
143
|
iola search TEXT [--limit 5] [--format table|json|csv]
|
|
101
144
|
iola mcp-info [--json]
|
|
@@ -178,7 +221,17 @@ async function handleAgentLine(line, state) {
|
|
|
178
221
|
}
|
|
179
222
|
|
|
180
223
|
if (command === "config") {
|
|
181
|
-
await
|
|
224
|
+
await handleConfig(args.length > 0 ? args : ["get"]);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (command === "doctor") {
|
|
229
|
+
await doctor(args);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (command === "cfg" || command === "settings") {
|
|
234
|
+
await handleConfig(args);
|
|
182
235
|
return false;
|
|
183
236
|
}
|
|
184
237
|
|
|
@@ -187,6 +240,21 @@ async function handleAgentLine(line, state) {
|
|
|
187
240
|
return false;
|
|
188
241
|
}
|
|
189
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
|
+
|
|
190
258
|
if (command === "use") {
|
|
191
259
|
await useAiProvider(args);
|
|
192
260
|
return false;
|
|
@@ -229,6 +297,8 @@ async function handleAgentLine(line, state) {
|
|
|
229
297
|
|
|
230
298
|
const mapped = {
|
|
231
299
|
health: ["health", args],
|
|
300
|
+
doctor: ["doctor", args],
|
|
301
|
+
config: ["config", args],
|
|
232
302
|
layers: ["layers", args],
|
|
233
303
|
data: ["data", args],
|
|
234
304
|
schools: ["schools", args],
|
|
@@ -253,6 +323,9 @@ function printAgentHelp() {
|
|
|
253
323
|
console.log(`Slash-команды:
|
|
254
324
|
/help
|
|
255
325
|
/health
|
|
326
|
+
/doctor
|
|
327
|
+
/config get
|
|
328
|
+
/config set api.baseUrl URL
|
|
256
329
|
/layers
|
|
257
330
|
/data schools --limit 10
|
|
258
331
|
/schools --limit 10
|
|
@@ -262,6 +335,9 @@ function printAgentHelp() {
|
|
|
262
335
|
/search лицей --limit 3
|
|
263
336
|
/mcp-info
|
|
264
337
|
/context школа 29
|
|
338
|
+
/profiles
|
|
339
|
+
/profile use local
|
|
340
|
+
/models openrouter --search qwen
|
|
265
341
|
/ai doctor
|
|
266
342
|
/ai setup ollama
|
|
267
343
|
/use openai
|
|
@@ -355,7 +431,7 @@ async function checkUpdate() {
|
|
|
355
431
|
|
|
356
432
|
async function checkHealth(args) {
|
|
357
433
|
const options = parseOptions(args);
|
|
358
|
-
const health = await fetchJson(`${
|
|
434
|
+
const health = await fetchJson(`${await getMcpBaseUrl()}/mcp-health`);
|
|
359
435
|
|
|
360
436
|
if (options.json) {
|
|
361
437
|
printJson(health);
|
|
@@ -370,6 +446,94 @@ async function checkHealth(args) {
|
|
|
370
446
|
});
|
|
371
447
|
}
|
|
372
448
|
|
|
449
|
+
async function doctor(args = []) {
|
|
450
|
+
const options = parseOptions(args);
|
|
451
|
+
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
452
|
+
const config = await loadConfig();
|
|
453
|
+
const activeAiProfile = resolveAiProfile(config);
|
|
454
|
+
const secrets = await loadSecrets();
|
|
455
|
+
const diagnostics = await getLocalDiagnostics();
|
|
456
|
+
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
457
|
+
const apiBaseUrl = await getApiBaseUrl();
|
|
458
|
+
const mcpBaseUrl = await getMcpBaseUrl();
|
|
459
|
+
const report = {
|
|
460
|
+
cli: {
|
|
461
|
+
version: packageJson.default.version,
|
|
462
|
+
npmLatest: latest || "-",
|
|
463
|
+
update: getUpdateStatus(packageJson.default.version, latest),
|
|
464
|
+
},
|
|
465
|
+
api: {
|
|
466
|
+
baseUrl: apiBaseUrl,
|
|
467
|
+
mcpBaseUrl,
|
|
468
|
+
health: await probeEndpoint(`${mcpBaseUrl}/mcp-health`),
|
|
469
|
+
},
|
|
470
|
+
ai: {
|
|
471
|
+
activeProfile: getActiveProfileName(config),
|
|
472
|
+
provider: activeAiProfile.provider,
|
|
473
|
+
model: activeAiProfile.model,
|
|
474
|
+
modelAvailable: await checkConfiguredModel({ ai: activeAiProfile }),
|
|
475
|
+
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
476
|
+
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
477
|
+
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
478
|
+
},
|
|
479
|
+
system: diagnostics,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (options.json) {
|
|
483
|
+
printJson(report);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log("CLI");
|
|
488
|
+
printKeyValue(report.cli);
|
|
489
|
+
console.log("");
|
|
490
|
+
console.log("API/MCP");
|
|
491
|
+
printKeyValue(report.api);
|
|
492
|
+
console.log("");
|
|
493
|
+
console.log("AI");
|
|
494
|
+
printKeyValue(report.ai);
|
|
495
|
+
console.log("");
|
|
496
|
+
printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getUpdateStatus(current, latest) {
|
|
500
|
+
if (!latest) {
|
|
501
|
+
return "unknown";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const comparison = compareVersions(latest, current);
|
|
505
|
+
|
|
506
|
+
if (comparison > 0) {
|
|
507
|
+
return "available";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (comparison < 0) {
|
|
511
|
+
return "local-newer";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return "ok";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function checkConfiguredModel(config) {
|
|
518
|
+
if (config.ai.provider !== "ollama") {
|
|
519
|
+
return "external-api";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const response = await fetch(`${config.ai.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
|
|
524
|
+
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
return "unknown";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const payload = await response.json();
|
|
530
|
+
const models = payload.models || [];
|
|
531
|
+
return models.some((model) => model.name === config.ai.model) ? "installed" : "missing";
|
|
532
|
+
} catch {
|
|
533
|
+
return "ollama-unavailable";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
373
537
|
async function initCli(args = []) {
|
|
374
538
|
const options = parseOptions(args);
|
|
375
539
|
|
|
@@ -378,8 +542,8 @@ async function initCli(args = []) {
|
|
|
378
542
|
printKeyValue({
|
|
379
543
|
node: process.version,
|
|
380
544
|
npm: await getCommandVersion("npm", ["--version"]),
|
|
381
|
-
api: await probeEndpoint(`${
|
|
382
|
-
mcp:
|
|
545
|
+
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
546
|
+
mcp: await getMcpBaseUrl(),
|
|
383
547
|
});
|
|
384
548
|
console.log("");
|
|
385
549
|
|
|
@@ -417,6 +581,11 @@ async function handleAi(args) {
|
|
|
417
581
|
iola ai key set openrouter
|
|
418
582
|
iola ai key status
|
|
419
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]
|
|
420
589
|
iola ai doctor [--json]
|
|
421
590
|
iola ai setup
|
|
422
591
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
@@ -442,6 +611,21 @@ async function handleAi(args) {
|
|
|
442
611
|
return;
|
|
443
612
|
}
|
|
444
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
|
+
|
|
445
629
|
if (subcommand === "doctor") {
|
|
446
630
|
await aiDoctor(rest);
|
|
447
631
|
return;
|
|
@@ -455,6 +639,47 @@ async function handleAi(args) {
|
|
|
455
639
|
throw new Error(`Unknown AI command: ${subcommand}\nRun "iola ai help" to see available commands.`);
|
|
456
640
|
}
|
|
457
641
|
|
|
642
|
+
async function handleConfig(args) {
|
|
643
|
+
const [action = "get", key, ...rest] = args;
|
|
644
|
+
|
|
645
|
+
if (action === "get") {
|
|
646
|
+
const config = await loadConfig();
|
|
647
|
+
if (key) {
|
|
648
|
+
console.log(getConfigValue(config, key) ?? "-");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
printJson({
|
|
652
|
+
file: CONFIG_FILE,
|
|
653
|
+
config,
|
|
654
|
+
effective: {
|
|
655
|
+
apiBaseUrl: await getApiBaseUrl(),
|
|
656
|
+
mcpBaseUrl: await getMcpBaseUrl(),
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (action === "set") {
|
|
663
|
+
const value = rest.join(" ").trim();
|
|
664
|
+
if (!key || !value) {
|
|
665
|
+
throw new Error("Пример: iola config set api.baseUrl https://apiiola.yasg.ru/api/v1");
|
|
666
|
+
}
|
|
667
|
+
const config = await loadConfig();
|
|
668
|
+
setConfigValue(config, key, value);
|
|
669
|
+
await saveConfig(config);
|
|
670
|
+
console.log(`Сохранено: ${key} = ${value}`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (action === "reset") {
|
|
675
|
+
await writeConfig(DEFAULT_AI_CONFIG);
|
|
676
|
+
console.log(`Конфигурация сброшена: ${CONFIG_FILE}`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
throw new Error("Команды config: get, set, reset.");
|
|
681
|
+
}
|
|
682
|
+
|
|
458
683
|
async function aiDoctor(args) {
|
|
459
684
|
const options = parseOptions(args);
|
|
460
685
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -486,21 +711,50 @@ async function aiSetup(args) {
|
|
|
486
711
|
if (provider === "openai" || provider === "openrouter") {
|
|
487
712
|
const options = parseOptions(args.slice(1));
|
|
488
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();
|
|
489
717
|
await saveConfig({
|
|
490
718
|
ai: {
|
|
719
|
+
...config.ai,
|
|
720
|
+
activeProfile: profileName,
|
|
491
721
|
provider,
|
|
492
722
|
model,
|
|
493
|
-
baseUrl:
|
|
723
|
+
baseUrl: profile.baseUrl,
|
|
724
|
+
profiles: {
|
|
725
|
+
...(config.ai.profiles || {}),
|
|
726
|
+
[profileName]: profile,
|
|
727
|
+
},
|
|
494
728
|
},
|
|
495
729
|
});
|
|
496
|
-
console.log(`AI-профиль ${
|
|
730
|
+
console.log(`AI-профиль ${profileName} сохранен и выбран в ${CONFIG_FILE}`);
|
|
497
731
|
console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
|
|
498
732
|
console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
499
733
|
return;
|
|
500
734
|
}
|
|
501
735
|
|
|
502
736
|
if (provider === "codex") {
|
|
503
|
-
|
|
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");
|
|
504
758
|
return;
|
|
505
759
|
}
|
|
506
760
|
|
|
@@ -532,33 +786,318 @@ async function handleAiKey(args) {
|
|
|
532
786
|
iola ai key delete openai|openrouter`);
|
|
533
787
|
}
|
|
534
788
|
|
|
535
|
-
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) {
|
|
536
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
|
+
});
|
|
979
|
+
|
|
980
|
+
console.log(`AI-профиль сохранен: ${name}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function useAiProfile(name) {
|
|
984
|
+
if (!name) {
|
|
985
|
+
throw new Error("Имя профиля обязательно. Пример: iola ai profile use local");
|
|
986
|
+
}
|
|
987
|
+
|
|
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
|
+
}
|
|
537
1007
|
|
|
538
|
-
|
|
539
|
-
|
|
1008
|
+
async function deleteAiProfile(name) {
|
|
1009
|
+
if (!name) {
|
|
1010
|
+
throw new Error("Имя профиля обязательно.");
|
|
540
1011
|
}
|
|
541
1012
|
|
|
542
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
|
+
|
|
543
1077
|
const defaultModel = {
|
|
544
1078
|
ollama: config.ai.provider === "ollama" ? config.ai.model : "llama3.2:1b",
|
|
545
1079
|
openai: config.ai.provider === "openai" ? config.ai.model : "gpt-4.1-mini",
|
|
546
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",
|
|
547
1082
|
}[provider];
|
|
1083
|
+
const profileName = provider === "ollama" ? "local" : provider;
|
|
1084
|
+
const profile = buildProfileFromOptions(provider, { model: defaultModel });
|
|
548
1085
|
|
|
549
1086
|
await saveConfig({
|
|
550
1087
|
ai: {
|
|
1088
|
+
...config.ai,
|
|
1089
|
+
activeProfile: profileName,
|
|
551
1090
|
provider,
|
|
552
1091
|
model: defaultModel,
|
|
553
|
-
baseUrl:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1092
|
+
baseUrl: profile.baseUrl,
|
|
1093
|
+
profiles: {
|
|
1094
|
+
...(config.ai.profiles || {}),
|
|
1095
|
+
[profileName]: profile,
|
|
1096
|
+
},
|
|
558
1097
|
},
|
|
559
1098
|
});
|
|
560
1099
|
|
|
561
|
-
console.log(`AI-провайдер переключен: ${provider}, модель: ${defaultModel}`);
|
|
1100
|
+
console.log(`AI-провайдер переключен: ${provider}, профиль: ${profileName}, модель: ${defaultModel}`);
|
|
562
1101
|
}
|
|
563
1102
|
|
|
564
1103
|
async function aiContext(args) {
|
|
@@ -683,11 +1222,23 @@ async function setupOllama(args) {
|
|
|
683
1222
|
await runCommand("ollama", ["pull", model], { inherit: true });
|
|
684
1223
|
}
|
|
685
1224
|
|
|
1225
|
+
const config = await loadConfig();
|
|
1226
|
+
const profileName = options.name || "local";
|
|
686
1227
|
await saveConfig({
|
|
687
1228
|
ai: {
|
|
1229
|
+
...config.ai,
|
|
1230
|
+
activeProfile: profileName,
|
|
688
1231
|
provider: "ollama",
|
|
689
1232
|
model,
|
|
690
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
|
+
},
|
|
691
1242
|
},
|
|
692
1243
|
});
|
|
693
1244
|
|
|
@@ -704,13 +1255,7 @@ async function aiAsk(args, context = {}) {
|
|
|
704
1255
|
}
|
|
705
1256
|
|
|
706
1257
|
const config = await loadConfig();
|
|
707
|
-
const
|
|
708
|
-
const model = options.model || config.ai.model;
|
|
709
|
-
const providerConfig = {
|
|
710
|
-
...config.ai,
|
|
711
|
-
provider,
|
|
712
|
-
model,
|
|
713
|
-
};
|
|
1258
|
+
const providerConfig = resolveAiProfile(config, options);
|
|
714
1259
|
const dataContext = await buildDataContext(question);
|
|
715
1260
|
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
716
1261
|
const answer = await callAiProvider(providerConfig, messages);
|
|
@@ -719,11 +1264,33 @@ async function aiAsk(args, context = {}) {
|
|
|
719
1264
|
return answer;
|
|
720
1265
|
}
|
|
721
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
|
+
|
|
722
1287
|
async function buildDataContext(question) {
|
|
1288
|
+
const apiBaseUrl = await getApiBaseUrl();
|
|
1289
|
+
const mcpBaseUrl = await getMcpBaseUrl();
|
|
723
1290
|
const [layers, schools, kindergartens] = await Promise.all([
|
|
724
|
-
fetchJson(`${
|
|
725
|
-
fetchJson(`${
|
|
726
|
-
fetchJson(`${
|
|
1291
|
+
fetchJson(`${mcpBaseUrl}/mcp-version`),
|
|
1292
|
+
fetchJson(`${apiBaseUrl}/schools?limit=100&offset=0`),
|
|
1293
|
+
fetchJson(`${apiBaseUrl}/kindergartens?limit=100&offset=0`),
|
|
727
1294
|
]);
|
|
728
1295
|
const queryTerms = extractSearchTerms(question);
|
|
729
1296
|
const patterns = extractStructuredPatterns(question);
|
|
@@ -891,9 +1458,49 @@ async function callAiProvider(config, messages) {
|
|
|
891
1458
|
return callOpenAiCompatible(config, messages, await getApiKey("openrouter"), "OpenRouter");
|
|
892
1459
|
}
|
|
893
1460
|
|
|
1461
|
+
if (config.provider === "codex") {
|
|
1462
|
+
return callCodex(config, messages);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
894
1465
|
throw new Error(`Неизвестный AI-провайдер: ${config.provider}`);
|
|
895
1466
|
}
|
|
896
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
|
+
|
|
897
1504
|
async function callOllama(config, messages) {
|
|
898
1505
|
let response;
|
|
899
1506
|
|
|
@@ -921,7 +1528,7 @@ async function callOllama(config, messages) {
|
|
|
921
1528
|
|
|
922
1529
|
async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
923
1530
|
if (!apiKey) {
|
|
924
|
-
throw new Error(`${providerName} API key не найден.
|
|
1531
|
+
throw new Error(`${providerName} API key не найден. Выполните iola ai key set ${providerName === "OpenAI" ? "openai" : "openrouter"} или задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
925
1532
|
}
|
|
926
1533
|
|
|
927
1534
|
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
@@ -963,7 +1570,7 @@ async function getApiKey(provider) {
|
|
|
963
1570
|
|
|
964
1571
|
async function listLayers(args) {
|
|
965
1572
|
const options = parseOptions(args);
|
|
966
|
-
const info = await fetchJson(`${
|
|
1573
|
+
const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
|
|
967
1574
|
|
|
968
1575
|
if (options.json) {
|
|
969
1576
|
printJson(info.data_layers);
|
|
@@ -980,7 +1587,7 @@ async function listLayers(args) {
|
|
|
980
1587
|
|
|
981
1588
|
async function showMcpInfo(args) {
|
|
982
1589
|
const options = parseOptions(args);
|
|
983
|
-
const info = await fetchJson(`${
|
|
1590
|
+
const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
|
|
984
1591
|
|
|
985
1592
|
if (options.json) {
|
|
986
1593
|
printJson(info);
|
|
@@ -1039,22 +1646,24 @@ async function listDataset(dataset, args) {
|
|
|
1039
1646
|
params.set("limit", options.limit || "20");
|
|
1040
1647
|
params.set("offset", options.offset || "0");
|
|
1041
1648
|
|
|
1042
|
-
const data = await fetchJson(`${
|
|
1649
|
+
const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`);
|
|
1043
1650
|
const items = normalizeItems(data);
|
|
1044
|
-
const filtered =
|
|
1651
|
+
const filtered = applyDatasetFilters(items, options);
|
|
1045
1652
|
const limited = filtered.slice(0, Number(options.limit || 20));
|
|
1653
|
+
const summarized = limited.map(selectPublicSummary);
|
|
1654
|
+
const projected = projectColumns(summarized, options.columns);
|
|
1046
1655
|
|
|
1047
1656
|
if (options.json || options.format === "json") {
|
|
1048
|
-
printJson(
|
|
1657
|
+
printJson(projected);
|
|
1049
1658
|
return;
|
|
1050
1659
|
}
|
|
1051
1660
|
|
|
1052
1661
|
if (options.format === "csv") {
|
|
1053
|
-
printCsv(
|
|
1662
|
+
printCsv(projected);
|
|
1054
1663
|
return;
|
|
1055
1664
|
}
|
|
1056
1665
|
|
|
1057
|
-
printDatasetTable(
|
|
1666
|
+
printDatasetTable(projected, options.columns);
|
|
1058
1667
|
}
|
|
1059
1668
|
|
|
1060
1669
|
async function getDatasetItem(dataset, options) {
|
|
@@ -1062,7 +1671,7 @@ async function getDatasetItem(dataset, options) {
|
|
|
1062
1671
|
throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
|
|
1063
1672
|
}
|
|
1064
1673
|
|
|
1065
|
-
const data = await fetchJson(`${
|
|
1674
|
+
const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
1066
1675
|
const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
|
|
1067
1676
|
|
|
1068
1677
|
if (!item) {
|
|
@@ -1086,14 +1695,14 @@ async function searchAll(args) {
|
|
|
1086
1695
|
}
|
|
1087
1696
|
|
|
1088
1697
|
const [schools, kindergartens] = await Promise.all([
|
|
1089
|
-
fetchJson(`${
|
|
1090
|
-
fetchJson(`${
|
|
1698
|
+
fetchJson(`${await getApiBaseUrl()}/schools?limit=100&offset=0`),
|
|
1699
|
+
fetchJson(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`),
|
|
1091
1700
|
]);
|
|
1092
1701
|
|
|
1093
1702
|
const limit = Number(options.limit || 5);
|
|
1094
1703
|
const result = {
|
|
1095
|
-
schools: filterItems(normalizeItems(schools), query).slice(0, limit),
|
|
1096
|
-
kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
|
|
1704
|
+
schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
1705
|
+
kindergartens: projectColumns(filterItems(normalizeItems(kindergartens), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
1097
1706
|
};
|
|
1098
1707
|
|
|
1099
1708
|
if (options.json || options.format === "json") {
|
|
@@ -1103,17 +1712,17 @@ async function searchAll(args) {
|
|
|
1103
1712
|
|
|
1104
1713
|
if (options.format === "csv") {
|
|
1105
1714
|
printCsv([
|
|
1106
|
-
...result.schools.map((item) => ({ layer: "schools", ...
|
|
1107
|
-
...result.kindergartens.map((item) => ({ layer: "kindergartens", ...
|
|
1715
|
+
...result.schools.map((item) => ({ layer: "schools", ...item })),
|
|
1716
|
+
...result.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
|
|
1108
1717
|
]);
|
|
1109
1718
|
return;
|
|
1110
1719
|
}
|
|
1111
1720
|
|
|
1112
1721
|
console.log("Школы");
|
|
1113
|
-
printDatasetTable(result.schools);
|
|
1722
|
+
printDatasetTable(result.schools, options.columns);
|
|
1114
1723
|
console.log("");
|
|
1115
1724
|
console.log("Детские сады");
|
|
1116
|
-
printDatasetTable(result.kindergartens);
|
|
1725
|
+
printDatasetTable(result.kindergartens, options.columns);
|
|
1117
1726
|
}
|
|
1118
1727
|
|
|
1119
1728
|
async function setupClient(args) {
|
|
@@ -1137,7 +1746,7 @@ function parseOptions(args) {
|
|
|
1137
1746
|
result[arg.slice(2)] = true;
|
|
1138
1747
|
} else if (arg === "--check") {
|
|
1139
1748
|
result.check = true;
|
|
1140
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || 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") {
|
|
1141
1750
|
result[arg.slice(2)] = args[index + 1];
|
|
1142
1751
|
index += 1;
|
|
1143
1752
|
} else {
|
|
@@ -1187,6 +1796,42 @@ function filterItems(items, query) {
|
|
|
1187
1796
|
return items.filter((item) => JSON.stringify(item).toLocaleLowerCase("ru-RU").includes(normalized));
|
|
1188
1797
|
}
|
|
1189
1798
|
|
|
1799
|
+
function applyDatasetFilters(items, options) {
|
|
1800
|
+
let result = options.search ? filterItems(items, options.search) : items;
|
|
1801
|
+
|
|
1802
|
+
if (options.where) {
|
|
1803
|
+
const [field, ...valueParts] = String(options.where).split("=");
|
|
1804
|
+
const value = valueParts.join("=").trim().toLocaleLowerCase("ru-RU");
|
|
1805
|
+
const key = field.trim();
|
|
1806
|
+
|
|
1807
|
+
if (!key || !value) {
|
|
1808
|
+
throw new Error('Фильтр --where должен быть в формате field=value. Пример: --where address=Петрова');
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
result = result.filter((item) => {
|
|
1812
|
+
const summary = selectPublicSummary(item);
|
|
1813
|
+
const raw = summary[key] ?? item[key];
|
|
1814
|
+
return String(raw ?? "").toLocaleLowerCase("ru-RU").includes(value);
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
return result;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function projectColumns(rows, columnsValue) {
|
|
1822
|
+
if (!columnsValue) {
|
|
1823
|
+
return rows;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const columns = String(columnsValue).split(",").map((column) => column.trim()).filter(Boolean);
|
|
1827
|
+
|
|
1828
|
+
if (columns.length === 0) {
|
|
1829
|
+
return rows;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
return rows.map((row) => Object.fromEntries(columns.map((column) => [column, row[column] ?? ""])));
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1190
1835
|
function normalizeItems(payload) {
|
|
1191
1836
|
if (Array.isArray(payload)) {
|
|
1192
1837
|
return payload;
|
|
@@ -1206,7 +1851,7 @@ function normalizeItems(payload) {
|
|
|
1206
1851
|
function selectPublicSummary(item) {
|
|
1207
1852
|
return {
|
|
1208
1853
|
inn: item.inn,
|
|
1209
|
-
name: item.fns_short_name || item.fns_full_name,
|
|
1854
|
+
name: item.name || item.fns_short_name || item.fns_full_name,
|
|
1210
1855
|
address: item.address || item.legal_address,
|
|
1211
1856
|
phone: item.phone,
|
|
1212
1857
|
email: item.email,
|
|
@@ -1422,8 +2067,15 @@ async function confirm(question) {
|
|
|
1422
2067
|
async function saveConfig(value) {
|
|
1423
2068
|
const current = await loadConfig();
|
|
1424
2069
|
const merged = mergeConfig(current, value);
|
|
2070
|
+
if (value.ai?.profiles) {
|
|
2071
|
+
merged.ai.profiles = value.ai.profiles;
|
|
2072
|
+
}
|
|
2073
|
+
await writeConfig(merged);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
async function writeConfig(value) {
|
|
1425
2077
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
1426
|
-
await writeFile(CONFIG_FILE, `${JSON.stringify(
|
|
2078
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1427
2079
|
}
|
|
1428
2080
|
|
|
1429
2081
|
async function loadConfig() {
|
|
@@ -1439,13 +2091,68 @@ function mergeConfig(base, override) {
|
|
|
1439
2091
|
return {
|
|
1440
2092
|
...base,
|
|
1441
2093
|
...override,
|
|
2094
|
+
api: {
|
|
2095
|
+
...base.api,
|
|
2096
|
+
...(override.api || {}),
|
|
2097
|
+
},
|
|
1442
2098
|
ai: {
|
|
1443
2099
|
...base.ai,
|
|
1444
2100
|
...(override.ai || {}),
|
|
2101
|
+
profiles: {
|
|
2102
|
+
...(base.ai.profiles || {}),
|
|
2103
|
+
...(override.ai?.profiles || {}),
|
|
2104
|
+
},
|
|
1445
2105
|
},
|
|
1446
2106
|
};
|
|
1447
2107
|
}
|
|
1448
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
|
+
|
|
2122
|
+
async function getApiBaseUrl() {
|
|
2123
|
+
if (process.env.IOLA_API_BASE_URL) {
|
|
2124
|
+
return process.env.IOLA_API_BASE_URL;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const config = await loadConfig();
|
|
2128
|
+
return config.api.baseUrl;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
async function getMcpBaseUrl() {
|
|
2132
|
+
if (process.env.IOLA_MCP_BASE_URL) {
|
|
2133
|
+
return process.env.IOLA_MCP_BASE_URL;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const config = await loadConfig();
|
|
2137
|
+
return config.api.mcpBaseUrl;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function getConfigValue(config, key) {
|
|
2141
|
+
return key.split(".").reduce((value, part) => value?.[part], config);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function setConfigValue(config, key, value) {
|
|
2145
|
+
const parts = key.split(".");
|
|
2146
|
+
let current = config;
|
|
2147
|
+
|
|
2148
|
+
for (const part of parts.slice(0, -1)) {
|
|
2149
|
+
current[part] = current[part] && typeof current[part] === "object" ? current[part] : {};
|
|
2150
|
+
current = current[part];
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
current[parts.at(-1)] = value;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
1449
2156
|
async function loadSecrets() {
|
|
1450
2157
|
try {
|
|
1451
2158
|
return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
|
|
@@ -1518,6 +2225,14 @@ function runCommand(command, args, options = {}) {
|
|
|
1518
2225
|
maxBuffer: 1024 * 1024 * 5,
|
|
1519
2226
|
}, (error, stdout, stderr) => {
|
|
1520
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
|
+
|
|
1521
2236
|
reject(error);
|
|
1522
2237
|
return;
|
|
1523
2238
|
}
|
|
@@ -1529,9 +2244,23 @@ function runCommand(command, args, options = {}) {
|
|
|
1529
2244
|
child.stdout?.pipe(process.stdout);
|
|
1530
2245
|
child.stderr?.pipe(process.stderr);
|
|
1531
2246
|
}
|
|
2247
|
+
|
|
2248
|
+
if (options.input) {
|
|
2249
|
+
child.stdin?.end(options.input);
|
|
2250
|
+
}
|
|
1532
2251
|
});
|
|
1533
2252
|
}
|
|
1534
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
|
+
|
|
1535
2264
|
function roundGb(bytes) {
|
|
1536
2265
|
return Math.round((bytes / 1024 / 1024 / 1024) * 10) / 10;
|
|
1537
2266
|
}
|
|
@@ -1573,7 +2302,17 @@ function csvCell(value) {
|
|
|
1573
2302
|
return `"${text.replace(/"/g, "\"\"")}"`;
|
|
1574
2303
|
}
|
|
1575
2304
|
|
|
1576
|
-
function printDatasetTable(items) {
|
|
2305
|
+
function printDatasetTable(items, columnsValue) {
|
|
2306
|
+
if (columnsValue) {
|
|
2307
|
+
const columns = String(columnsValue)
|
|
2308
|
+
.split(",")
|
|
2309
|
+
.map((column) => column.trim())
|
|
2310
|
+
.filter(Boolean)
|
|
2311
|
+
.map((column) => [column, column]);
|
|
2312
|
+
printTable(items, columns);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
1577
2316
|
printTable(items.map(selectPublicSummary), [
|
|
1578
2317
|
["inn", "ИНН"],
|
|
1579
2318
|
["name", "Название"],
|