@iola_adm/iola-cli 0.1.8 → 0.1.10
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 +42 -5
- package/package.json +1 -1
- package/src/cli.js +513 -30
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ npx -y @iola_adm/iola-cli ai setup ollama
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
npx -y @iola_adm/iola-cli help
|
|
65
|
+
npx -y @iola_adm/iola-cli init --yes
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
Глобальная установка:
|
|
@@ -69,6 +70,7 @@ npx -y @iola_adm/iola-cli help
|
|
|
69
70
|
```bash
|
|
70
71
|
npm install -g @iola_adm/iola-cli
|
|
71
72
|
iola help
|
|
73
|
+
iola init
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
## Команды
|
|
@@ -76,6 +78,19 @@ iola help
|
|
|
76
78
|
```bash
|
|
77
79
|
iola banner
|
|
78
80
|
iola agent
|
|
81
|
+
iola chat
|
|
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
|
|
87
|
+
iola update
|
|
88
|
+
iola version --check
|
|
89
|
+
iola ask "Найди школу 29"
|
|
90
|
+
iola data schools --limit 10
|
|
91
|
+
iola data kindergartens --search "29"
|
|
92
|
+
iola data schools --where address=Петрова --columns name,address,phone
|
|
93
|
+
iola data schools --format csv
|
|
79
94
|
iola ai doctor
|
|
80
95
|
iola ai setup ollama
|
|
81
96
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
@@ -87,6 +102,7 @@ iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
|
87
102
|
iola health
|
|
88
103
|
iola layers
|
|
89
104
|
iola schools --limit 10
|
|
105
|
+
iola schools --format csv
|
|
90
106
|
iola schools get --inn 1215067180
|
|
91
107
|
iola kindergartens --search "29"
|
|
92
108
|
iola kindergartens get --inn 1215077421 --json
|
|
@@ -96,7 +112,8 @@ iola setup codex
|
|
|
96
112
|
```
|
|
97
113
|
|
|
98
114
|
По умолчанию команды выводят компактную таблицу. Для полного ответа API
|
|
99
|
-
используйте `--json`.
|
|
115
|
+
используйте `--json` или `--format json`. Для выгрузки используйте
|
|
116
|
+
`--format csv`.
|
|
100
117
|
|
|
101
118
|
## Интерактивный режим
|
|
102
119
|
|
|
@@ -109,7 +126,10 @@ iola agent
|
|
|
109
126
|
```text
|
|
110
127
|
/help
|
|
111
128
|
/health
|
|
129
|
+
/doctor
|
|
130
|
+
/config get
|
|
112
131
|
/layers
|
|
132
|
+
/data schools --limit 10
|
|
113
133
|
/schools --limit 10
|
|
114
134
|
/schools get --inn 1215067180
|
|
115
135
|
/kindergartens --search 29
|
|
@@ -126,10 +146,13 @@ iola agent
|
|
|
126
146
|
/config
|
|
127
147
|
/history
|
|
128
148
|
/clear
|
|
149
|
+
/update
|
|
150
|
+
/init
|
|
129
151
|
/exit
|
|
130
152
|
```
|
|
131
153
|
|
|
132
154
|
Обычный текст без `/` в `iola agent` отправляется в настроенный AI-провайдер.
|
|
155
|
+
`iola chat` запускает тот же интерактивный режим.
|
|
133
156
|
|
|
134
157
|
## AI-запросы
|
|
135
158
|
|
|
@@ -138,6 +161,7 @@ iola agent
|
|
|
138
161
|
```bash
|
|
139
162
|
iola ai setup ollama
|
|
140
163
|
iola ai ask "Какие школы есть на улице Петрова?"
|
|
164
|
+
iola ask "Какие школы есть на улице Петрова?"
|
|
141
165
|
```
|
|
142
166
|
|
|
143
167
|
OpenAI:
|
|
@@ -166,6 +190,10 @@ iola ai context "улица Петрова"
|
|
|
166
190
|
|
|
167
191
|
Поиск контекста учитывает номера учреждений, ИНН и улицы.
|
|
168
192
|
|
|
193
|
+
AI-ответ строится по контексту из публичного API. Ассистент получает краткий
|
|
194
|
+
список источников контекста и должен указывать слой, название и ИНН, если
|
|
195
|
+
отвечает по конкретным организациям.
|
|
196
|
+
|
|
169
197
|
Ключи OpenAI/OpenRouter сохраняются локально на компьютере пользователя:
|
|
170
198
|
|
|
171
199
|
```text
|
|
@@ -185,14 +213,14 @@ iola ai key delete openai
|
|
|
185
213
|
`OPENROUTER_API_KEY`) и сохранен локальный ключ, CLI использует переменную
|
|
186
214
|
окружения как более приоритетную.
|
|
187
215
|
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
Если данных в контексте недостаточно, ассистент должен сообщить об этом, а не
|
|
217
|
+
выдумывать сведения.
|
|
190
218
|
|
|
191
219
|
## Назначение
|
|
192
220
|
|
|
193
221
|
CLI дает прямой терминальный доступ к открытым данным городского округа,
|
|
194
|
-
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter
|
|
195
|
-
интерактивному агентному
|
|
222
|
+
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter,
|
|
223
|
+
интерактивному агентному режиму, экспорту данных и проверке обновлений.
|
|
196
224
|
|
|
197
225
|
## Переменные окружения
|
|
198
226
|
|
|
@@ -200,3 +228,12 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
200
228
|
IOLA_API_BASE_URL=https://apiiola.yasg.ru/api/v1
|
|
201
229
|
IOLA_MCP_BASE_URL=https://apiiola.yasg.ru
|
|
202
230
|
```
|
|
231
|
+
|
|
232
|
+
Переменные окружения имеют приоритет над локальной конфигурацией. Локальные
|
|
233
|
+
endpoints можно настроить так:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
237
|
+
iola config set api.mcpBaseUrl https://apiiola.yasg.ru
|
|
238
|
+
iola config get
|
|
239
|
+
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -11,12 +11,26 @@ 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: {
|
|
15
19
|
provider: "ollama",
|
|
16
20
|
model: "llama3.2:1b",
|
|
17
21
|
baseUrl: "http://127.0.0.1:11434",
|
|
18
22
|
},
|
|
19
23
|
};
|
|
24
|
+
const DATASETS = {
|
|
25
|
+
schools: {
|
|
26
|
+
title: "Школы",
|
|
27
|
+
endpoint: "schools",
|
|
28
|
+
},
|
|
29
|
+
kindergartens: {
|
|
30
|
+
title: "Детские сады",
|
|
31
|
+
endpoint: "kindergartens",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
20
34
|
const BANNER = `\x1b[38;5;45m┌────────────────────────────────────────────────────────────────────────────┐
|
|
21
35
|
│\x1b[38;5;51m ____ _ ___ ____ ____ ___ _____ _ _______ \x1b[38;5;45m│
|
|
22
36
|
│\x1b[38;5;51m / ___| | |_ _| | _ \\| _ \\ / _ \\| ____| |/ /_ _| \x1b[38;5;45m│
|
|
@@ -34,11 +48,18 @@ const BANNER = `\x1b[38;5;45m┌────────────────
|
|
|
34
48
|
const COMMANDS = new Map([
|
|
35
49
|
["help", showHelp],
|
|
36
50
|
["version", showVersion],
|
|
51
|
+
["update", checkUpdate],
|
|
52
|
+
["doctor", doctor],
|
|
53
|
+
["config", handleConfig],
|
|
37
54
|
["banner", showBanner],
|
|
38
55
|
["agent", startAgent],
|
|
56
|
+
["chat", startAgent],
|
|
57
|
+
["ask", aiAsk],
|
|
39
58
|
["ai", handleAi],
|
|
59
|
+
["init", initCli],
|
|
40
60
|
["health", checkHealth],
|
|
41
61
|
["layers", listLayers],
|
|
62
|
+
["data", handleData],
|
|
42
63
|
["schools", listSchools],
|
|
43
64
|
["kindergartens", listKindergartens],
|
|
44
65
|
["search", searchAll],
|
|
@@ -64,6 +85,16 @@ async function showHelp() {
|
|
|
64
85
|
Usage:
|
|
65
86
|
iola banner
|
|
66
87
|
iola agent
|
|
88
|
+
iola chat
|
|
89
|
+
iola init
|
|
90
|
+
iola doctor
|
|
91
|
+
iola config get
|
|
92
|
+
iola config set api.baseUrl URL
|
|
93
|
+
iola config set api.mcpBaseUrl URL
|
|
94
|
+
iola config reset
|
|
95
|
+
iola update
|
|
96
|
+
iola ask TEXT
|
|
97
|
+
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
67
98
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
68
99
|
iola ai context TEXT [--json]
|
|
69
100
|
iola ai key set openai
|
|
@@ -75,11 +106,11 @@ Usage:
|
|
|
75
106
|
iola ai setup ollama [--yes] [--model MODEL]
|
|
76
107
|
iola health [--json]
|
|
77
108
|
iola layers [--json]
|
|
78
|
-
iola schools [--limit 10] [--search TEXT] [--json]
|
|
109
|
+
iola schools [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
79
110
|
iola schools get --inn INN [--json]
|
|
80
|
-
iola kindergartens [--limit 10] [--search TEXT] [--json]
|
|
111
|
+
iola kindergartens [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
81
112
|
iola kindergartens get --inn INN [--json]
|
|
82
|
-
iola search TEXT [--limit 5] [--json]
|
|
113
|
+
iola search TEXT [--limit 5] [--format table|json|csv]
|
|
83
114
|
iola mcp-info [--json]
|
|
84
115
|
iola setup codex
|
|
85
116
|
iola version
|
|
@@ -160,7 +191,17 @@ async function handleAgentLine(line, state) {
|
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
if (command === "config") {
|
|
163
|
-
await
|
|
194
|
+
await handleConfig(args.length > 0 ? args : ["get"]);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (command === "doctor") {
|
|
199
|
+
await doctor(args);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (command === "cfg" || command === "settings") {
|
|
204
|
+
await handleConfig(args);
|
|
164
205
|
return false;
|
|
165
206
|
}
|
|
166
207
|
|
|
@@ -194,6 +235,16 @@ async function handleAgentLine(line, state) {
|
|
|
194
235
|
return false;
|
|
195
236
|
}
|
|
196
237
|
|
|
238
|
+
if (command === "update") {
|
|
239
|
+
await checkUpdate(args);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (command === "init") {
|
|
244
|
+
await initCli(args);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
197
248
|
if (command === "ai") {
|
|
198
249
|
await handleAi(args);
|
|
199
250
|
return false;
|
|
@@ -201,7 +252,10 @@ async function handleAgentLine(line, state) {
|
|
|
201
252
|
|
|
202
253
|
const mapped = {
|
|
203
254
|
health: ["health", args],
|
|
255
|
+
doctor: ["doctor", args],
|
|
256
|
+
config: ["config", args],
|
|
204
257
|
layers: ["layers", args],
|
|
258
|
+
data: ["data", args],
|
|
205
259
|
schools: ["schools", args],
|
|
206
260
|
kindergartens: ["kindergartens", args],
|
|
207
261
|
search: ["search", args],
|
|
@@ -224,7 +278,11 @@ function printAgentHelp() {
|
|
|
224
278
|
console.log(`Slash-команды:
|
|
225
279
|
/help
|
|
226
280
|
/health
|
|
281
|
+
/doctor
|
|
282
|
+
/config get
|
|
283
|
+
/config set api.baseUrl URL
|
|
227
284
|
/layers
|
|
285
|
+
/data schools --limit 10
|
|
228
286
|
/schools --limit 10
|
|
229
287
|
/schools get --inn 1215067180
|
|
230
288
|
/kindergartens --search 29
|
|
@@ -244,6 +302,8 @@ function printAgentHelp() {
|
|
|
244
302
|
/history
|
|
245
303
|
/clear
|
|
246
304
|
/banner
|
|
305
|
+
/update
|
|
306
|
+
/init
|
|
247
307
|
/exit
|
|
248
308
|
|
|
249
309
|
Обычный текст без slash-команды отправляется в настроенный AI-провайдер.`);
|
|
@@ -282,14 +342,48 @@ function showBanner() {
|
|
|
282
342
|
console.log("открытые данные • MCP • локальный AI");
|
|
283
343
|
}
|
|
284
344
|
|
|
285
|
-
async function showVersion() {
|
|
345
|
+
async function showVersion(args = []) {
|
|
346
|
+
const options = parseOptions(args);
|
|
286
347
|
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
287
348
|
console.log(packageJson.default.version);
|
|
349
|
+
|
|
350
|
+
if (options.check) {
|
|
351
|
+
await checkUpdate([]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function checkUpdate() {
|
|
356
|
+
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
357
|
+
const current = packageJson.default.version;
|
|
358
|
+
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
359
|
+
|
|
360
|
+
if (!latest) {
|
|
361
|
+
console.log("Не удалось проверить npm-версию.");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const comparison = compareVersions(latest, current);
|
|
366
|
+
|
|
367
|
+
if (comparison > 0) {
|
|
368
|
+
console.log(`Доступна новая версия: ${latest}`);
|
|
369
|
+
console.log("Обновление:");
|
|
370
|
+
console.log(` npm install -g ${packageJson.default.name}@latest`);
|
|
371
|
+
console.log("Или запуск без установки:");
|
|
372
|
+
console.log(` npx -y ${packageJson.default.name}@latest help`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (comparison < 0) {
|
|
377
|
+
console.log(`Локальная версия ${current} новее опубликованной npm latest ${latest}.`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(`Установлена актуальная версия: ${current}`);
|
|
288
382
|
}
|
|
289
383
|
|
|
290
384
|
async function checkHealth(args) {
|
|
291
385
|
const options = parseOptions(args);
|
|
292
|
-
const health = await fetchJson(`${
|
|
386
|
+
const health = await fetchJson(`${await getMcpBaseUrl()}/mcp-health`);
|
|
293
387
|
|
|
294
388
|
if (options.json) {
|
|
295
389
|
printJson(health);
|
|
@@ -304,6 +398,127 @@ async function checkHealth(args) {
|
|
|
304
398
|
});
|
|
305
399
|
}
|
|
306
400
|
|
|
401
|
+
async function doctor(args = []) {
|
|
402
|
+
const options = parseOptions(args);
|
|
403
|
+
const packageJson = await import("../package.json", { with: { type: "json" } });
|
|
404
|
+
const config = await loadConfig();
|
|
405
|
+
const secrets = await loadSecrets();
|
|
406
|
+
const diagnostics = await getLocalDiagnostics();
|
|
407
|
+
const latest = await getLatestNpmVersion(packageJson.default.name);
|
|
408
|
+
const apiBaseUrl = await getApiBaseUrl();
|
|
409
|
+
const mcpBaseUrl = await getMcpBaseUrl();
|
|
410
|
+
const report = {
|
|
411
|
+
cli: {
|
|
412
|
+
version: packageJson.default.version,
|
|
413
|
+
npmLatest: latest || "-",
|
|
414
|
+
update: getUpdateStatus(packageJson.default.version, latest),
|
|
415
|
+
},
|
|
416
|
+
api: {
|
|
417
|
+
baseUrl: apiBaseUrl,
|
|
418
|
+
mcpBaseUrl,
|
|
419
|
+
health: await probeEndpoint(`${mcpBaseUrl}/mcp-health`),
|
|
420
|
+
},
|
|
421
|
+
ai: {
|
|
422
|
+
provider: config.ai.provider,
|
|
423
|
+
model: config.ai.model,
|
|
424
|
+
modelAvailable: await checkConfiguredModel(config),
|
|
425
|
+
openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
|
|
426
|
+
openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
|
|
427
|
+
ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
|
|
428
|
+
},
|
|
429
|
+
system: diagnostics,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
if (options.json) {
|
|
433
|
+
printJson(report);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
console.log("CLI");
|
|
438
|
+
printKeyValue(report.cli);
|
|
439
|
+
console.log("");
|
|
440
|
+
console.log("API/MCP");
|
|
441
|
+
printKeyValue(report.api);
|
|
442
|
+
console.log("");
|
|
443
|
+
console.log("AI");
|
|
444
|
+
printKeyValue(report.ai);
|
|
445
|
+
console.log("");
|
|
446
|
+
printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getUpdateStatus(current, latest) {
|
|
450
|
+
if (!latest) {
|
|
451
|
+
return "unknown";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const comparison = compareVersions(latest, current);
|
|
455
|
+
|
|
456
|
+
if (comparison > 0) {
|
|
457
|
+
return "available";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (comparison < 0) {
|
|
461
|
+
return "local-newer";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return "ok";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function checkConfiguredModel(config) {
|
|
468
|
+
if (config.ai.provider !== "ollama") {
|
|
469
|
+
return "external-api";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const response = await fetch(`${config.ai.baseUrl || "http://127.0.0.1:11434"}/api/tags`);
|
|
474
|
+
|
|
475
|
+
if (!response.ok) {
|
|
476
|
+
return "unknown";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const payload = await response.json();
|
|
480
|
+
const models = payload.models || [];
|
|
481
|
+
return models.some((model) => model.name === config.ai.model) ? "installed" : "missing";
|
|
482
|
+
} catch {
|
|
483
|
+
return "ollama-unavailable";
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function initCli(args = []) {
|
|
488
|
+
const options = parseOptions(args);
|
|
489
|
+
|
|
490
|
+
showBanner();
|
|
491
|
+
console.log("Проверка окружения");
|
|
492
|
+
printKeyValue({
|
|
493
|
+
node: process.version,
|
|
494
|
+
npm: await getCommandVersion("npm", ["--version"]),
|
|
495
|
+
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
496
|
+
mcp: await getMcpBaseUrl(),
|
|
497
|
+
});
|
|
498
|
+
console.log("");
|
|
499
|
+
|
|
500
|
+
await aiDoctor(options.json ? ["--json"] : []);
|
|
501
|
+
|
|
502
|
+
if (!process.stdin.isTTY || options.yes) {
|
|
503
|
+
console.log("");
|
|
504
|
+
console.log("Для настройки AI используйте:");
|
|
505
|
+
console.log(" iola ai setup ollama");
|
|
506
|
+
console.log(" iola ai key set openai");
|
|
507
|
+
console.log(" iola ai setup openai --model gpt-4.1-mini");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log("");
|
|
512
|
+
const configureAi = await confirm("Настроить AI-провайдер сейчас? [Y/n] ");
|
|
513
|
+
|
|
514
|
+
if (configureAi) {
|
|
515
|
+
await aiSetup([]);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log("");
|
|
519
|
+
await checkUpdate();
|
|
520
|
+
}
|
|
521
|
+
|
|
307
522
|
async function handleAi(args) {
|
|
308
523
|
const [subcommand = "help", ...rest] = args;
|
|
309
524
|
|
|
@@ -354,6 +569,47 @@ async function handleAi(args) {
|
|
|
354
569
|
throw new Error(`Unknown AI command: ${subcommand}\nRun "iola ai help" to see available commands.`);
|
|
355
570
|
}
|
|
356
571
|
|
|
572
|
+
async function handleConfig(args) {
|
|
573
|
+
const [action = "get", key, ...rest] = args;
|
|
574
|
+
|
|
575
|
+
if (action === "get") {
|
|
576
|
+
const config = await loadConfig();
|
|
577
|
+
if (key) {
|
|
578
|
+
console.log(getConfigValue(config, key) ?? "-");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
printJson({
|
|
582
|
+
file: CONFIG_FILE,
|
|
583
|
+
config,
|
|
584
|
+
effective: {
|
|
585
|
+
apiBaseUrl: await getApiBaseUrl(),
|
|
586
|
+
mcpBaseUrl: await getMcpBaseUrl(),
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (action === "set") {
|
|
593
|
+
const value = rest.join(" ").trim();
|
|
594
|
+
if (!key || !value) {
|
|
595
|
+
throw new Error("Пример: iola config set api.baseUrl https://apiiola.yasg.ru/api/v1");
|
|
596
|
+
}
|
|
597
|
+
const config = await loadConfig();
|
|
598
|
+
setConfigValue(config, key, value);
|
|
599
|
+
await saveConfig(config);
|
|
600
|
+
console.log(`Сохранено: ${key} = ${value}`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (action === "reset") {
|
|
605
|
+
await writeConfig(DEFAULT_AI_CONFIG);
|
|
606
|
+
console.log(`Конфигурация сброшена: ${CONFIG_FILE}`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw new Error("Команды config: get, set, reset.");
|
|
611
|
+
}
|
|
612
|
+
|
|
357
613
|
async function aiDoctor(args) {
|
|
358
614
|
const options = parseOptions(args);
|
|
359
615
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -393,7 +649,8 @@ async function aiSetup(args) {
|
|
|
393
649
|
},
|
|
394
650
|
});
|
|
395
651
|
console.log(`AI-профиль ${provider} сохранен в ${CONFIG_FILE}`);
|
|
396
|
-
console.log(`Ключ
|
|
652
|
+
console.log(`Ключ сохраните командой: iola ai key set ${provider}`);
|
|
653
|
+
console.log(`Также можно использовать переменную окружения ${provider === "openai" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
397
654
|
return;
|
|
398
655
|
}
|
|
399
656
|
|
|
@@ -618,10 +875,12 @@ async function aiAsk(args, context = {}) {
|
|
|
618
875
|
}
|
|
619
876
|
|
|
620
877
|
async function buildDataContext(question) {
|
|
878
|
+
const apiBaseUrl = await getApiBaseUrl();
|
|
879
|
+
const mcpBaseUrl = await getMcpBaseUrl();
|
|
621
880
|
const [layers, schools, kindergartens] = await Promise.all([
|
|
622
|
-
fetchJson(`${
|
|
623
|
-
fetchJson(`${
|
|
624
|
-
fetchJson(`${
|
|
881
|
+
fetchJson(`${mcpBaseUrl}/mcp-version`),
|
|
882
|
+
fetchJson(`${apiBaseUrl}/schools?limit=100&offset=0`),
|
|
883
|
+
fetchJson(`${apiBaseUrl}/kindergartens?limit=100&offset=0`),
|
|
625
884
|
]);
|
|
626
885
|
const queryTerms = extractSearchTerms(question);
|
|
627
886
|
const patterns = extractStructuredPatterns(question);
|
|
@@ -738,12 +997,14 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
738
997
|
}
|
|
739
998
|
|
|
740
999
|
function buildAiMessages(question, dataContext, history) {
|
|
1000
|
+
const sourceLines = buildSourceLines(dataContext);
|
|
741
1001
|
const system = [
|
|
742
1002
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
743
1003
|
"Отвечай на русском языке.",
|
|
744
1004
|
"Используй только данные из переданного контекста.",
|
|
745
1005
|
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
746
1006
|
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
1007
|
+
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
747
1008
|
"Отвечай кратко и по делу.",
|
|
748
1009
|
].join(" ");
|
|
749
1010
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
@@ -754,11 +1015,26 @@ function buildAiMessages(question, dataContext, history) {
|
|
|
754
1015
|
...recentHistory,
|
|
755
1016
|
{
|
|
756
1017
|
role: "user",
|
|
757
|
-
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nВопрос пользователя: ${question}`,
|
|
1018
|
+
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nКраткие источники контекста:\n${sourceLines}\n\nВопрос пользователя: ${question}`,
|
|
758
1019
|
},
|
|
759
1020
|
];
|
|
760
1021
|
}
|
|
761
1022
|
|
|
1023
|
+
function buildSourceLines(dataContext) {
|
|
1024
|
+
const rows = [
|
|
1025
|
+
...dataContext.schools.map((item) => ({ layer: "schools", ...item })),
|
|
1026
|
+
...dataContext.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
|
|
1027
|
+
];
|
|
1028
|
+
|
|
1029
|
+
if (rows.length === 0) {
|
|
1030
|
+
return "Совпавших организаций нет.";
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return rows
|
|
1034
|
+
.map((item) => `- ${item.layer}: ${item.name || "-"}; ИНН ${item.inn || "-"}; адрес ${item.address || "-"}`)
|
|
1035
|
+
.join("\n");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
762
1038
|
async function callAiProvider(config, messages) {
|
|
763
1039
|
if (config.provider === "ollama") {
|
|
764
1040
|
return callOllama(config, messages);
|
|
@@ -802,7 +1078,7 @@ async function callOllama(config, messages) {
|
|
|
802
1078
|
|
|
803
1079
|
async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
804
1080
|
if (!apiKey) {
|
|
805
|
-
throw new Error(`${providerName} API key не найден.
|
|
1081
|
+
throw new Error(`${providerName} API key не найден. Выполните iola ai key set ${providerName === "OpenAI" ? "openai" : "openrouter"} или задайте ${providerName === "OpenAI" ? "OPENAI_API_KEY" : "OPENROUTER_API_KEY"}.`);
|
|
806
1082
|
}
|
|
807
1083
|
|
|
808
1084
|
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
@@ -844,7 +1120,7 @@ async function getApiKey(provider) {
|
|
|
844
1120
|
|
|
845
1121
|
async function listLayers(args) {
|
|
846
1122
|
const options = parseOptions(args);
|
|
847
|
-
const info = await fetchJson(`${
|
|
1123
|
+
const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
|
|
848
1124
|
|
|
849
1125
|
if (options.json) {
|
|
850
1126
|
printJson(info.data_layers);
|
|
@@ -861,7 +1137,7 @@ async function listLayers(args) {
|
|
|
861
1137
|
|
|
862
1138
|
async function showMcpInfo(args) {
|
|
863
1139
|
const options = parseOptions(args);
|
|
864
|
-
const info = await fetchJson(`${
|
|
1140
|
+
const info = await fetchJson(`${await getMcpBaseUrl()}/mcp-version`);
|
|
865
1141
|
|
|
866
1142
|
if (options.json) {
|
|
867
1143
|
printJson(info);
|
|
@@ -886,6 +1162,28 @@ async function listKindergartens(args) {
|
|
|
886
1162
|
await listDataset("kindergartens", args);
|
|
887
1163
|
}
|
|
888
1164
|
|
|
1165
|
+
async function handleData(args) {
|
|
1166
|
+
const [dataset, ...rest] = args;
|
|
1167
|
+
|
|
1168
|
+
if (!dataset) {
|
|
1169
|
+
console.log("Доступные слои:");
|
|
1170
|
+
printTable(Object.entries(DATASETS).map(([id, value]) => ({ id, name: value.title })), [
|
|
1171
|
+
["id", "ID"],
|
|
1172
|
+
["name", "Название"],
|
|
1173
|
+
]);
|
|
1174
|
+
console.log("");
|
|
1175
|
+
console.log("Пример:");
|
|
1176
|
+
console.log(" iola data schools --limit 10");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (!DATASETS[dataset]) {
|
|
1181
|
+
throw new Error(`Неизвестный слой: ${dataset}. Доступно: ${Object.keys(DATASETS).join(", ")}`);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
await listDataset(dataset, rest);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
889
1187
|
async function listDataset(dataset, args) {
|
|
890
1188
|
const options = parseOptions(args);
|
|
891
1189
|
|
|
@@ -898,17 +1196,24 @@ async function listDataset(dataset, args) {
|
|
|
898
1196
|
params.set("limit", options.limit || "20");
|
|
899
1197
|
params.set("offset", options.offset || "0");
|
|
900
1198
|
|
|
901
|
-
const data = await fetchJson(`${
|
|
1199
|
+
const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`);
|
|
902
1200
|
const items = normalizeItems(data);
|
|
903
|
-
const filtered =
|
|
1201
|
+
const filtered = applyDatasetFilters(items, options);
|
|
904
1202
|
const limited = filtered.slice(0, Number(options.limit || 20));
|
|
1203
|
+
const summarized = limited.map(selectPublicSummary);
|
|
1204
|
+
const projected = projectColumns(summarized, options.columns);
|
|
905
1205
|
|
|
906
|
-
if (options.json) {
|
|
907
|
-
printJson(
|
|
1206
|
+
if (options.json || options.format === "json") {
|
|
1207
|
+
printJson(projected);
|
|
908
1208
|
return;
|
|
909
1209
|
}
|
|
910
1210
|
|
|
911
|
-
|
|
1211
|
+
if (options.format === "csv") {
|
|
1212
|
+
printCsv(projected);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
printDatasetTable(projected, options.columns);
|
|
912
1217
|
}
|
|
913
1218
|
|
|
914
1219
|
async function getDatasetItem(dataset, options) {
|
|
@@ -916,7 +1221,7 @@ async function getDatasetItem(dataset, options) {
|
|
|
916
1221
|
throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
|
|
917
1222
|
}
|
|
918
1223
|
|
|
919
|
-
const data = await fetchJson(`${
|
|
1224
|
+
const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
920
1225
|
const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
|
|
921
1226
|
|
|
922
1227
|
if (!item) {
|
|
@@ -940,26 +1245,34 @@ async function searchAll(args) {
|
|
|
940
1245
|
}
|
|
941
1246
|
|
|
942
1247
|
const [schools, kindergartens] = await Promise.all([
|
|
943
|
-
fetchJson(`${
|
|
944
|
-
fetchJson(`${
|
|
1248
|
+
fetchJson(`${await getApiBaseUrl()}/schools?limit=100&offset=0`),
|
|
1249
|
+
fetchJson(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`),
|
|
945
1250
|
]);
|
|
946
1251
|
|
|
947
1252
|
const limit = Number(options.limit || 5);
|
|
948
1253
|
const result = {
|
|
949
|
-
schools: filterItems(normalizeItems(schools), query).slice(0, limit),
|
|
950
|
-
kindergartens: filterItems(normalizeItems(kindergartens), query).slice(0, limit),
|
|
1254
|
+
schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
1255
|
+
kindergartens: projectColumns(filterItems(normalizeItems(kindergartens), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
951
1256
|
};
|
|
952
1257
|
|
|
953
|
-
if (options.json) {
|
|
1258
|
+
if (options.json || options.format === "json") {
|
|
954
1259
|
printJson(result);
|
|
955
1260
|
return;
|
|
956
1261
|
}
|
|
957
1262
|
|
|
1263
|
+
if (options.format === "csv") {
|
|
1264
|
+
printCsv([
|
|
1265
|
+
...result.schools.map((item) => ({ layer: "schools", ...item })),
|
|
1266
|
+
...result.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
|
|
1267
|
+
]);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
958
1271
|
console.log("Школы");
|
|
959
|
-
printDatasetTable(result.schools);
|
|
1272
|
+
printDatasetTable(result.schools, options.columns);
|
|
960
1273
|
console.log("");
|
|
961
1274
|
console.log("Детские сады");
|
|
962
|
-
printDatasetTable(result.kindergartens);
|
|
1275
|
+
printDatasetTable(result.kindergartens, options.columns);
|
|
963
1276
|
}
|
|
964
1277
|
|
|
965
1278
|
async function setupClient(args) {
|
|
@@ -981,7 +1294,9 @@ function parseOptions(args) {
|
|
|
981
1294
|
const arg = args[index];
|
|
982
1295
|
if (arg === "--json" || arg === "--yes") {
|
|
983
1296
|
result[arg.slice(2)] = true;
|
|
984
|
-
} else if (arg === "--
|
|
1297
|
+
} else if (arg === "--check") {
|
|
1298
|
+
result.check = true;
|
|
1299
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--format") {
|
|
985
1300
|
result[arg.slice(2)] = args[index + 1];
|
|
986
1301
|
index += 1;
|
|
987
1302
|
} else {
|
|
@@ -1031,6 +1346,42 @@ function filterItems(items, query) {
|
|
|
1031
1346
|
return items.filter((item) => JSON.stringify(item).toLocaleLowerCase("ru-RU").includes(normalized));
|
|
1032
1347
|
}
|
|
1033
1348
|
|
|
1349
|
+
function applyDatasetFilters(items, options) {
|
|
1350
|
+
let result = options.search ? filterItems(items, options.search) : items;
|
|
1351
|
+
|
|
1352
|
+
if (options.where) {
|
|
1353
|
+
const [field, ...valueParts] = String(options.where).split("=");
|
|
1354
|
+
const value = valueParts.join("=").trim().toLocaleLowerCase("ru-RU");
|
|
1355
|
+
const key = field.trim();
|
|
1356
|
+
|
|
1357
|
+
if (!key || !value) {
|
|
1358
|
+
throw new Error('Фильтр --where должен быть в формате field=value. Пример: --where address=Петрова');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
result = result.filter((item) => {
|
|
1362
|
+
const summary = selectPublicSummary(item);
|
|
1363
|
+
const raw = summary[key] ?? item[key];
|
|
1364
|
+
return String(raw ?? "").toLocaleLowerCase("ru-RU").includes(value);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return result;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function projectColumns(rows, columnsValue) {
|
|
1372
|
+
if (!columnsValue) {
|
|
1373
|
+
return rows;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const columns = String(columnsValue).split(",").map((column) => column.trim()).filter(Boolean);
|
|
1377
|
+
|
|
1378
|
+
if (columns.length === 0) {
|
|
1379
|
+
return rows;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return rows.map((row) => Object.fromEntries(columns.map((column) => [column, row[column] ?? ""])));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1034
1385
|
function normalizeItems(payload) {
|
|
1035
1386
|
if (Array.isArray(payload)) {
|
|
1036
1387
|
return payload;
|
|
@@ -1050,7 +1401,7 @@ function normalizeItems(payload) {
|
|
|
1050
1401
|
function selectPublicSummary(item) {
|
|
1051
1402
|
return {
|
|
1052
1403
|
inn: item.inn,
|
|
1053
|
-
name: item.fns_short_name || item.fns_full_name,
|
|
1404
|
+
name: item.name || item.fns_short_name || item.fns_full_name,
|
|
1054
1405
|
address: item.address || item.legal_address,
|
|
1055
1406
|
phone: item.phone,
|
|
1056
1407
|
email: item.email,
|
|
@@ -1134,6 +1485,66 @@ async function getOllamaVersion() {
|
|
|
1134
1485
|
}
|
|
1135
1486
|
}
|
|
1136
1487
|
|
|
1488
|
+
async function getCommandVersion(command, args) {
|
|
1489
|
+
try {
|
|
1490
|
+
const { stdout } = await runCommand(command, args);
|
|
1491
|
+
return stdout.trim() || "installed";
|
|
1492
|
+
} catch {
|
|
1493
|
+
if (process.platform === "win32" && !command.endsWith(".cmd")) {
|
|
1494
|
+
try {
|
|
1495
|
+
const { stdout } = await runCommand(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `${command} ${args.join(" ")}`]);
|
|
1496
|
+
return stdout.trim() || "installed";
|
|
1497
|
+
} catch {
|
|
1498
|
+
return "не найден";
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return "не найден";
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async function probeEndpoint(url) {
|
|
1507
|
+
try {
|
|
1508
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
1509
|
+
return response.ok ? "доступен" : `${response.status} ${response.statusText}`;
|
|
1510
|
+
} catch {
|
|
1511
|
+
return "недоступен";
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async function getLatestNpmVersion(packageName) {
|
|
1516
|
+
try {
|
|
1517
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
|
|
1518
|
+
headers: { accept: "application/json" },
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
if (!response.ok) {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const payload = await response.json();
|
|
1526
|
+
return payload.version || null;
|
|
1527
|
+
} catch {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function compareVersions(left, right) {
|
|
1533
|
+
const leftParts = String(left).split(".").map(Number);
|
|
1534
|
+
const rightParts = String(right).split(".").map(Number);
|
|
1535
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
1536
|
+
|
|
1537
|
+
for (let index = 0; index < length; index += 1) {
|
|
1538
|
+
const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
|
|
1539
|
+
|
|
1540
|
+
if (diff !== 0) {
|
|
1541
|
+
return diff > 0 ? 1 : -1;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
return 0;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1137
1548
|
function recommendOllamaModel(diagnostics) {
|
|
1138
1549
|
const ramGb = diagnostics.ramGb || 0;
|
|
1139
1550
|
const vramGb = diagnostics.gpu.vramGb || 0;
|
|
@@ -1204,6 +1615,12 @@ async function confirm(question) {
|
|
|
1204
1615
|
}
|
|
1205
1616
|
|
|
1206
1617
|
async function saveConfig(value) {
|
|
1618
|
+
const current = await loadConfig();
|
|
1619
|
+
const merged = mergeConfig(current, value);
|
|
1620
|
+
await writeConfig(merged);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function writeConfig(value) {
|
|
1207
1624
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
1208
1625
|
await writeFile(CONFIG_FILE, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1209
1626
|
}
|
|
@@ -1221,6 +1638,10 @@ function mergeConfig(base, override) {
|
|
|
1221
1638
|
return {
|
|
1222
1639
|
...base,
|
|
1223
1640
|
...override,
|
|
1641
|
+
api: {
|
|
1642
|
+
...base.api,
|
|
1643
|
+
...(override.api || {}),
|
|
1644
|
+
},
|
|
1224
1645
|
ai: {
|
|
1225
1646
|
...base.ai,
|
|
1226
1647
|
...(override.ai || {}),
|
|
@@ -1228,6 +1649,40 @@ function mergeConfig(base, override) {
|
|
|
1228
1649
|
};
|
|
1229
1650
|
}
|
|
1230
1651
|
|
|
1652
|
+
async function getApiBaseUrl() {
|
|
1653
|
+
if (process.env.IOLA_API_BASE_URL) {
|
|
1654
|
+
return process.env.IOLA_API_BASE_URL;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const config = await loadConfig();
|
|
1658
|
+
return config.api.baseUrl;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
async function getMcpBaseUrl() {
|
|
1662
|
+
if (process.env.IOLA_MCP_BASE_URL) {
|
|
1663
|
+
return process.env.IOLA_MCP_BASE_URL;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const config = await loadConfig();
|
|
1667
|
+
return config.api.mcpBaseUrl;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function getConfigValue(config, key) {
|
|
1671
|
+
return key.split(".").reduce((value, part) => value?.[part], config);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function setConfigValue(config, key, value) {
|
|
1675
|
+
const parts = key.split(".");
|
|
1676
|
+
let current = config;
|
|
1677
|
+
|
|
1678
|
+
for (const part of parts.slice(0, -1)) {
|
|
1679
|
+
current[part] = current[part] && typeof current[part] === "object" ? current[part] : {};
|
|
1680
|
+
current = current[part];
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
current[parts.at(-1)] = value;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1231
1686
|
async function loadSecrets() {
|
|
1232
1687
|
try {
|
|
1233
1688
|
return JSON.parse(await readFile(SECRETS_FILE, "utf8"));
|
|
@@ -1337,7 +1792,35 @@ function printJson(value) {
|
|
|
1337
1792
|
console.log(JSON.stringify(value, null, 2));
|
|
1338
1793
|
}
|
|
1339
1794
|
|
|
1340
|
-
function
|
|
1795
|
+
function printCsv(rows) {
|
|
1796
|
+
if (rows.length === 0) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
1801
|
+
console.log(columns.map(csvCell).join(","));
|
|
1802
|
+
|
|
1803
|
+
for (const row of rows) {
|
|
1804
|
+
console.log(columns.map((column) => csvCell(row[column])).join(","));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function csvCell(value) {
|
|
1809
|
+
const text = value == null ? "" : String(value);
|
|
1810
|
+
return `"${text.replace(/"/g, "\"\"")}"`;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function printDatasetTable(items, columnsValue) {
|
|
1814
|
+
if (columnsValue) {
|
|
1815
|
+
const columns = String(columnsValue)
|
|
1816
|
+
.split(",")
|
|
1817
|
+
.map((column) => column.trim())
|
|
1818
|
+
.filter(Boolean)
|
|
1819
|
+
.map((column) => [column, column]);
|
|
1820
|
+
printTable(items, columns);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1341
1824
|
printTable(items.map(selectPublicSummary), [
|
|
1342
1825
|
["inn", "ИНН"],
|
|
1343
1826
|
["name", "Название"],
|