@iola_adm/iola-cli 0.1.13 → 0.1.15
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 +104 -0
- package/package.json +1 -1
- package/src/cli.js +982 -26
package/README.md
CHANGED
|
@@ -91,12 +91,37 @@ iola db status
|
|
|
91
91
|
iola db init
|
|
92
92
|
iola history --limit 20
|
|
93
93
|
iola history clear
|
|
94
|
+
iola sessions --limit 20
|
|
95
|
+
iola resume 1 "продолжи"
|
|
96
|
+
iola fork 1 "новый вопрос"
|
|
97
|
+
iola features list
|
|
98
|
+
iola features enable api-cache
|
|
99
|
+
iola mcp status
|
|
100
|
+
iola mcp list
|
|
101
|
+
iola mcp install codex
|
|
102
|
+
iola cache status
|
|
103
|
+
iola cache warm
|
|
104
|
+
iola cache clear
|
|
105
|
+
iola sync
|
|
106
|
+
iola search "Петрова" --local
|
|
107
|
+
iola data schools --where address=Петрова --save schools-petrova
|
|
108
|
+
iola views
|
|
109
|
+
iola view schools-petrova --format csv --output schools-petrova.csv
|
|
110
|
+
iola views delete schools-petrova
|
|
111
|
+
iola report missing-phones
|
|
112
|
+
iola privacy
|
|
113
|
+
iola backup create
|
|
114
|
+
iola alias add petrova "data schools --where address=Петрова --columns name,address,phone"
|
|
115
|
+
iola run "выгрузи школы на Петрова в csv"
|
|
94
116
|
iola config get
|
|
95
117
|
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
96
118
|
iola config reset
|
|
97
119
|
iola update
|
|
98
120
|
iola version --check
|
|
99
121
|
iola ask "Найди школу 29"
|
|
122
|
+
iola ask "Найди школу 29" --profile codex --events --output answer.txt
|
|
123
|
+
iola ask "Найди школу 29" --schema json --no-history
|
|
124
|
+
iola data schools --format csv --output schools.csv
|
|
100
125
|
iola data schools --limit 10
|
|
101
126
|
iola data kindergartens --search "29"
|
|
102
127
|
iola data schools --where address=Петрова --columns name,address,phone
|
|
@@ -144,6 +169,10 @@ iola agent
|
|
|
144
169
|
/health
|
|
145
170
|
/doctor
|
|
146
171
|
/db status
|
|
172
|
+
/sessions
|
|
173
|
+
/resume 1
|
|
174
|
+
/features list
|
|
175
|
+
/mcp status
|
|
147
176
|
/config get
|
|
148
177
|
/layers
|
|
149
178
|
/data schools --limit 10
|
|
@@ -300,11 +329,86 @@ iola db reset
|
|
|
300
329
|
iola history --limit 20
|
|
301
330
|
iola history --json
|
|
302
331
|
iola history clear
|
|
332
|
+
iola sessions --limit 20
|
|
333
|
+
iola sessions clear
|
|
334
|
+
iola resume 1 "продолжи"
|
|
335
|
+
iola fork 1 "новая ветка разговора"
|
|
303
336
|
```
|
|
304
337
|
|
|
305
338
|
Ключи OpenAI/OpenRouter в SQLite не сохраняются. Они остаются в локальном
|
|
306
339
|
`secrets.json` или в переменных окружения.
|
|
307
340
|
|
|
341
|
+
## Feature flags, MCP и машинный вывод
|
|
342
|
+
|
|
343
|
+
Экспериментальные и системные возможности можно включать отдельно:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
iola features list
|
|
347
|
+
iola features enable api-cache
|
|
348
|
+
iola features disable sqlite-history
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
MCP-интеграции:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
iola mcp status
|
|
355
|
+
iola mcp list
|
|
356
|
+
iola mcp install codex
|
|
357
|
+
iola mcp remove codex
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Для автоматизации доступны события, JSON-ответ и запись результата в файл:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
iola ask "Найди школу 29" --events
|
|
364
|
+
iola ask "Найди школу 29" --schema json
|
|
365
|
+
iola ask "Найди школу 29" --output answer.txt
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Кеш, локальный поиск и выборки
|
|
369
|
+
|
|
370
|
+
API-ответы можно кешировать локально:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
iola cache status
|
|
374
|
+
iola cache warm
|
|
375
|
+
iola cache clear
|
|
376
|
+
iola data schools --cache
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Локальная синхронизация сохраняет открытые слои в SQLite и позволяет искать без
|
|
380
|
+
повторного обращения к API:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
iola sync
|
|
384
|
+
iola search "Петрова" --local
|
|
385
|
+
iola data schools --local --search "лицей"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Сохраненные выборки:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
iola data schools --where address=Петрова --columns name,address,phone --save schools-petrova
|
|
392
|
+
iola views
|
|
393
|
+
iola view schools-petrova
|
|
394
|
+
iola view schools-petrova --format csv --output schools-petrova.csv
|
|
395
|
+
iola views delete schools-petrova
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Отчеты, backup и алиасы:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
iola report schools-summary
|
|
402
|
+
iola report education-contacts
|
|
403
|
+
iola report missing-phones
|
|
404
|
+
iola report licenses
|
|
405
|
+
iola privacy
|
|
406
|
+
iola backup create
|
|
407
|
+
iola alias add petrova "data schools --where address=Петрова --columns name,address,phone"
|
|
408
|
+
iola petrova
|
|
409
|
+
iola run "выгрузи школы на Петрова в csv"
|
|
410
|
+
```
|
|
411
|
+
|
|
308
412
|
## Переменные окружения
|
|
309
413
|
|
|
310
414
|
```bash
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -14,7 +14,15 @@ const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
|
14
14
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
15
15
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
16
16
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
17
|
-
const DB_SCHEMA_VERSION =
|
|
17
|
+
const DB_SCHEMA_VERSION = 2;
|
|
18
|
+
const FEATURES = {
|
|
19
|
+
"sqlite-history": { stage: "stable", defaultEnabled: true, description: "Запись истории AI-запросов в SQLite." },
|
|
20
|
+
sessions: { stage: "stable", defaultEnabled: true, description: "Сессии, resume и fork для AI-диалогов." },
|
|
21
|
+
"api-cache": { stage: "experimental", defaultEnabled: false, description: "Локальный кеш API-ответов." },
|
|
22
|
+
events: { stage: "experimental", defaultEnabled: true, description: "JSONL-события выполнения ask." },
|
|
23
|
+
"mcp-management": { stage: "stable", defaultEnabled: true, description: "Команды управления MCP-интеграциями." },
|
|
24
|
+
"web-search": { stage: "experimental", defaultEnabled: false, description: "Резерв под web-search режимы AI." },
|
|
25
|
+
};
|
|
18
26
|
const DEFAULT_AI_CONFIG = {
|
|
19
27
|
api: {
|
|
20
28
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -82,6 +90,20 @@ const COMMANDS = new Map([
|
|
|
82
90
|
["doctor", doctor],
|
|
83
91
|
["db", handleDb],
|
|
84
92
|
["history", handleHistory],
|
|
93
|
+
["sessions", handleSessions],
|
|
94
|
+
["resume", resumeSession],
|
|
95
|
+
["fork", forkSession],
|
|
96
|
+
["features", handleFeatures],
|
|
97
|
+
["mcp", handleMcp],
|
|
98
|
+
["cache", handleCache],
|
|
99
|
+
["sync", handleSync],
|
|
100
|
+
["views", handleViews],
|
|
101
|
+
["view", handleView],
|
|
102
|
+
["report", handleReport],
|
|
103
|
+
["privacy", handlePrivacy],
|
|
104
|
+
["backup", handleBackup],
|
|
105
|
+
["alias", handleAlias],
|
|
106
|
+
["run", runNaturalLanguage],
|
|
85
107
|
["config", handleConfig],
|
|
86
108
|
["banner", showBanner],
|
|
87
109
|
["agent", startAgent],
|
|
@@ -109,6 +131,11 @@ export async function main(argv) {
|
|
|
109
131
|
const handler = COMMANDS.get(command);
|
|
110
132
|
|
|
111
133
|
if (!handler) {
|
|
134
|
+
const alias = getAlias(command);
|
|
135
|
+
if (alias) {
|
|
136
|
+
await main([...splitCommandLine(alias.command), ...args]);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
112
139
|
throw new Error(`Unknown command: ${command}\nRun "iola help" to see available commands.`);
|
|
113
140
|
}
|
|
114
141
|
|
|
@@ -129,12 +156,26 @@ Usage:
|
|
|
129
156
|
iola db init
|
|
130
157
|
iola history [--limit 20]
|
|
131
158
|
iola history clear
|
|
159
|
+
iola sessions [--limit 20]
|
|
160
|
+
iola resume SESSION_ID [TEXT]
|
|
161
|
+
iola fork SESSION_ID [TEXT]
|
|
162
|
+
iola features list|enable|disable
|
|
163
|
+
iola mcp list|status|install|remove
|
|
164
|
+
iola cache status|warm|clear
|
|
165
|
+
iola sync [--dataset schools|kindergartens]
|
|
166
|
+
iola views
|
|
167
|
+
iola view NAME [--format table|json|csv] [--output FILE]
|
|
168
|
+
iola report schools-summary|education-contacts|missing-phones|licenses
|
|
169
|
+
iola privacy
|
|
170
|
+
iola backup create
|
|
171
|
+
iola alias add NAME COMMAND
|
|
172
|
+
iola run "выгрузи школы на Петрова в csv"
|
|
132
173
|
iola config get
|
|
133
174
|
iola config set api.baseUrl URL
|
|
134
175
|
iola config set api.mcpBaseUrl URL
|
|
135
176
|
iola config reset
|
|
136
177
|
iola update
|
|
137
|
-
iola ask TEXT
|
|
178
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
138
179
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
139
180
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
140
181
|
iola ai context TEXT [--json]
|
|
@@ -248,6 +289,46 @@ async function handleAgentLine(line, state) {
|
|
|
248
289
|
return false;
|
|
249
290
|
}
|
|
250
291
|
|
|
292
|
+
if (command === "sessions") {
|
|
293
|
+
await handleSessions(args);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (command === "resume") {
|
|
298
|
+
await resumeSession(args);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (command === "fork") {
|
|
303
|
+
await forkSession(args);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (command === "features") {
|
|
308
|
+
await handleFeatures(args);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (command === "mcp") {
|
|
313
|
+
await handleMcp(args);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (command === "cache") {
|
|
318
|
+
await handleCache(args);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (command === "sync") {
|
|
323
|
+
await handleSync(args);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
|
|
328
|
+
await COMMANDS.get(command)(args);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
251
332
|
if (command === "config") {
|
|
252
333
|
await handleConfig(args.length > 0 ? args : ["get"]);
|
|
253
334
|
return false;
|
|
@@ -328,6 +409,13 @@ async function handleAgentLine(line, state) {
|
|
|
328
409
|
doctor: ["doctor", args],
|
|
329
410
|
db: ["db", args],
|
|
330
411
|
history: ["history", args],
|
|
412
|
+
sessions: ["sessions", args],
|
|
413
|
+
resume: ["resume", args],
|
|
414
|
+
fork: ["fork", args],
|
|
415
|
+
features: ["features", args],
|
|
416
|
+
mcp: ["mcp", args],
|
|
417
|
+
cache: ["cache", args],
|
|
418
|
+
sync: ["sync", args],
|
|
331
419
|
config: ["config", args],
|
|
332
420
|
layers: ["layers", args],
|
|
333
421
|
data: ["data", args],
|
|
@@ -355,6 +443,13 @@ function printAgentHelp() {
|
|
|
355
443
|
/health
|
|
356
444
|
/doctor
|
|
357
445
|
/db status
|
|
446
|
+
/sessions
|
|
447
|
+
/resume SESSION_ID
|
|
448
|
+
/features list
|
|
449
|
+
/mcp status
|
|
450
|
+
/cache status
|
|
451
|
+
/sync
|
|
452
|
+
/views
|
|
358
453
|
/config get
|
|
359
454
|
/config set api.baseUrl URL
|
|
360
455
|
/layers
|
|
@@ -520,6 +615,20 @@ async function doctor(args = []) {
|
|
|
520
615
|
return;
|
|
521
616
|
}
|
|
522
617
|
|
|
618
|
+
if (options.summary) {
|
|
619
|
+
printTable([
|
|
620
|
+
{ group: "cli", status: report.cli.nodeStatus === "ok" && report.cli.update !== "available" ? "ok" : "check" },
|
|
621
|
+
{ group: "sqlite", status: report.db.status },
|
|
622
|
+
{ group: "api", status: report.api.health },
|
|
623
|
+
{ group: "ai", status: report.ai.provider },
|
|
624
|
+
{ group: "ollama", status: report.ai.ollama },
|
|
625
|
+
], [
|
|
626
|
+
["group", "Группа"],
|
|
627
|
+
["status", "Статус"],
|
|
628
|
+
]);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
523
632
|
console.log("CLI");
|
|
524
633
|
printKeyValue(report.cli);
|
|
525
634
|
console.log("");
|
|
@@ -533,6 +642,11 @@ async function doctor(args = []) {
|
|
|
533
642
|
printKeyValue(report.ai);
|
|
534
643
|
console.log("");
|
|
535
644
|
printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
|
|
645
|
+
if (options.all) {
|
|
646
|
+
console.log("");
|
|
647
|
+
console.log("Фичи");
|
|
648
|
+
await handleFeatures(["list"]);
|
|
649
|
+
}
|
|
536
650
|
}
|
|
537
651
|
|
|
538
652
|
function getUpdateStatus(current, latest) {
|
|
@@ -853,6 +967,270 @@ async function handleHistory(args) {
|
|
|
853
967
|
]);
|
|
854
968
|
}
|
|
855
969
|
|
|
970
|
+
async function handleSessions(args) {
|
|
971
|
+
const [action] = args;
|
|
972
|
+
if (action === "clear") {
|
|
973
|
+
clearSessions();
|
|
974
|
+
console.log("Сессии очищены.");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const options = parseOptions(args);
|
|
979
|
+
const rows = listSessions(Number(options.limit || 20));
|
|
980
|
+
|
|
981
|
+
if (options.json) {
|
|
982
|
+
printJson(rows);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
printTable(rows, [
|
|
987
|
+
["id", "ID"],
|
|
988
|
+
["updated_at", "Обновлена"],
|
|
989
|
+
["profile", "Профиль"],
|
|
990
|
+
["provider", "Провайдер"],
|
|
991
|
+
["model", "Модель"],
|
|
992
|
+
["messages", "Сообщ."],
|
|
993
|
+
["title", "Название"],
|
|
994
|
+
]);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function resumeSession(args) {
|
|
998
|
+
const [sessionId, ...questionParts] = args;
|
|
999
|
+
if (!sessionId) {
|
|
1000
|
+
throw new Error("SESSION_ID обязателен. Пример: iola resume 1 \"продолжи\"");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const question = questionParts.join(" ").trim();
|
|
1004
|
+
if (!question) {
|
|
1005
|
+
printSessionMessages(Number(sessionId));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const session = getSession(Number(sessionId));
|
|
1010
|
+
await aiAsk([question, "--session", sessionId, "--profile", session.profile || "local"]);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function forkSession(args) {
|
|
1014
|
+
const [sessionId, ...questionParts] = args;
|
|
1015
|
+
if (!sessionId) {
|
|
1016
|
+
throw new Error("SESSION_ID обязателен. Пример: iola fork 1 \"новый вопрос\"");
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const forkedId = forkSessionInDb(Number(sessionId));
|
|
1020
|
+
console.log(`Создана новая сессия: ${forkedId}`);
|
|
1021
|
+
const question = questionParts.join(" ").trim();
|
|
1022
|
+
if (question) {
|
|
1023
|
+
const session = getSession(forkedId);
|
|
1024
|
+
await aiAsk([question, "--session", String(forkedId), "--profile", session.profile || "local"]);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function handleFeatures(args) {
|
|
1029
|
+
const [action = "list", name] = args;
|
|
1030
|
+
|
|
1031
|
+
if (action === "list" || action === "ls") {
|
|
1032
|
+
const rows = listFeatures();
|
|
1033
|
+
printTable(rows, [
|
|
1034
|
+
["name", "Фича"],
|
|
1035
|
+
["enabled", "Вкл"],
|
|
1036
|
+
["stage", "Стадия"],
|
|
1037
|
+
["description", "Описание"],
|
|
1038
|
+
]);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (action === "enable" || action === "disable") {
|
|
1043
|
+
if (!name || !FEATURES[name]) {
|
|
1044
|
+
throw new Error(`Неизвестная фича. Доступно: ${Object.keys(FEATURES).join(", ")}`);
|
|
1045
|
+
}
|
|
1046
|
+
setFeatureEnabled(name, action === "enable");
|
|
1047
|
+
console.log(`${name}: ${action === "enable" ? "enabled" : "disabled"}`);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function handleMcp(args) {
|
|
1055
|
+
const [action = "status", target = "codex"] = args;
|
|
1056
|
+
|
|
1057
|
+
if (action === "status") {
|
|
1058
|
+
const [health, version] = await Promise.all([
|
|
1059
|
+
fetchJson(`${await getMcpBaseUrl()}/mcp-health`),
|
|
1060
|
+
fetchJson(`${await getMcpBaseUrl()}/mcp-version`),
|
|
1061
|
+
]);
|
|
1062
|
+
printKeyValue({
|
|
1063
|
+
endpoint: `${await getMcpBaseUrl()}/mcp`,
|
|
1064
|
+
status: health.status,
|
|
1065
|
+
server_version: version.server_version,
|
|
1066
|
+
layers: version.data_layers?.map((layer) => layer.id).join(", "),
|
|
1067
|
+
});
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (action === "list") {
|
|
1072
|
+
await runCommand("codex", ["mcp", "list"], { inherit: true });
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (action === "install" || action === "add") {
|
|
1077
|
+
await setupClient([target]);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (action === "remove" || action === "delete") {
|
|
1082
|
+
if (target !== "codex") {
|
|
1083
|
+
throw new Error("Пока доступно удаление только Codex MCP.");
|
|
1084
|
+
}
|
|
1085
|
+
await runCommand("codex", ["mcp", "remove", "yoshkarOlaPublicData"], { inherit: true });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
throw new Error("Команды mcp: status, list, install codex, remove codex.");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function handleCache(args) {
|
|
1093
|
+
const [action = "status"] = args;
|
|
1094
|
+
if (action === "status") {
|
|
1095
|
+
printKeyValue(getCacheStatus());
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (action === "clear") {
|
|
1099
|
+
clearCache();
|
|
1100
|
+
console.log("Кеш очищен.");
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (action === "warm") {
|
|
1104
|
+
const result = await warmCache();
|
|
1105
|
+
printKeyValue(result);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
throw new Error("Команды cache: status, warm, clear.");
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function handleSync(args) {
|
|
1112
|
+
const options = parseOptions(args);
|
|
1113
|
+
const datasets = options.dataset ? [options.dataset] : Object.keys(DATASETS);
|
|
1114
|
+
const rows = [];
|
|
1115
|
+
for (const dataset of datasets) {
|
|
1116
|
+
rows.push(await syncDataset(dataset));
|
|
1117
|
+
}
|
|
1118
|
+
printTable(rows, [
|
|
1119
|
+
["dataset", "Слой"],
|
|
1120
|
+
["records", "Записей"],
|
|
1121
|
+
["status", "Статус"],
|
|
1122
|
+
["message", "Сообщение"],
|
|
1123
|
+
]);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async function handleViews(args) {
|
|
1127
|
+
const [action, name] = args;
|
|
1128
|
+
if (action === "delete" || action === "remove") {
|
|
1129
|
+
deleteSavedView(name);
|
|
1130
|
+
console.log(`View удален: ${name}`);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const rows = listSavedViews();
|
|
1134
|
+
printTable(rows, [
|
|
1135
|
+
["name", "Имя"],
|
|
1136
|
+
["dataset", "Слой"],
|
|
1137
|
+
["created_at", "Создано"],
|
|
1138
|
+
]);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function handleView(args) {
|
|
1142
|
+
const [name, ...rest] = args;
|
|
1143
|
+
if (!name) {
|
|
1144
|
+
throw new Error("Имя view обязательно.");
|
|
1145
|
+
}
|
|
1146
|
+
const view = getSavedView(name);
|
|
1147
|
+
const query = JSON.parse(view.query_json);
|
|
1148
|
+
await listDataset(view.dataset, [...(query.args || []), ...rest]);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async function handleReport(args) {
|
|
1152
|
+
const [name] = args;
|
|
1153
|
+
await ensureLocalData();
|
|
1154
|
+
if (name === "schools-summary") {
|
|
1155
|
+
printTable(getLocalSummaryRows("schools"), [["metric", "Показатель"], ["value", "Значение"]]);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (name === "education-contacts") {
|
|
1159
|
+
printDatasetTable(searchLocalRecords("", { dataset: "all", limit: 500 }), "name,address,phone,email,website");
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (name === "missing-phones") {
|
|
1163
|
+
printDatasetTable(searchLocalRecords("", { dataset: "all", limit: 500 }).filter((item) => !item.phone || item.phone === "-"));
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (name === "licenses") {
|
|
1167
|
+
printDatasetTable(searchLocalRecords("", { dataset: "all", limit: 500 }), "name,license_number,license_status");
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
throw new Error("Отчеты: schools-summary, education-contacts, missing-phones, licenses.");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function handlePrivacy() {
|
|
1174
|
+
printKeyValue({
|
|
1175
|
+
config: CONFIG_FILE,
|
|
1176
|
+
secrets: SECRETS_FILE,
|
|
1177
|
+
sqlite: DB_FILE,
|
|
1178
|
+
api: await getApiBaseUrl(),
|
|
1179
|
+
mcp: await getMcpBaseUrl(),
|
|
1180
|
+
keys_in_sqlite: "no",
|
|
1181
|
+
history_clear: "iola history clear",
|
|
1182
|
+
db_reset: "iola db reset",
|
|
1183
|
+
delete_openai_key: "iola ai key delete openai",
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
async function handleBackup(args) {
|
|
1188
|
+
const [action = "create", fileArg] = args;
|
|
1189
|
+
if (action !== "create") {
|
|
1190
|
+
throw new Error("Пока доступно: iola backup create [FILE]");
|
|
1191
|
+
}
|
|
1192
|
+
const file = fileArg || path.join(CONFIG_DIR, `iola-backup-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
|
|
1193
|
+
const payload = {
|
|
1194
|
+
created_at: new Date().toISOString(),
|
|
1195
|
+
config: await loadConfig(),
|
|
1196
|
+
db: exportDbSnapshot(),
|
|
1197
|
+
};
|
|
1198
|
+
await writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
1199
|
+
console.log(`Backup создан: ${file}`);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
async function handleAlias(args) {
|
|
1203
|
+
const [action, name, ...commandParts] = args;
|
|
1204
|
+
if (action === "list" || !action) {
|
|
1205
|
+
printTable(listAliases(), [["name", "Алиас"], ["command", "Команда"]]);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (action === "add") {
|
|
1209
|
+
if (!name || commandParts.length === 0) {
|
|
1210
|
+
throw new Error('Пример: iola alias add petrova "data schools --where address=Петрова"');
|
|
1211
|
+
}
|
|
1212
|
+
saveAlias(name, commandParts.join(" "));
|
|
1213
|
+
console.log(`Алиас сохранен: ${name}`);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (action === "delete" || action === "remove") {
|
|
1217
|
+
deleteAlias(name);
|
|
1218
|
+
console.log(`Алиас удален: ${name}`);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
throw new Error("Команды alias: list, add NAME COMMAND, delete NAME.");
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async function runNaturalLanguage(args) {
|
|
1225
|
+
const text = args.join(" ").trim();
|
|
1226
|
+
if (!text) {
|
|
1227
|
+
throw new Error('Пример: iola run "выгрузи школы на Петрова в csv"');
|
|
1228
|
+
}
|
|
1229
|
+
const command = inferCommandFromText(text);
|
|
1230
|
+
console.log(`> iola ${command.join(" ")}`);
|
|
1231
|
+
await main(command);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
856
1234
|
async function aiDoctor(args) {
|
|
857
1235
|
const options = parseOptions(args);
|
|
858
1236
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -1367,6 +1745,32 @@ function initDatabase() {
|
|
|
1367
1745
|
error TEXT
|
|
1368
1746
|
);
|
|
1369
1747
|
CREATE INDEX IF NOT EXISTS idx_ask_history_created_at ON ask_history(created_at DESC);
|
|
1748
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
1749
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1750
|
+
parent_id INTEGER,
|
|
1751
|
+
title TEXT NOT NULL,
|
|
1752
|
+
profile TEXT,
|
|
1753
|
+
provider TEXT,
|
|
1754
|
+
model TEXT,
|
|
1755
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1756
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1757
|
+
);
|
|
1758
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at DESC);
|
|
1759
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
1760
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1761
|
+
session_id INTEGER NOT NULL,
|
|
1762
|
+
role TEXT NOT NULL,
|
|
1763
|
+
content TEXT NOT NULL,
|
|
1764
|
+
context_json TEXT,
|
|
1765
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1766
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
1767
|
+
);
|
|
1768
|
+
CREATE INDEX IF NOT EXISTS idx_session_messages_session_id ON session_messages(session_id, id);
|
|
1769
|
+
CREATE TABLE IF NOT EXISTS feature_flags (
|
|
1770
|
+
name TEXT PRIMARY KEY,
|
|
1771
|
+
enabled INTEGER NOT NULL,
|
|
1772
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1773
|
+
);
|
|
1370
1774
|
CREATE TABLE IF NOT EXISTS api_cache (
|
|
1371
1775
|
key TEXT PRIMARY KEY,
|
|
1372
1776
|
url TEXT NOT NULL,
|
|
@@ -1381,6 +1785,29 @@ function initDatabase() {
|
|
|
1381
1785
|
query_json TEXT NOT NULL,
|
|
1382
1786
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1383
1787
|
);
|
|
1788
|
+
CREATE TABLE IF NOT EXISTS local_records (
|
|
1789
|
+
dataset TEXT NOT NULL,
|
|
1790
|
+
record_key TEXT NOT NULL,
|
|
1791
|
+
record_json TEXT NOT NULL,
|
|
1792
|
+
searchable_text TEXT NOT NULL,
|
|
1793
|
+
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1794
|
+
PRIMARY KEY(dataset, record_key)
|
|
1795
|
+
);
|
|
1796
|
+
CREATE INDEX IF NOT EXISTS idx_local_records_dataset ON local_records(dataset);
|
|
1797
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS local_records_fts USING fts5(dataset, record_key, searchable_text);
|
|
1798
|
+
CREATE TABLE IF NOT EXISTS sync_runs (
|
|
1799
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1800
|
+
dataset TEXT NOT NULL,
|
|
1801
|
+
records INTEGER NOT NULL,
|
|
1802
|
+
status TEXT NOT NULL,
|
|
1803
|
+
message TEXT,
|
|
1804
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1805
|
+
);
|
|
1806
|
+
CREATE TABLE IF NOT EXISTS aliases (
|
|
1807
|
+
name TEXT PRIMARY KEY,
|
|
1808
|
+
command TEXT NOT NULL,
|
|
1809
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1810
|
+
);
|
|
1384
1811
|
`);
|
|
1385
1812
|
db.prepare(`
|
|
1386
1813
|
INSERT INTO meta(key, value) VALUES ('schema_version', ?)
|
|
@@ -1406,11 +1833,17 @@ function getDbStatus() {
|
|
|
1406
1833
|
try {
|
|
1407
1834
|
const schema = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
1408
1835
|
const history = db.prepare("SELECT COUNT(*) AS count FROM ask_history").get();
|
|
1836
|
+
const sessions = db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
1837
|
+
const local = db.prepare("SELECT COUNT(*) AS count FROM local_records").get();
|
|
1838
|
+
const cache = db.prepare("SELECT COUNT(*) AS count FROM api_cache").get();
|
|
1409
1839
|
return {
|
|
1410
1840
|
status: "ok",
|
|
1411
1841
|
file: DB_FILE,
|
|
1412
1842
|
schema: schema?.value || "-",
|
|
1413
1843
|
history: history?.count ?? 0,
|
|
1844
|
+
sessions: sessions?.count ?? 0,
|
|
1845
|
+
local_records: local?.count ?? 0,
|
|
1846
|
+
cache: cache?.count ?? 0,
|
|
1414
1847
|
};
|
|
1415
1848
|
} finally {
|
|
1416
1849
|
db.close();
|
|
@@ -1426,7 +1859,7 @@ function getDbStatus() {
|
|
|
1426
1859
|
}
|
|
1427
1860
|
}
|
|
1428
1861
|
|
|
1429
|
-
function recordAskHistory({ question, answer, providerConfig, dataContext, error }) {
|
|
1862
|
+
function recordAskHistory({ question, answer, providerConfig, dataContext, error, sessionId }) {
|
|
1430
1863
|
try {
|
|
1431
1864
|
initDatabase();
|
|
1432
1865
|
const db = openDatabase();
|
|
@@ -1476,6 +1909,474 @@ function clearHistory() {
|
|
|
1476
1909
|
}
|
|
1477
1910
|
}
|
|
1478
1911
|
|
|
1912
|
+
function clearSessions() {
|
|
1913
|
+
initDatabase();
|
|
1914
|
+
const db = openDatabase();
|
|
1915
|
+
try {
|
|
1916
|
+
db.exec("DELETE FROM session_messages; DELETE FROM sessions;");
|
|
1917
|
+
} finally {
|
|
1918
|
+
db.close();
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function createSession(providerConfig, title, parentId = null) {
|
|
1923
|
+
initDatabase();
|
|
1924
|
+
const db = openDatabase();
|
|
1925
|
+
try {
|
|
1926
|
+
const result = db.prepare(`
|
|
1927
|
+
INSERT INTO sessions(parent_id, title, profile, provider, model)
|
|
1928
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1929
|
+
`).run(parentId, title.slice(0, 120), providerConfig.name || "", providerConfig.provider || "", providerConfig.model || "");
|
|
1930
|
+
return Number(result.lastInsertRowid);
|
|
1931
|
+
} finally {
|
|
1932
|
+
db.close();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
function ensureSessionForAsk(options, providerConfig, question) {
|
|
1937
|
+
if (options.session) {
|
|
1938
|
+
return Number(options.session);
|
|
1939
|
+
}
|
|
1940
|
+
return createSession(providerConfig, question);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function appendSessionExchange(sessionId, question, answer, dataContext, error) {
|
|
1944
|
+
if (!sessionId) {
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const db = openDatabase();
|
|
1948
|
+
try {
|
|
1949
|
+
db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'user', ?, ?)")
|
|
1950
|
+
.run(sessionId, question, JSON.stringify(dataContext));
|
|
1951
|
+
db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'assistant', ?, ?)")
|
|
1952
|
+
.run(sessionId, error || answer || "", JSON.stringify({ error: error || "" }));
|
|
1953
|
+
db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
1954
|
+
} finally {
|
|
1955
|
+
db.close();
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function listSessions(limit) {
|
|
1960
|
+
initDatabase();
|
|
1961
|
+
const db = openDatabase();
|
|
1962
|
+
try {
|
|
1963
|
+
return db.prepare(`
|
|
1964
|
+
SELECT s.id, s.updated_at, s.profile, s.provider, s.model, s.title, COUNT(m.id) AS messages
|
|
1965
|
+
FROM sessions s
|
|
1966
|
+
LEFT JOIN session_messages m ON m.session_id = s.id
|
|
1967
|
+
GROUP BY s.id
|
|
1968
|
+
ORDER BY s.updated_at DESC, s.id DESC
|
|
1969
|
+
LIMIT ?
|
|
1970
|
+
`).all(limit);
|
|
1971
|
+
} finally {
|
|
1972
|
+
db.close();
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function getSessionAiHistory(sessionId) {
|
|
1977
|
+
initDatabase();
|
|
1978
|
+
const db = openDatabase();
|
|
1979
|
+
try {
|
|
1980
|
+
return db.prepare("SELECT role, content FROM session_messages WHERE session_id = ? ORDER BY id ASC")
|
|
1981
|
+
.all(sessionId)
|
|
1982
|
+
.map((row) => ({ role: row.role, content: row.content }));
|
|
1983
|
+
} finally {
|
|
1984
|
+
db.close();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function getSession(sessionId) {
|
|
1989
|
+
initDatabase();
|
|
1990
|
+
const db = openDatabase();
|
|
1991
|
+
try {
|
|
1992
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1993
|
+
if (!session) {
|
|
1994
|
+
throw new Error(`Сессия не найдена: ${sessionId}`);
|
|
1995
|
+
}
|
|
1996
|
+
return session;
|
|
1997
|
+
} finally {
|
|
1998
|
+
db.close();
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function printSessionMessages(sessionId) {
|
|
2003
|
+
const rows = getSessionAiHistory(sessionId).map((row, index) => ({ id: index + 1, ...row }));
|
|
2004
|
+
printTable(rows, [
|
|
2005
|
+
["id", "#"],
|
|
2006
|
+
["role", "Роль"],
|
|
2007
|
+
["content", "Текст"],
|
|
2008
|
+
]);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function forkSessionInDb(sessionId) {
|
|
2012
|
+
initDatabase();
|
|
2013
|
+
const db = openDatabase();
|
|
2014
|
+
try {
|
|
2015
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
2016
|
+
if (!session) {
|
|
2017
|
+
throw new Error(`Сессия не найдена: ${sessionId}`);
|
|
2018
|
+
}
|
|
2019
|
+
const result = db.prepare(`
|
|
2020
|
+
INSERT INTO sessions(parent_id, title, profile, provider, model)
|
|
2021
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2022
|
+
`).run(sessionId, `Fork: ${session.title}`, session.profile, session.provider, session.model);
|
|
2023
|
+
const newId = Number(result.lastInsertRowid);
|
|
2024
|
+
const messages = db.prepare("SELECT role, content, context_json FROM session_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
|
|
2025
|
+
const insert = db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, ?, ?, ?)");
|
|
2026
|
+
for (const message of messages) {
|
|
2027
|
+
insert.run(newId, message.role, message.content, message.context_json);
|
|
2028
|
+
}
|
|
2029
|
+
return newId;
|
|
2030
|
+
} finally {
|
|
2031
|
+
db.close();
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
function listFeatures() {
|
|
2036
|
+
initDatabase();
|
|
2037
|
+
const db = openDatabase();
|
|
2038
|
+
try {
|
|
2039
|
+
return Object.entries(FEATURES).map(([name, meta]) => {
|
|
2040
|
+
const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
|
|
2041
|
+
const enabled = row ? Boolean(row.enabled) : meta.defaultEnabled;
|
|
2042
|
+
return { name, enabled: enabled ? "yes" : "no", stage: meta.stage, description: meta.description };
|
|
2043
|
+
});
|
|
2044
|
+
} finally {
|
|
2045
|
+
db.close();
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function isFeatureEnabled(name) {
|
|
2050
|
+
const meta = FEATURES[name];
|
|
2051
|
+
if (!meta) {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
initDatabase();
|
|
2055
|
+
const db = openDatabase();
|
|
2056
|
+
try {
|
|
2057
|
+
const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
|
|
2058
|
+
return row ? Boolean(row.enabled) : meta.defaultEnabled;
|
|
2059
|
+
} finally {
|
|
2060
|
+
db.close();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function setFeatureEnabled(name, enabled) {
|
|
2065
|
+
initDatabase();
|
|
2066
|
+
const db = openDatabase();
|
|
2067
|
+
try {
|
|
2068
|
+
db.prepare(`
|
|
2069
|
+
INSERT INTO feature_flags(name, enabled, updated_at) VALUES (?, ?, datetime('now'))
|
|
2070
|
+
ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, updated_at = excluded.updated_at
|
|
2071
|
+
`).run(name, enabled ? 1 : 0);
|
|
2072
|
+
} finally {
|
|
2073
|
+
db.close();
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
async function fetchJsonMaybeCached(url, options = {}) {
|
|
2078
|
+
if (!options.cache && !isFeatureEnabled("api-cache")) {
|
|
2079
|
+
return fetchJson(url);
|
|
2080
|
+
}
|
|
2081
|
+
const cached = getCachedResponse(url);
|
|
2082
|
+
if (cached) {
|
|
2083
|
+
return cached;
|
|
2084
|
+
}
|
|
2085
|
+
const payload = await fetchJson(url);
|
|
2086
|
+
setCachedResponse(url, payload, 3600);
|
|
2087
|
+
return payload;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function getCachedResponse(url) {
|
|
2091
|
+
initDatabase();
|
|
2092
|
+
const db = openDatabase();
|
|
2093
|
+
try {
|
|
2094
|
+
const row = db.prepare("SELECT response_json, expires_at FROM api_cache WHERE key = ?").get(cacheKey(url));
|
|
2095
|
+
if (!row) return null;
|
|
2096
|
+
if (row.expires_at && new Date(row.expires_at) < new Date()) return null;
|
|
2097
|
+
return JSON.parse(row.response_json);
|
|
2098
|
+
} finally {
|
|
2099
|
+
db.close();
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function setCachedResponse(url, payload, ttlSeconds) {
|
|
2104
|
+
initDatabase();
|
|
2105
|
+
const db = openDatabase();
|
|
2106
|
+
try {
|
|
2107
|
+
const expires = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
2108
|
+
db.prepare(`
|
|
2109
|
+
INSERT INTO api_cache(key, url, response_json, expires_at, created_at)
|
|
2110
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
2111
|
+
ON CONFLICT(key) DO UPDATE SET response_json = excluded.response_json, expires_at = excluded.expires_at, created_at = excluded.created_at
|
|
2112
|
+
`).run(cacheKey(url), url, JSON.stringify(payload), expires);
|
|
2113
|
+
} finally {
|
|
2114
|
+
db.close();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
function cacheKey(url) {
|
|
2119
|
+
return Buffer.from(url).toString("base64url");
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function getCacheStatus() {
|
|
2123
|
+
initDatabase();
|
|
2124
|
+
const db = openDatabase();
|
|
2125
|
+
try {
|
|
2126
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM api_cache").get();
|
|
2127
|
+
return { status: "ok", entries: row?.count ?? 0 };
|
|
2128
|
+
} finally {
|
|
2129
|
+
db.close();
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function clearCache() {
|
|
2134
|
+
initDatabase();
|
|
2135
|
+
const db = openDatabase();
|
|
2136
|
+
try {
|
|
2137
|
+
db.exec("DELETE FROM api_cache");
|
|
2138
|
+
} finally {
|
|
2139
|
+
db.close();
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
async function warmCache() {
|
|
2144
|
+
const urls = [
|
|
2145
|
+
`${await getMcpBaseUrl()}/mcp-version`,
|
|
2146
|
+
`${await getMcpBaseUrl()}/mcp-health`,
|
|
2147
|
+
`${await getApiBaseUrl()}/schools?limit=100&offset=0`,
|
|
2148
|
+
`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`,
|
|
2149
|
+
];
|
|
2150
|
+
for (const url of urls) {
|
|
2151
|
+
setCachedResponse(url, await fetchJson(url), 3600);
|
|
2152
|
+
}
|
|
2153
|
+
return { status: "ok", entries: urls.length };
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
async function syncDataset(dataset) {
|
|
2157
|
+
if (!DATASETS[dataset]) {
|
|
2158
|
+
throw new Error(`Неизвестный слой: ${dataset}`);
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
const payload = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
2162
|
+
const items = normalizeItems(payload);
|
|
2163
|
+
saveLocalRecords(dataset, items);
|
|
2164
|
+
recordSyncRun(dataset, items.length, "ok", "");
|
|
2165
|
+
return { dataset, records: items.length, status: "ok", message: "" };
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2168
|
+
recordSyncRun(dataset, 0, "error", message);
|
|
2169
|
+
return { dataset, records: 0, status: "error", message };
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function saveLocalRecords(dataset, items) {
|
|
2174
|
+
initDatabase();
|
|
2175
|
+
const db = openDatabase();
|
|
2176
|
+
try {
|
|
2177
|
+
db.prepare("DELETE FROM local_records WHERE dataset = ?").run(dataset);
|
|
2178
|
+
db.prepare("DELETE FROM local_records_fts WHERE dataset = ?").run(dataset);
|
|
2179
|
+
const insert = db.prepare("INSERT INTO local_records(dataset, record_key, record_json, searchable_text, synced_at) VALUES (?, ?, ?, ?, datetime('now'))");
|
|
2180
|
+
const insertFts = db.prepare("INSERT INTO local_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
|
|
2181
|
+
for (const item of items) {
|
|
2182
|
+
const summary = selectPublicSummary(item);
|
|
2183
|
+
const key = String(summary.inn || item.id || `${dataset}-${Math.random()}`);
|
|
2184
|
+
const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
|
|
2185
|
+
insert.run(dataset, key, JSON.stringify(item), text);
|
|
2186
|
+
insertFts.run(dataset, key, text);
|
|
2187
|
+
}
|
|
2188
|
+
} finally {
|
|
2189
|
+
db.close();
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function recordSyncRun(dataset, records, status, message) {
|
|
2194
|
+
initDatabase();
|
|
2195
|
+
const db = openDatabase();
|
|
2196
|
+
try {
|
|
2197
|
+
db.prepare("INSERT INTO sync_runs(dataset, records, status, message) VALUES (?, ?, ?, ?)").run(dataset, records, status, message);
|
|
2198
|
+
} finally {
|
|
2199
|
+
db.close();
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
async function ensureLocalData() {
|
|
2204
|
+
const status = getDbStatus();
|
|
2205
|
+
if (Number(status.local_records || 0) === 0) {
|
|
2206
|
+
await handleSync([]);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function searchLocalRecords(query, options = {}) {
|
|
2211
|
+
initDatabase();
|
|
2212
|
+
const db = openDatabase();
|
|
2213
|
+
const dataset = options.dataset || "all";
|
|
2214
|
+
const limit = Number(options.limit || 20);
|
|
2215
|
+
try {
|
|
2216
|
+
const params = [];
|
|
2217
|
+
let sql = "SELECT dataset, record_json FROM local_records";
|
|
2218
|
+
const where = [];
|
|
2219
|
+
if (dataset !== "all") {
|
|
2220
|
+
where.push("dataset = ?");
|
|
2221
|
+
params.push(dataset);
|
|
2222
|
+
}
|
|
2223
|
+
if (query) {
|
|
2224
|
+
where.push("searchable_text LIKE ?");
|
|
2225
|
+
params.push(`%${query.toLocaleLowerCase("ru-RU")}%`);
|
|
2226
|
+
}
|
|
2227
|
+
if (where.length) sql += ` WHERE ${where.join(" AND ")}`;
|
|
2228
|
+
sql += " ORDER BY dataset, record_key LIMIT ?";
|
|
2229
|
+
params.push(limit);
|
|
2230
|
+
return db.prepare(sql).all(...params).map((row) => selectPublicSummary(JSON.parse(row.record_json)));
|
|
2231
|
+
} finally {
|
|
2232
|
+
db.close();
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function saveView(name, dataset, args) {
|
|
2237
|
+
initDatabase();
|
|
2238
|
+
const db = openDatabase();
|
|
2239
|
+
try {
|
|
2240
|
+
db.prepare(`
|
|
2241
|
+
INSERT INTO saved_views(name, dataset, query_json) VALUES (?, ?, ?)
|
|
2242
|
+
ON CONFLICT(name) DO UPDATE SET dataset = excluded.dataset, query_json = excluded.query_json
|
|
2243
|
+
`).run(name, dataset, JSON.stringify({ args }));
|
|
2244
|
+
} finally {
|
|
2245
|
+
db.close();
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function listSavedViews() {
|
|
2250
|
+
initDatabase();
|
|
2251
|
+
const db = openDatabase();
|
|
2252
|
+
try {
|
|
2253
|
+
return db.prepare("SELECT name, dataset, created_at FROM saved_views ORDER BY name").all();
|
|
2254
|
+
} finally {
|
|
2255
|
+
db.close();
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function getSavedView(name) {
|
|
2260
|
+
initDatabase();
|
|
2261
|
+
const db = openDatabase();
|
|
2262
|
+
try {
|
|
2263
|
+
const row = db.prepare("SELECT * FROM saved_views WHERE name = ?").get(name);
|
|
2264
|
+
if (!row) throw new Error(`View не найден: ${name}`);
|
|
2265
|
+
return row;
|
|
2266
|
+
} finally {
|
|
2267
|
+
db.close();
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function deleteSavedView(name) {
|
|
2272
|
+
if (!name) {
|
|
2273
|
+
throw new Error("Имя view обязательно.");
|
|
2274
|
+
}
|
|
2275
|
+
initDatabase();
|
|
2276
|
+
const db = openDatabase();
|
|
2277
|
+
try {
|
|
2278
|
+
db.prepare("DELETE FROM saved_views WHERE name = ?").run(name);
|
|
2279
|
+
} finally {
|
|
2280
|
+
db.close();
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function getLocalSummaryRows(dataset) {
|
|
2285
|
+
const rows = searchLocalRecords("", { dataset, limit: 1000 });
|
|
2286
|
+
return [
|
|
2287
|
+
{ metric: "records", value: rows.length },
|
|
2288
|
+
{ metric: "with_phone", value: rows.filter((row) => row.phone && row.phone !== "-").length },
|
|
2289
|
+
{ metric: "with_email", value: rows.filter((row) => row.email).length },
|
|
2290
|
+
{ metric: "with_website", value: rows.filter((row) => row.website).length },
|
|
2291
|
+
];
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
function exportDbSnapshot() {
|
|
2295
|
+
initDatabase();
|
|
2296
|
+
return {
|
|
2297
|
+
db: getDbStatus(),
|
|
2298
|
+
views: listSavedViews(),
|
|
2299
|
+
aliases: listAliases(),
|
|
2300
|
+
features: listFeatures(),
|
|
2301
|
+
sessions: listSessions(100),
|
|
2302
|
+
history: listHistory(100),
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
function listAliases() {
|
|
2307
|
+
initDatabase();
|
|
2308
|
+
const db = openDatabase();
|
|
2309
|
+
try {
|
|
2310
|
+
return db.prepare("SELECT name, command FROM aliases ORDER BY name").all();
|
|
2311
|
+
} finally {
|
|
2312
|
+
db.close();
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function getAlias(name) {
|
|
2317
|
+
try {
|
|
2318
|
+
initDatabase();
|
|
2319
|
+
const db = openDatabase();
|
|
2320
|
+
try {
|
|
2321
|
+
return db.prepare("SELECT name, command FROM aliases WHERE name = ?").get(name);
|
|
2322
|
+
} finally {
|
|
2323
|
+
db.close();
|
|
2324
|
+
}
|
|
2325
|
+
} catch {
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function saveAlias(name, command) {
|
|
2331
|
+
initDatabase();
|
|
2332
|
+
const db = openDatabase();
|
|
2333
|
+
try {
|
|
2334
|
+
db.prepare("INSERT INTO aliases(name, command) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET command = excluded.command").run(name, command);
|
|
2335
|
+
} finally {
|
|
2336
|
+
db.close();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function deleteAlias(name) {
|
|
2341
|
+
initDatabase();
|
|
2342
|
+
const db = openDatabase();
|
|
2343
|
+
try {
|
|
2344
|
+
db.prepare("DELETE FROM aliases WHERE name = ?").run(name);
|
|
2345
|
+
} finally {
|
|
2346
|
+
db.close();
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function inferCommandFromText(text) {
|
|
2351
|
+
const normalized = text.toLocaleLowerCase("ru-RU");
|
|
2352
|
+
const dataset = normalized.includes("сад") ? "kindergartens" : "schools";
|
|
2353
|
+
const command = ["data", dataset];
|
|
2354
|
+
const street = normalized.match(/(?:на|по|улица|ул\.?)\s+([а-яёa-z-]+)/iu)?.[1];
|
|
2355
|
+
if (street) command.push("--where", `address=${street}`);
|
|
2356
|
+
if (normalized.includes("csv")) command.push("--format", "csv");
|
|
2357
|
+
if (normalized.includes("json")) command.push("--format", "json");
|
|
2358
|
+
const output = text.match(/(?:в файл|файл)\s+([^\s]+)/iu)?.[1];
|
|
2359
|
+
if (output) command.push("--output", output);
|
|
2360
|
+
return command;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
async function outputData(value, options, format) {
|
|
2364
|
+
const text = format === "csv" ? toCsv(value) : `${JSON.stringify(value, null, 2)}\n`;
|
|
2365
|
+
if (options.output) {
|
|
2366
|
+
await writeFile(options.output, text, "utf8");
|
|
2367
|
+
console.log(`Файл сохранен: ${options.output}`);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
process.stdout.write(text);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function toCsv(rows) {
|
|
2374
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
2375
|
+
if (list.length === 0) return "";
|
|
2376
|
+
const columns = [...new Set(list.flatMap((row) => Object.keys(row)))];
|
|
2377
|
+
return `${columns.map(csvCell).join(",")}\n${list.map((row) => columns.map((column) => csvCell(row[column])).join(",")).join("\n")}\n`;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
1479
2380
|
function assertKeyProvider(provider) {
|
|
1480
2381
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
1481
2382
|
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
@@ -1565,20 +2466,43 @@ async function aiAsk(args, context = {}) {
|
|
|
1565
2466
|
|
|
1566
2467
|
const config = await loadConfig();
|
|
1567
2468
|
const providerConfig = resolveAiProfile(config, options);
|
|
2469
|
+
applyRuntimeConfig(providerConfig, options.config);
|
|
1568
2470
|
const dataContext = await buildDataContext(question);
|
|
1569
|
-
|
|
2471
|
+
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
2472
|
+
const historyEnabled = !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
2473
|
+
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
2474
|
+
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
2475
|
+
const messages = buildAiMessages(question, dataContext, history, options);
|
|
1570
2476
|
let answer = "";
|
|
1571
2477
|
let errorMessage = "";
|
|
1572
2478
|
|
|
1573
2479
|
try {
|
|
2480
|
+
emitEvent(options, "provider_selected", { profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model });
|
|
1574
2481
|
answer = await callAiProvider(providerConfig, messages);
|
|
1575
2482
|
} catch (error) {
|
|
1576
2483
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1577
|
-
|
|
2484
|
+
if (historyEnabled) {
|
|
2485
|
+
recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage, sessionId });
|
|
2486
|
+
appendSessionExchange(sessionId, question, "", dataContext, errorMessage);
|
|
2487
|
+
}
|
|
1578
2488
|
throw error;
|
|
1579
2489
|
}
|
|
1580
2490
|
|
|
1581
|
-
|
|
2491
|
+
if (historyEnabled) {
|
|
2492
|
+
recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
|
|
2493
|
+
appendSessionExchange(sessionId, question, answer, dataContext, "");
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
2497
|
+
|
|
2498
|
+
if (options.output) {
|
|
2499
|
+
await writeFile(options.output, answer, "utf8");
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (options.format === "json" || options.schema === "json") {
|
|
2503
|
+
printJson({ answer, profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model, sessionId, context: dataContext });
|
|
2504
|
+
return answer;
|
|
2505
|
+
}
|
|
1582
2506
|
|
|
1583
2507
|
console.log(answer);
|
|
1584
2508
|
return answer;
|
|
@@ -1601,9 +2525,28 @@ function resolveAiProfile(config, options = {}) {
|
|
|
1601
2525
|
provider,
|
|
1602
2526
|
model: options.model || activeProfile.model || config.ai.model,
|
|
1603
2527
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
2528
|
+
temperature: options.temperature || activeProfile.temperature,
|
|
1604
2529
|
};
|
|
1605
2530
|
}
|
|
1606
2531
|
|
|
2532
|
+
function applyRuntimeConfig(target, value) {
|
|
2533
|
+
if (!value) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const [key, ...parts] = String(value).split("=");
|
|
2537
|
+
if (!key || parts.length === 0) {
|
|
2538
|
+
throw new Error("Флаг --config должен быть в формате key=value.");
|
|
2539
|
+
}
|
|
2540
|
+
setConfigValue(target, key, parts.join("="));
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
function emitEvent(options, type, data) {
|
|
2544
|
+
if (!options.events) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
printJson({ type, at: new Date().toISOString(), ...data });
|
|
2548
|
+
}
|
|
2549
|
+
|
|
1607
2550
|
async function buildDataContext(question) {
|
|
1608
2551
|
const apiBaseUrl = await getApiBaseUrl();
|
|
1609
2552
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
@@ -1726,7 +2669,7 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
1726
2669
|
return score;
|
|
1727
2670
|
}
|
|
1728
2671
|
|
|
1729
|
-
function buildAiMessages(question, dataContext, history) {
|
|
2672
|
+
function buildAiMessages(question, dataContext, history, options = {}) {
|
|
1730
2673
|
const sourceLines = buildSourceLines(dataContext);
|
|
1731
2674
|
const system = [
|
|
1732
2675
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
@@ -1735,8 +2678,10 @@ function buildAiMessages(question, dataContext, history) {
|
|
|
1735
2678
|
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
1736
2679
|
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
1737
2680
|
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
2681
|
+
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
2682
|
+
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
1738
2683
|
"Отвечай кратко и по делу.",
|
|
1739
|
-
].join(" ");
|
|
2684
|
+
].filter(Boolean).join(" ");
|
|
1740
2685
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
1741
2686
|
const recentHistory = history.slice(-6);
|
|
1742
2687
|
|
|
@@ -1862,7 +2807,7 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
1862
2807
|
body: JSON.stringify({
|
|
1863
2808
|
model: config.model,
|
|
1864
2809
|
messages,
|
|
1865
|
-
temperature: 0.2,
|
|
2810
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
1866
2811
|
}),
|
|
1867
2812
|
});
|
|
1868
2813
|
|
|
@@ -1966,20 +2911,26 @@ async function listDataset(dataset, args) {
|
|
|
1966
2911
|
params.set("limit", options.limit || "20");
|
|
1967
2912
|
params.set("offset", options.offset || "0");
|
|
1968
2913
|
|
|
1969
|
-
const data =
|
|
1970
|
-
|
|
2914
|
+
const data = options.local
|
|
2915
|
+
? searchLocalRecords(options.search || "", { dataset, limit: Number(options.limit || 20) })
|
|
2916
|
+
: normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
|
|
2917
|
+
const items = data;
|
|
1971
2918
|
const filtered = applyDatasetFilters(items, options);
|
|
1972
2919
|
const limited = filtered.slice(0, Number(options.limit || 20));
|
|
1973
2920
|
const summarized = limited.map(selectPublicSummary);
|
|
1974
2921
|
const projected = projectColumns(summarized, options.columns);
|
|
2922
|
+
if (options.save) {
|
|
2923
|
+
saveView(options.save, dataset, args.filter((arg) => arg !== "--save" && arg !== options.save));
|
|
2924
|
+
console.log(`View сохранен: ${options.save}`);
|
|
2925
|
+
}
|
|
1975
2926
|
|
|
1976
2927
|
if (options.json || options.format === "json") {
|
|
1977
|
-
|
|
2928
|
+
await outputData(projected, options, "json");
|
|
1978
2929
|
return;
|
|
1979
2930
|
}
|
|
1980
2931
|
|
|
1981
2932
|
if (options.format === "csv") {
|
|
1982
|
-
|
|
2933
|
+
await outputData(projected, options, "csv");
|
|
1983
2934
|
return;
|
|
1984
2935
|
}
|
|
1985
2936
|
|
|
@@ -2014,27 +2965,32 @@ async function searchAll(args) {
|
|
|
2014
2965
|
throw new Error('Search text is required. Example: iola search "лицей"');
|
|
2015
2966
|
}
|
|
2016
2967
|
|
|
2017
|
-
const [schools, kindergartens] = await Promise.all([
|
|
2018
|
-
fetchJson(`${await getApiBaseUrl()}/schools?limit=100&offset=0`),
|
|
2019
|
-
fetchJson(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`),
|
|
2020
|
-
]);
|
|
2021
|
-
|
|
2022
2968
|
const limit = Number(options.limit || 5);
|
|
2969
|
+
const [schools, kindergartens] = options.local
|
|
2970
|
+
? [
|
|
2971
|
+
searchLocalRecords(query, { dataset: "schools", limit }),
|
|
2972
|
+
searchLocalRecords(query, { dataset: "kindergartens", limit }),
|
|
2973
|
+
]
|
|
2974
|
+
: await Promise.all([
|
|
2975
|
+
fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
|
|
2976
|
+
fetchJsonMaybeCached(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`, options),
|
|
2977
|
+
]);
|
|
2978
|
+
|
|
2023
2979
|
const result = {
|
|
2024
2980
|
schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
2025
2981
|
kindergartens: projectColumns(filterItems(normalizeItems(kindergartens), query).slice(0, limit).map(selectPublicSummary), options.columns),
|
|
2026
2982
|
};
|
|
2027
2983
|
|
|
2028
2984
|
if (options.json || options.format === "json") {
|
|
2029
|
-
|
|
2985
|
+
await outputData(result, options, "json");
|
|
2030
2986
|
return;
|
|
2031
2987
|
}
|
|
2032
2988
|
|
|
2033
2989
|
if (options.format === "csv") {
|
|
2034
|
-
|
|
2990
|
+
await outputData([
|
|
2035
2991
|
...result.schools.map((item) => ({ layer: "schools", ...item })),
|
|
2036
2992
|
...result.kindergartens.map((item) => ({ layer: "kindergartens", ...item })),
|
|
2037
|
-
]);
|
|
2993
|
+
], options, "csv");
|
|
2038
2994
|
return;
|
|
2039
2995
|
}
|
|
2040
2996
|
|
|
@@ -2052,9 +3008,9 @@ async function setupClient(args) {
|
|
|
2052
3008
|
throw new Error('Only "iola setup codex" is available in this first release.');
|
|
2053
3009
|
}
|
|
2054
3010
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
console.log("
|
|
3011
|
+
await runCommand("codex", ["mcp", "add", "yoshkarOlaPublicData", "--url", `${await getMcpBaseUrl()}/mcp`], { inherit: true });
|
|
3012
|
+
await runCommand("npx", ["-y", "@iola_adm/yoshkar-ola-public-mcp", "install-skill", "codex"], { inherit: true });
|
|
3013
|
+
console.log("Codex MCP и skill установлены.");
|
|
2058
3014
|
}
|
|
2059
3015
|
|
|
2060
3016
|
function parseOptions(args) {
|
|
@@ -2062,12 +3018,12 @@ function parseOptions(args) {
|
|
|
2062
3018
|
|
|
2063
3019
|
for (let index = 0; index < args.length; index += 1) {
|
|
2064
3020
|
const arg = args[index];
|
|
2065
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent") {
|
|
3021
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache") {
|
|
2066
3022
|
result[arg.slice(2)] = true;
|
|
2067
3023
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
2068
3024
|
result.check = true;
|
|
2069
3025
|
result[arg.slice(2)] = true;
|
|
2070
|
-
} 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") {
|
|
3026
|
+
} 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" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save") {
|
|
2071
3027
|
result[arg.slice(2)] = args[index + 1];
|
|
2072
3028
|
index += 1;
|
|
2073
3029
|
} else {
|