@iola_adm/iola-cli 0.1.2 → 0.1.3
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 +51 -0
- package/package.json +1 -1
- package/src/cli.js +315 -2
package/README.md
CHANGED
|
@@ -9,6 +9,55 @@ CLI для работы с открытыми данными городског
|
|
|
9
9
|
- `https://apiiola.yasg.ru/api/v1`
|
|
10
10
|
- `https://apiiola.yasg.ru/mcp`
|
|
11
11
|
|
|
12
|
+
## Необходимые компоненты
|
|
13
|
+
|
|
14
|
+
Проверьте Node.js и npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node --version
|
|
18
|
+
npm --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Нужен Node.js `18` или новее. Если Node.js не установлен:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Windows
|
|
25
|
+
winget install OpenJS.NodeJS.LTS
|
|
26
|
+
|
|
27
|
+
# macOS
|
|
28
|
+
brew install node
|
|
29
|
+
|
|
30
|
+
# Linux, вариант через NodeSource
|
|
31
|
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
|
32
|
+
sudo apt-get install -y nodejs
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Для локального AI-режима нужен Ollama. Проверка:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ollama --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Установка Ollama:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Windows
|
|
45
|
+
winget install Ollama.Ollama
|
|
46
|
+
|
|
47
|
+
# macOS
|
|
48
|
+
brew install --cask ollama
|
|
49
|
+
|
|
50
|
+
# Linux
|
|
51
|
+
curl -fsSL https://ollama.com/install.sh | sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Диагностика ПК и подбор локальной модели:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y @iola_adm/iola-cli ai doctor
|
|
58
|
+
npx -y @iola_adm/iola-cli ai setup ollama
|
|
59
|
+
```
|
|
60
|
+
|
|
12
61
|
## Установка и запуск
|
|
13
62
|
|
|
14
63
|
```bash
|
|
@@ -25,6 +74,8 @@ iola help
|
|
|
25
74
|
## Команды
|
|
26
75
|
|
|
27
76
|
```bash
|
|
77
|
+
iola ai doctor
|
|
78
|
+
iola ai setup ollama
|
|
28
79
|
iola health
|
|
29
80
|
iola layers
|
|
30
81
|
iola schools --limit 10
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
+
|
|
1
8
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
2
9
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
3
12
|
|
|
4
13
|
const COMMANDS = new Map([
|
|
5
14
|
["help", showHelp],
|
|
6
15
|
["version", showVersion],
|
|
16
|
+
["ai", handleAi],
|
|
7
17
|
["health", checkHealth],
|
|
8
18
|
["layers", listLayers],
|
|
9
19
|
["schools", listSchools],
|
|
@@ -28,6 +38,9 @@ async function showHelp() {
|
|
|
28
38
|
console.log(`iola - CLI для открытых данных городского округа "Город Йошкар-Ола"
|
|
29
39
|
|
|
30
40
|
Usage:
|
|
41
|
+
iola ai doctor [--json]
|
|
42
|
+
iola ai setup
|
|
43
|
+
iola ai setup ollama [--yes] [--model MODEL]
|
|
31
44
|
iola health [--json]
|
|
32
45
|
iola layers [--json]
|
|
33
46
|
iola schools [--limit 10] [--search TEXT] [--json]
|
|
@@ -67,6 +80,134 @@ async function checkHealth(args) {
|
|
|
67
80
|
});
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
async function handleAi(args) {
|
|
84
|
+
const [subcommand = "help", ...rest] = args;
|
|
85
|
+
|
|
86
|
+
if (subcommand === "help") {
|
|
87
|
+
console.log(`AI-команды:
|
|
88
|
+
iola ai doctor [--json]
|
|
89
|
+
iola ai setup
|
|
90
|
+
iola ai setup ollama [--yes] [--model MODEL]
|
|
91
|
+
|
|
92
|
+
Локальная настройка сохраняется в ${CONFIG_FILE}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (subcommand === "doctor") {
|
|
97
|
+
await aiDoctor(rest);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (subcommand === "setup") {
|
|
102
|
+
await aiSetup(rest);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unknown AI command: ${subcommand}\nRun "iola ai help" to see available commands.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function aiDoctor(args) {
|
|
110
|
+
const options = parseOptions(args);
|
|
111
|
+
const diagnostics = await getLocalDiagnostics();
|
|
112
|
+
const recommendation = recommendOllamaModel(diagnostics);
|
|
113
|
+
|
|
114
|
+
if (options.json) {
|
|
115
|
+
printJson({ ...diagnostics, recommendation });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
printDiagnostics(diagnostics, recommendation);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function aiSetup(args) {
|
|
123
|
+
const [provider] = args;
|
|
124
|
+
|
|
125
|
+
if (!provider) {
|
|
126
|
+
const selected = await chooseAiProvider();
|
|
127
|
+
await aiSetup([selected]);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (provider === "ollama") {
|
|
132
|
+
await setupOllama(args.slice(1));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (provider === "openai" || provider === "openrouter") {
|
|
137
|
+
console.log(`Для ${provider} используйте переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
138
|
+
console.log("AI-запросы через API будут добавлены отдельной командой iola ai ask.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (provider === "codex") {
|
|
143
|
+
await setupClient(["codex"]);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function chooseAiProvider() {
|
|
151
|
+
console.log("Выберите режим AI:");
|
|
152
|
+
console.log("1. Локальная модель через Ollama");
|
|
153
|
+
console.log("2. OpenAI API");
|
|
154
|
+
console.log("3. OpenRouter API");
|
|
155
|
+
console.log("4. Codex/MCP");
|
|
156
|
+
|
|
157
|
+
const rl = readline.createInterface({ input, output });
|
|
158
|
+
try {
|
|
159
|
+
const answer = (await rl.question("Введите номер [1]: ")).trim() || "1";
|
|
160
|
+
return {
|
|
161
|
+
1: "ollama",
|
|
162
|
+
2: "openai",
|
|
163
|
+
3: "openrouter",
|
|
164
|
+
4: "codex",
|
|
165
|
+
}[answer] || "ollama";
|
|
166
|
+
} finally {
|
|
167
|
+
rl.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function setupOllama(args) {
|
|
172
|
+
const options = parseOptions(args);
|
|
173
|
+
const diagnostics = await getLocalDiagnostics();
|
|
174
|
+
const recommendation = recommendOllamaModel(diagnostics);
|
|
175
|
+
const model = options.model || recommendation.model;
|
|
176
|
+
|
|
177
|
+
printDiagnostics(diagnostics, { ...recommendation, model });
|
|
178
|
+
|
|
179
|
+
if (!diagnostics.ollama.installed) {
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log("Ollama не найден. Установите Ollama, затем повторите команду:");
|
|
182
|
+
console.log(" iola ai setup ollama");
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log("Windows:");
|
|
185
|
+
console.log(" winget install Ollama.Ollama");
|
|
186
|
+
console.log("macOS:");
|
|
187
|
+
console.log(" brew install --cask ollama");
|
|
188
|
+
console.log("Linux:");
|
|
189
|
+
console.log(" curl -fsSL https://ollama.com/install.sh | sh");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const shouldInstall = options.yes || (await confirm(`Установить модель ${model} через "ollama pull ${model}"? [Y/n] `));
|
|
194
|
+
|
|
195
|
+
if (shouldInstall) {
|
|
196
|
+
await runCommand("ollama", ["pull", model], { inherit: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await saveConfig({
|
|
200
|
+
ai: {
|
|
201
|
+
provider: "ollama",
|
|
202
|
+
model,
|
|
203
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
console.log("");
|
|
208
|
+
console.log(`Готово. Локальный AI-профиль сохранен в ${CONFIG_FILE}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
70
211
|
async function listLayers(args) {
|
|
71
212
|
const options = parseOptions(args);
|
|
72
213
|
const info = await fetchJson(`${MCP_BASE_URL}/mcp-version`);
|
|
@@ -204,8 +345,8 @@ function parseOptions(args) {
|
|
|
204
345
|
|
|
205
346
|
for (let index = 0; index < args.length; index += 1) {
|
|
206
347
|
const arg = args[index];
|
|
207
|
-
if (arg === "--json") {
|
|
208
|
-
result.
|
|
348
|
+
if (arg === "--json" || arg === "--yes") {
|
|
349
|
+
result[arg.slice(2)] = true;
|
|
209
350
|
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--inn") {
|
|
210
351
|
result[arg.slice(2)] = args[index + 1];
|
|
211
352
|
index += 1;
|
|
@@ -252,6 +393,178 @@ function selectPublicSummary(item) {
|
|
|
252
393
|
};
|
|
253
394
|
}
|
|
254
395
|
|
|
396
|
+
async function getLocalDiagnostics() {
|
|
397
|
+
const [nvidia, windowsGpu, ollamaVersion] = await Promise.all([
|
|
398
|
+
getNvidiaGpu(),
|
|
399
|
+
process.platform === "win32" ? getWindowsGpu() : Promise.resolve(null),
|
|
400
|
+
getOllamaVersion(),
|
|
401
|
+
]);
|
|
402
|
+
const gpu = nvidia || windowsGpu || { name: "-", vramGb: null, source: "not-detected" };
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
os: `${os.type()} ${os.release()} (${process.arch})`,
|
|
406
|
+
cpu: os.cpus()?.[0]?.model || "-",
|
|
407
|
+
ramGb: roundGb(os.totalmem()),
|
|
408
|
+
gpu,
|
|
409
|
+
ollama: {
|
|
410
|
+
installed: Boolean(ollamaVersion),
|
|
411
|
+
version: ollamaVersion || "-",
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function getNvidiaGpu() {
|
|
417
|
+
try {
|
|
418
|
+
const { stdout } = await runCommand("nvidia-smi", [
|
|
419
|
+
"--query-gpu=name,memory.total",
|
|
420
|
+
"--format=csv,noheader,nounits",
|
|
421
|
+
]);
|
|
422
|
+
const [line] = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
423
|
+
|
|
424
|
+
if (!line) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const [name, memoryMb] = line.split(",").map((value) => value.trim());
|
|
429
|
+
return {
|
|
430
|
+
name,
|
|
431
|
+
vramGb: Math.round((Number(memoryMb) / 1024) * 10) / 10,
|
|
432
|
+
source: "nvidia-smi",
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function getWindowsGpu() {
|
|
440
|
+
try {
|
|
441
|
+
const command = [
|
|
442
|
+
"$gpu = Get-CimInstance Win32_VideoController |",
|
|
443
|
+
"Sort-Object AdapterRAM -Descending |",
|
|
444
|
+
"Select-Object -First 1 Name,AdapterRAM;",
|
|
445
|
+
"$gpu | ConvertTo-Json -Compress",
|
|
446
|
+
].join(" ");
|
|
447
|
+
const { stdout } = await runCommand("powershell.exe", ["-NoProfile", "-Command", command]);
|
|
448
|
+
const parsed = JSON.parse(stdout.trim());
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
name: parsed.Name || "-",
|
|
452
|
+
vramGb: parsed.AdapterRAM ? roundGb(Number(parsed.AdapterRAM)) : null,
|
|
453
|
+
source: "Win32_VideoController",
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function getOllamaVersion() {
|
|
461
|
+
try {
|
|
462
|
+
const { stdout } = await runCommand("ollama", ["--version"]);
|
|
463
|
+
return stdout.trim();
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function recommendOllamaModel(diagnostics) {
|
|
470
|
+
const ramGb = diagnostics.ramGb || 0;
|
|
471
|
+
const vramGb = diagnostics.gpu.vramGb || 0;
|
|
472
|
+
|
|
473
|
+
if (ramGb >= 32 && vramGb >= 8) {
|
|
474
|
+
return {
|
|
475
|
+
profile: "good",
|
|
476
|
+
model: "qwen3:8b",
|
|
477
|
+
reason: "достаточно RAM/VRAM для более качественной локальной модели.",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (ramGb >= 16 && vramGb >= 4) {
|
|
482
|
+
return {
|
|
483
|
+
profile: "balanced",
|
|
484
|
+
model: "qwen3:4b",
|
|
485
|
+
reason: "баланс качества, скорости и памяти для работы с вопросами по данным.",
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (ramGb >= 12) {
|
|
490
|
+
return {
|
|
491
|
+
profile: "standard",
|
|
492
|
+
model: "llama3.2:3b",
|
|
493
|
+
reason: "достаточно оперативной памяти для компактной универсальной модели.",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
profile: "low",
|
|
499
|
+
model: "llama3.2:1b",
|
|
500
|
+
reason: "минимальная модель для слабого ПК или CPU-only режима.",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function printDiagnostics(diagnostics, recommendation) {
|
|
505
|
+
console.log("Диагностика системы");
|
|
506
|
+
printKeyValue({
|
|
507
|
+
os: diagnostics.os,
|
|
508
|
+
cpu: diagnostics.cpu,
|
|
509
|
+
ram: `${diagnostics.ramGb} GB`,
|
|
510
|
+
gpu: diagnostics.gpu.name,
|
|
511
|
+
vram: diagnostics.gpu.vramGb ? `${diagnostics.gpu.vramGb} GB` : "-",
|
|
512
|
+
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "не установлен",
|
|
513
|
+
});
|
|
514
|
+
console.log("");
|
|
515
|
+
console.log("Рекомендация");
|
|
516
|
+
printKeyValue({
|
|
517
|
+
profile: recommendation.profile,
|
|
518
|
+
model: recommendation.model,
|
|
519
|
+
reason: recommendation.reason,
|
|
520
|
+
install: `ollama pull ${recommendation.model}`,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function confirm(question) {
|
|
525
|
+
if (!process.stdin.isTTY) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const rl = readline.createInterface({ input, output });
|
|
530
|
+
try {
|
|
531
|
+
const answer = (await rl.question(question)).trim().toLocaleLowerCase("ru-RU");
|
|
532
|
+
return answer === "" || answer === "y" || answer === "yes" || answer === "д" || answer === "да";
|
|
533
|
+
} finally {
|
|
534
|
+
rl.close();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function saveConfig(value) {
|
|
539
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
540
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function runCommand(command, args, options = {}) {
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
const child = execFile(command, args, {
|
|
546
|
+
windowsHide: true,
|
|
547
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
548
|
+
}, (error, stdout, stderr) => {
|
|
549
|
+
if (error) {
|
|
550
|
+
reject(error);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
resolve({ stdout, stderr });
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (options.inherit) {
|
|
558
|
+
child.stdout?.pipe(process.stdout);
|
|
559
|
+
child.stderr?.pipe(process.stderr);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function roundGb(bytes) {
|
|
565
|
+
return Math.round((bytes / 1024 / 1024 / 1024) * 10) / 10;
|
|
566
|
+
}
|
|
567
|
+
|
|
255
568
|
async function fetchJson(url) {
|
|
256
569
|
const response = await fetch(url, {
|
|
257
570
|
headers: {
|