@iola_adm/iola-cli 0.1.12 → 0.1.14
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 +79 -0
- package/bin/iola.js +1 -2
- package/package.json +6 -5
- package/src/cli.js +679 -12
package/README.md
CHANGED
|
@@ -87,12 +87,26 @@ iola agent
|
|
|
87
87
|
iola chat
|
|
88
88
|
iola init
|
|
89
89
|
iola doctor
|
|
90
|
+
iola db status
|
|
91
|
+
iola db init
|
|
92
|
+
iola history --limit 20
|
|
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
|
|
90
102
|
iola config get
|
|
91
103
|
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
92
104
|
iola config reset
|
|
93
105
|
iola update
|
|
94
106
|
iola version --check
|
|
95
107
|
iola ask "Найди школу 29"
|
|
108
|
+
iola ask "Найди школу 29" --profile codex --events --output answer.txt
|
|
109
|
+
iola ask "Найди школу 29" --schema json --no-history
|
|
96
110
|
iola data schools --limit 10
|
|
97
111
|
iola data kindergartens --search "29"
|
|
98
112
|
iola data schools --where address=Петрова --columns name,address,phone
|
|
@@ -139,6 +153,11 @@ iola agent
|
|
|
139
153
|
/help
|
|
140
154
|
/health
|
|
141
155
|
/doctor
|
|
156
|
+
/db status
|
|
157
|
+
/sessions
|
|
158
|
+
/resume 1
|
|
159
|
+
/features list
|
|
160
|
+
/mcp status
|
|
142
161
|
/config get
|
|
143
162
|
/layers
|
|
144
163
|
/data schools --limit 10
|
|
@@ -160,6 +179,7 @@ iola agent
|
|
|
160
179
|
/provider
|
|
161
180
|
/config
|
|
162
181
|
/history
|
|
182
|
+
/history --limit 20
|
|
163
183
|
/clear
|
|
164
184
|
/update
|
|
165
185
|
/init
|
|
@@ -271,6 +291,65 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
271
291
|
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter,
|
|
272
292
|
интерактивному агентному режиму, экспорту данных и проверке обновлений.
|
|
273
293
|
|
|
294
|
+
## Локальная SQLite-БД
|
|
295
|
+
|
|
296
|
+
CLI использует встроенный `node:sqlite` и хранит локальную БД в профиле
|
|
297
|
+
пользователя:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
%USERPROFILE%\.iola\iola.db
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
БД создается автоматически при установке npm-пакета и при `iola init`.
|
|
304
|
+
В ней хранятся история AI-запросов, контекст ответа, ошибки выполнения,
|
|
305
|
+
служебная таблица версии схемы, а также подготовлены таблицы для кеша API и
|
|
306
|
+
сохраненных выборок.
|
|
307
|
+
|
|
308
|
+
Команды:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
iola db status
|
|
312
|
+
iola db init
|
|
313
|
+
iola db reset
|
|
314
|
+
iola history --limit 20
|
|
315
|
+
iola history --json
|
|
316
|
+
iola history clear
|
|
317
|
+
iola sessions --limit 20
|
|
318
|
+
iola sessions clear
|
|
319
|
+
iola resume 1 "продолжи"
|
|
320
|
+
iola fork 1 "новая ветка разговора"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Ключи OpenAI/OpenRouter в SQLite не сохраняются. Они остаются в локальном
|
|
324
|
+
`secrets.json` или в переменных окружения.
|
|
325
|
+
|
|
326
|
+
## Feature flags, MCP и машинный вывод
|
|
327
|
+
|
|
328
|
+
Экспериментальные и системные возможности можно включать отдельно:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
iola features list
|
|
332
|
+
iola features enable api-cache
|
|
333
|
+
iola features disable sqlite-history
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
MCP-интеграции:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
iola mcp status
|
|
340
|
+
iola mcp list
|
|
341
|
+
iola mcp install codex
|
|
342
|
+
iola mcp remove codex
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Для автоматизации доступны события, JSON-ответ и запись результата в файл:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
iola ask "Найди школу 29" --events
|
|
349
|
+
iola ask "Найди школу 29" --schema json
|
|
350
|
+
iola ask "Найди школу 29" --output answer.txt
|
|
351
|
+
```
|
|
352
|
+
|
|
274
353
|
## Переменные окружения
|
|
275
354
|
|
|
276
355
|
```bash
|
package/bin/iola.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings
|
|
2
2
|
|
|
3
3
|
import { main } from "../src/cli.js";
|
|
4
4
|
|
|
@@ -6,4 +6,3 @@ main(process.argv.slice(2)).catch((error) => {
|
|
|
6
6
|
console.error(error instanceof Error ? error.message : String(error));
|
|
7
7
|
process.exitCode = 1;
|
|
8
8
|
});
|
|
9
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iola_adm/iola-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/adm-iola/iola-cli#readme",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"iola": "bin/iola.js"
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"postinstall": "node --no-warnings bin/iola.js db init --silent || true",
|
|
20
|
+
"start": "node --no-warnings bin/iola.js",
|
|
21
|
+
"test": "node --no-warnings --check bin/iola.js && node --no-warnings --check src/cli.js"
|
|
22
|
+
},
|
|
18
23
|
"files": [
|
|
19
24
|
"bin",
|
|
20
25
|
"src",
|
|
@@ -25,10 +30,6 @@
|
|
|
25
30
|
"publishConfig": {
|
|
26
31
|
"access": "public"
|
|
27
32
|
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"start": "node bin/iola.js",
|
|
30
|
-
"test": "node --check bin/iola.js && node --check src/cli.js"
|
|
31
|
-
},
|
|
32
33
|
"keywords": [
|
|
33
34
|
"yoshkar-ola",
|
|
34
35
|
"open-data",
|
package/src/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
2
3
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import readline from "node:readline/promises";
|
|
6
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
9
|
|
|
8
10
|
const API_BASE_URL = process.env.IOLA_API_BASE_URL || "https://apiiola.yasg.ru/api/v1";
|
|
9
11
|
const MCP_BASE_URL = process.env.IOLA_MCP_BASE_URL || "https://apiiola.yasg.ru";
|
|
@@ -11,6 +13,16 @@ const MIN_NODE_VERSION = "22.5.0";
|
|
|
11
13
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
12
14
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
13
15
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
16
|
+
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
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
|
+
};
|
|
14
26
|
const DEFAULT_AI_CONFIG = {
|
|
15
27
|
api: {
|
|
16
28
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -76,6 +88,13 @@ const COMMANDS = new Map([
|
|
|
76
88
|
["version", showVersion],
|
|
77
89
|
["update", checkUpdate],
|
|
78
90
|
["doctor", doctor],
|
|
91
|
+
["db", handleDb],
|
|
92
|
+
["history", handleHistory],
|
|
93
|
+
["sessions", handleSessions],
|
|
94
|
+
["resume", resumeSession],
|
|
95
|
+
["fork", forkSession],
|
|
96
|
+
["features", handleFeatures],
|
|
97
|
+
["mcp", handleMcp],
|
|
79
98
|
["config", handleConfig],
|
|
80
99
|
["banner", showBanner],
|
|
81
100
|
["agent", startAgent],
|
|
@@ -119,12 +138,21 @@ Usage:
|
|
|
119
138
|
iola chat
|
|
120
139
|
iola init
|
|
121
140
|
iola doctor
|
|
141
|
+
iola db status
|
|
142
|
+
iola db init
|
|
143
|
+
iola history [--limit 20]
|
|
144
|
+
iola history clear
|
|
145
|
+
iola sessions [--limit 20]
|
|
146
|
+
iola resume SESSION_ID [TEXT]
|
|
147
|
+
iola fork SESSION_ID [TEXT]
|
|
148
|
+
iola features list|enable|disable
|
|
149
|
+
iola mcp list|status|install|remove
|
|
122
150
|
iola config get
|
|
123
151
|
iola config set api.baseUrl URL
|
|
124
152
|
iola config set api.mcpBaseUrl URL
|
|
125
153
|
iola config reset
|
|
126
154
|
iola update
|
|
127
|
-
iola ask TEXT
|
|
155
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
128
156
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
129
157
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
130
158
|
iola ai context TEXT [--json]
|
|
@@ -225,7 +253,41 @@ async function handleAgentLine(line, state) {
|
|
|
225
253
|
}
|
|
226
254
|
|
|
227
255
|
if (command === "history") {
|
|
228
|
-
|
|
256
|
+
if (args.length > 0) {
|
|
257
|
+
await handleHistory(args);
|
|
258
|
+
} else {
|
|
259
|
+
printAgentHistory(state.history);
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (command === "db") {
|
|
265
|
+
await handleDb(args);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (command === "sessions") {
|
|
270
|
+
await handleSessions(args);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (command === "resume") {
|
|
275
|
+
await resumeSession(args);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (command === "fork") {
|
|
280
|
+
await forkSession(args);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (command === "features") {
|
|
285
|
+
await handleFeatures(args);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (command === "mcp") {
|
|
290
|
+
await handleMcp(args);
|
|
229
291
|
return false;
|
|
230
292
|
}
|
|
231
293
|
|
|
@@ -307,6 +369,13 @@ async function handleAgentLine(line, state) {
|
|
|
307
369
|
const mapped = {
|
|
308
370
|
health: ["health", args],
|
|
309
371
|
doctor: ["doctor", args],
|
|
372
|
+
db: ["db", args],
|
|
373
|
+
history: ["history", args],
|
|
374
|
+
sessions: ["sessions", args],
|
|
375
|
+
resume: ["resume", args],
|
|
376
|
+
fork: ["fork", args],
|
|
377
|
+
features: ["features", args],
|
|
378
|
+
mcp: ["mcp", args],
|
|
310
379
|
config: ["config", args],
|
|
311
380
|
layers: ["layers", args],
|
|
312
381
|
data: ["data", args],
|
|
@@ -333,6 +402,11 @@ function printAgentHelp() {
|
|
|
333
402
|
/help
|
|
334
403
|
/health
|
|
335
404
|
/doctor
|
|
405
|
+
/db status
|
|
406
|
+
/sessions
|
|
407
|
+
/resume SESSION_ID
|
|
408
|
+
/features list
|
|
409
|
+
/mcp status
|
|
336
410
|
/config get
|
|
337
411
|
/config set api.baseUrl URL
|
|
338
412
|
/layers
|
|
@@ -357,6 +431,7 @@ function printAgentHelp() {
|
|
|
357
431
|
/provider
|
|
358
432
|
/model
|
|
359
433
|
/history
|
|
434
|
+
/history --limit 20
|
|
360
435
|
/clear
|
|
361
436
|
/banner
|
|
362
437
|
/update
|
|
@@ -474,6 +549,7 @@ async function doctor(args = []) {
|
|
|
474
549
|
nodeRequired: `>=${MIN_NODE_VERSION}`,
|
|
475
550
|
nodeStatus: getNodeRequirementStatus().ok ? "ok" : "upgrade-required",
|
|
476
551
|
},
|
|
552
|
+
db: getDbStatus(),
|
|
477
553
|
api: {
|
|
478
554
|
baseUrl: apiBaseUrl,
|
|
479
555
|
mcpBaseUrl,
|
|
@@ -496,9 +572,26 @@ async function doctor(args = []) {
|
|
|
496
572
|
return;
|
|
497
573
|
}
|
|
498
574
|
|
|
575
|
+
if (options.summary) {
|
|
576
|
+
printTable([
|
|
577
|
+
{ group: "cli", status: report.cli.nodeStatus === "ok" && report.cli.update !== "available" ? "ok" : "check" },
|
|
578
|
+
{ group: "sqlite", status: report.db.status },
|
|
579
|
+
{ group: "api", status: report.api.health },
|
|
580
|
+
{ group: "ai", status: report.ai.provider },
|
|
581
|
+
{ group: "ollama", status: report.ai.ollama },
|
|
582
|
+
], [
|
|
583
|
+
["group", "Группа"],
|
|
584
|
+
["status", "Статус"],
|
|
585
|
+
]);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
499
589
|
console.log("CLI");
|
|
500
590
|
printKeyValue(report.cli);
|
|
501
591
|
console.log("");
|
|
592
|
+
console.log("SQLite");
|
|
593
|
+
printKeyValue(report.db);
|
|
594
|
+
console.log("");
|
|
502
595
|
console.log("API/MCP");
|
|
503
596
|
printKeyValue(report.api);
|
|
504
597
|
console.log("");
|
|
@@ -506,6 +599,11 @@ async function doctor(args = []) {
|
|
|
506
599
|
printKeyValue(report.ai);
|
|
507
600
|
console.log("");
|
|
508
601
|
printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
|
|
602
|
+
if (options.all) {
|
|
603
|
+
console.log("");
|
|
604
|
+
console.log("Фичи");
|
|
605
|
+
await handleFeatures(["list"]);
|
|
606
|
+
}
|
|
509
607
|
}
|
|
510
608
|
|
|
511
609
|
function getUpdateStatus(current, latest) {
|
|
@@ -615,6 +713,8 @@ async function initCli(args = []) {
|
|
|
615
713
|
|
|
616
714
|
showBanner();
|
|
617
715
|
console.log("Проверка окружения");
|
|
716
|
+
initDatabase();
|
|
717
|
+
const dbStatus = getDbStatus();
|
|
618
718
|
printKeyValue({
|
|
619
719
|
node: process.version,
|
|
620
720
|
node_required: `>=${MIN_NODE_VERSION}`,
|
|
@@ -622,6 +722,8 @@ async function initCli(args = []) {
|
|
|
622
722
|
npm: await getCommandVersion("npm", ["--version"]),
|
|
623
723
|
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
624
724
|
mcp: await getMcpBaseUrl(),
|
|
725
|
+
sqlite: dbStatus.status,
|
|
726
|
+
sqlite_file: dbStatus.file,
|
|
625
727
|
});
|
|
626
728
|
console.log("");
|
|
627
729
|
|
|
@@ -763,6 +865,187 @@ async function handleConfig(args) {
|
|
|
763
865
|
throw new Error("Команды config: get, set, reset.");
|
|
764
866
|
}
|
|
765
867
|
|
|
868
|
+
async function handleDb(args) {
|
|
869
|
+
const [action = "status"] = args;
|
|
870
|
+
const options = parseOptions(args);
|
|
871
|
+
|
|
872
|
+
if (action === "init") {
|
|
873
|
+
initDatabase();
|
|
874
|
+
if (!options.silent) {
|
|
875
|
+
console.log(`SQLite-БД готова: ${DB_FILE}`);
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (action === "status") {
|
|
881
|
+
printKeyValue(getDbStatus());
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (action === "reset") {
|
|
886
|
+
const shouldReset = await confirm("Удалить локальную SQLite-БД iola.db? [y/N] ");
|
|
887
|
+
if (!shouldReset) {
|
|
888
|
+
console.log("Сброс отменен.");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
await rm(DB_FILE, { force: true });
|
|
892
|
+
initDatabase();
|
|
893
|
+
console.log(`SQLite-БД пересоздана: ${DB_FILE}`);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
throw new Error("Команды db: status, init, reset.");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function handleHistory(args) {
|
|
901
|
+
const [action] = args;
|
|
902
|
+
const options = parseOptions(args);
|
|
903
|
+
|
|
904
|
+
if (action === "clear") {
|
|
905
|
+
clearHistory();
|
|
906
|
+
console.log("История очищена.");
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const rows = listHistory(Number(options.limit || 20));
|
|
911
|
+
|
|
912
|
+
if (options.json) {
|
|
913
|
+
printJson(rows);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
printTable(rows, [
|
|
918
|
+
["id", "ID"],
|
|
919
|
+
["created_at", "Дата"],
|
|
920
|
+
["profile", "Профиль"],
|
|
921
|
+
["provider", "Провайдер"],
|
|
922
|
+
["question", "Вопрос"],
|
|
923
|
+
["answer", "Ответ"],
|
|
924
|
+
]);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function handleSessions(args) {
|
|
928
|
+
const [action] = args;
|
|
929
|
+
if (action === "clear") {
|
|
930
|
+
clearSessions();
|
|
931
|
+
console.log("Сессии очищены.");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const options = parseOptions(args);
|
|
936
|
+
const rows = listSessions(Number(options.limit || 20));
|
|
937
|
+
|
|
938
|
+
if (options.json) {
|
|
939
|
+
printJson(rows);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
printTable(rows, [
|
|
944
|
+
["id", "ID"],
|
|
945
|
+
["updated_at", "Обновлена"],
|
|
946
|
+
["profile", "Профиль"],
|
|
947
|
+
["provider", "Провайдер"],
|
|
948
|
+
["model", "Модель"],
|
|
949
|
+
["messages", "Сообщ."],
|
|
950
|
+
["title", "Название"],
|
|
951
|
+
]);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function resumeSession(args) {
|
|
955
|
+
const [sessionId, ...questionParts] = args;
|
|
956
|
+
if (!sessionId) {
|
|
957
|
+
throw new Error("SESSION_ID обязателен. Пример: iola resume 1 \"продолжи\"");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const question = questionParts.join(" ").trim();
|
|
961
|
+
if (!question) {
|
|
962
|
+
printSessionMessages(Number(sessionId));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const session = getSession(Number(sessionId));
|
|
967
|
+
await aiAsk([question, "--session", sessionId, "--profile", session.profile || "local"]);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function forkSession(args) {
|
|
971
|
+
const [sessionId, ...questionParts] = args;
|
|
972
|
+
if (!sessionId) {
|
|
973
|
+
throw new Error("SESSION_ID обязателен. Пример: iola fork 1 \"новый вопрос\"");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const forkedId = forkSessionInDb(Number(sessionId));
|
|
977
|
+
console.log(`Создана новая сессия: ${forkedId}`);
|
|
978
|
+
const question = questionParts.join(" ").trim();
|
|
979
|
+
if (question) {
|
|
980
|
+
const session = getSession(forkedId);
|
|
981
|
+
await aiAsk([question, "--session", String(forkedId), "--profile", session.profile || "local"]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function handleFeatures(args) {
|
|
986
|
+
const [action = "list", name] = args;
|
|
987
|
+
|
|
988
|
+
if (action === "list" || action === "ls") {
|
|
989
|
+
const rows = listFeatures();
|
|
990
|
+
printTable(rows, [
|
|
991
|
+
["name", "Фича"],
|
|
992
|
+
["enabled", "Вкл"],
|
|
993
|
+
["stage", "Стадия"],
|
|
994
|
+
["description", "Описание"],
|
|
995
|
+
]);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (action === "enable" || action === "disable") {
|
|
1000
|
+
if (!name || !FEATURES[name]) {
|
|
1001
|
+
throw new Error(`Неизвестная фича. Доступно: ${Object.keys(FEATURES).join(", ")}`);
|
|
1002
|
+
}
|
|
1003
|
+
setFeatureEnabled(name, action === "enable");
|
|
1004
|
+
console.log(`${name}: ${action === "enable" ? "enabled" : "disabled"}`);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function handleMcp(args) {
|
|
1012
|
+
const [action = "status", target = "codex"] = args;
|
|
1013
|
+
|
|
1014
|
+
if (action === "status") {
|
|
1015
|
+
const [health, version] = await Promise.all([
|
|
1016
|
+
fetchJson(`${await getMcpBaseUrl()}/mcp-health`),
|
|
1017
|
+
fetchJson(`${await getMcpBaseUrl()}/mcp-version`),
|
|
1018
|
+
]);
|
|
1019
|
+
printKeyValue({
|
|
1020
|
+
endpoint: `${await getMcpBaseUrl()}/mcp`,
|
|
1021
|
+
status: health.status,
|
|
1022
|
+
server_version: version.server_version,
|
|
1023
|
+
layers: version.data_layers?.map((layer) => layer.id).join(", "),
|
|
1024
|
+
});
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (action === "list") {
|
|
1029
|
+
await runCommand("codex", ["mcp", "list"], { inherit: true });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (action === "install" || action === "add") {
|
|
1034
|
+
await setupClient([target]);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (action === "remove" || action === "delete") {
|
|
1039
|
+
if (target !== "codex") {
|
|
1040
|
+
throw new Error("Пока доступно удаление только Codex MCP.");
|
|
1041
|
+
}
|
|
1042
|
+
await runCommand("codex", ["mcp", "remove", "yoshkarOlaPublicData"], { inherit: true });
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
throw new Error("Команды mcp: status, list, install codex, remove codex.");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
766
1049
|
async function aiDoctor(args) {
|
|
767
1050
|
const options = parseOptions(args);
|
|
768
1051
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -1250,6 +1533,335 @@ async function deleteAiKey(provider) {
|
|
|
1250
1533
|
console.log(`Локальный ключ ${provider} удален.`);
|
|
1251
1534
|
}
|
|
1252
1535
|
|
|
1536
|
+
function openDatabase() {
|
|
1537
|
+
const db = new DatabaseSync(DB_FILE);
|
|
1538
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
1539
|
+
return db;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function initDatabase() {
|
|
1543
|
+
mkdirSyncSafe(CONFIG_DIR);
|
|
1544
|
+
const db = openDatabase();
|
|
1545
|
+
try {
|
|
1546
|
+
db.exec(`
|
|
1547
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
1548
|
+
key TEXT PRIMARY KEY,
|
|
1549
|
+
value TEXT NOT NULL
|
|
1550
|
+
);
|
|
1551
|
+
CREATE TABLE IF NOT EXISTS ask_history (
|
|
1552
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1553
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1554
|
+
profile TEXT,
|
|
1555
|
+
provider TEXT,
|
|
1556
|
+
model TEXT,
|
|
1557
|
+
question TEXT NOT NULL,
|
|
1558
|
+
answer TEXT NOT NULL,
|
|
1559
|
+
context_json TEXT NOT NULL,
|
|
1560
|
+
error TEXT
|
|
1561
|
+
);
|
|
1562
|
+
CREATE INDEX IF NOT EXISTS idx_ask_history_created_at ON ask_history(created_at DESC);
|
|
1563
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
1564
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1565
|
+
parent_id INTEGER,
|
|
1566
|
+
title TEXT NOT NULL,
|
|
1567
|
+
profile TEXT,
|
|
1568
|
+
provider TEXT,
|
|
1569
|
+
model TEXT,
|
|
1570
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1571
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1572
|
+
);
|
|
1573
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at DESC);
|
|
1574
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
1575
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1576
|
+
session_id INTEGER NOT NULL,
|
|
1577
|
+
role TEXT NOT NULL,
|
|
1578
|
+
content TEXT NOT NULL,
|
|
1579
|
+
context_json TEXT,
|
|
1580
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1581
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
1582
|
+
);
|
|
1583
|
+
CREATE INDEX IF NOT EXISTS idx_session_messages_session_id ON session_messages(session_id, id);
|
|
1584
|
+
CREATE TABLE IF NOT EXISTS feature_flags (
|
|
1585
|
+
name TEXT PRIMARY KEY,
|
|
1586
|
+
enabled INTEGER NOT NULL,
|
|
1587
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1588
|
+
);
|
|
1589
|
+
CREATE TABLE IF NOT EXISTS api_cache (
|
|
1590
|
+
key TEXT PRIMARY KEY,
|
|
1591
|
+
url TEXT NOT NULL,
|
|
1592
|
+
response_json TEXT NOT NULL,
|
|
1593
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1594
|
+
expires_at TEXT
|
|
1595
|
+
);
|
|
1596
|
+
CREATE TABLE IF NOT EXISTS saved_views (
|
|
1597
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1598
|
+
name TEXT NOT NULL UNIQUE,
|
|
1599
|
+
dataset TEXT NOT NULL,
|
|
1600
|
+
query_json TEXT NOT NULL,
|
|
1601
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1602
|
+
);
|
|
1603
|
+
`);
|
|
1604
|
+
db.prepare(`
|
|
1605
|
+
INSERT INTO meta(key, value) VALUES ('schema_version', ?)
|
|
1606
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1607
|
+
`).run(String(DB_SCHEMA_VERSION));
|
|
1608
|
+
} finally {
|
|
1609
|
+
db.close();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function mkdirSyncSafe(directory) {
|
|
1614
|
+
try {
|
|
1615
|
+
mkdirSync(directory, { recursive: true });
|
|
1616
|
+
} catch {
|
|
1617
|
+
// Directory creation is retried by write operations where needed.
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function getDbStatus() {
|
|
1622
|
+
try {
|
|
1623
|
+
initDatabase();
|
|
1624
|
+
const db = openDatabase();
|
|
1625
|
+
try {
|
|
1626
|
+
const schema = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
1627
|
+
const history = db.prepare("SELECT COUNT(*) AS count FROM ask_history").get();
|
|
1628
|
+
const sessions = db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
1629
|
+
return {
|
|
1630
|
+
status: "ok",
|
|
1631
|
+
file: DB_FILE,
|
|
1632
|
+
schema: schema?.value || "-",
|
|
1633
|
+
history: history?.count ?? 0,
|
|
1634
|
+
sessions: sessions?.count ?? 0,
|
|
1635
|
+
};
|
|
1636
|
+
} finally {
|
|
1637
|
+
db.close();
|
|
1638
|
+
}
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
return {
|
|
1641
|
+
status: "error",
|
|
1642
|
+
file: DB_FILE,
|
|
1643
|
+
schema: "-",
|
|
1644
|
+
history: "-",
|
|
1645
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function recordAskHistory({ question, answer, providerConfig, dataContext, error, sessionId }) {
|
|
1651
|
+
try {
|
|
1652
|
+
initDatabase();
|
|
1653
|
+
const db = openDatabase();
|
|
1654
|
+
try {
|
|
1655
|
+
db.prepare(`
|
|
1656
|
+
INSERT INTO ask_history(profile, provider, model, question, answer, context_json, error)
|
|
1657
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1658
|
+
`).run(
|
|
1659
|
+
providerConfig.name || "",
|
|
1660
|
+
providerConfig.provider || "",
|
|
1661
|
+
providerConfig.model || "",
|
|
1662
|
+
question,
|
|
1663
|
+
answer,
|
|
1664
|
+
JSON.stringify(dataContext),
|
|
1665
|
+
error || "",
|
|
1666
|
+
);
|
|
1667
|
+
} finally {
|
|
1668
|
+
db.close();
|
|
1669
|
+
}
|
|
1670
|
+
} catch {
|
|
1671
|
+
// History must never break the main answer path.
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function listHistory(limit) {
|
|
1676
|
+
initDatabase();
|
|
1677
|
+
const db = openDatabase();
|
|
1678
|
+
try {
|
|
1679
|
+
return db.prepare(`
|
|
1680
|
+
SELECT id, created_at, profile, provider, model, question, answer, error
|
|
1681
|
+
FROM ask_history
|
|
1682
|
+
ORDER BY id DESC
|
|
1683
|
+
LIMIT ?
|
|
1684
|
+
`).all(limit);
|
|
1685
|
+
} finally {
|
|
1686
|
+
db.close();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function clearHistory() {
|
|
1691
|
+
initDatabase();
|
|
1692
|
+
const db = openDatabase();
|
|
1693
|
+
try {
|
|
1694
|
+
db.exec("DELETE FROM ask_history");
|
|
1695
|
+
} finally {
|
|
1696
|
+
db.close();
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function clearSessions() {
|
|
1701
|
+
initDatabase();
|
|
1702
|
+
const db = openDatabase();
|
|
1703
|
+
try {
|
|
1704
|
+
db.exec("DELETE FROM session_messages; DELETE FROM sessions;");
|
|
1705
|
+
} finally {
|
|
1706
|
+
db.close();
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function createSession(providerConfig, title, parentId = null) {
|
|
1711
|
+
initDatabase();
|
|
1712
|
+
const db = openDatabase();
|
|
1713
|
+
try {
|
|
1714
|
+
const result = db.prepare(`
|
|
1715
|
+
INSERT INTO sessions(parent_id, title, profile, provider, model)
|
|
1716
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1717
|
+
`).run(parentId, title.slice(0, 120), providerConfig.name || "", providerConfig.provider || "", providerConfig.model || "");
|
|
1718
|
+
return Number(result.lastInsertRowid);
|
|
1719
|
+
} finally {
|
|
1720
|
+
db.close();
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function ensureSessionForAsk(options, providerConfig, question) {
|
|
1725
|
+
if (options.session) {
|
|
1726
|
+
return Number(options.session);
|
|
1727
|
+
}
|
|
1728
|
+
return createSession(providerConfig, question);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function appendSessionExchange(sessionId, question, answer, dataContext, error) {
|
|
1732
|
+
if (!sessionId) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const db = openDatabase();
|
|
1736
|
+
try {
|
|
1737
|
+
db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'user', ?, ?)")
|
|
1738
|
+
.run(sessionId, question, JSON.stringify(dataContext));
|
|
1739
|
+
db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, 'assistant', ?, ?)")
|
|
1740
|
+
.run(sessionId, error || answer || "", JSON.stringify({ error: error || "" }));
|
|
1741
|
+
db.prepare("UPDATE sessions SET updated_at = datetime('now') WHERE id = ?").run(sessionId);
|
|
1742
|
+
} finally {
|
|
1743
|
+
db.close();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function listSessions(limit) {
|
|
1748
|
+
initDatabase();
|
|
1749
|
+
const db = openDatabase();
|
|
1750
|
+
try {
|
|
1751
|
+
return db.prepare(`
|
|
1752
|
+
SELECT s.id, s.updated_at, s.profile, s.provider, s.model, s.title, COUNT(m.id) AS messages
|
|
1753
|
+
FROM sessions s
|
|
1754
|
+
LEFT JOIN session_messages m ON m.session_id = s.id
|
|
1755
|
+
GROUP BY s.id
|
|
1756
|
+
ORDER BY s.updated_at DESC, s.id DESC
|
|
1757
|
+
LIMIT ?
|
|
1758
|
+
`).all(limit);
|
|
1759
|
+
} finally {
|
|
1760
|
+
db.close();
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function getSessionAiHistory(sessionId) {
|
|
1765
|
+
initDatabase();
|
|
1766
|
+
const db = openDatabase();
|
|
1767
|
+
try {
|
|
1768
|
+
return db.prepare("SELECT role, content FROM session_messages WHERE session_id = ? ORDER BY id ASC")
|
|
1769
|
+
.all(sessionId)
|
|
1770
|
+
.map((row) => ({ role: row.role, content: row.content }));
|
|
1771
|
+
} finally {
|
|
1772
|
+
db.close();
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function getSession(sessionId) {
|
|
1777
|
+
initDatabase();
|
|
1778
|
+
const db = openDatabase();
|
|
1779
|
+
try {
|
|
1780
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1781
|
+
if (!session) {
|
|
1782
|
+
throw new Error(`Сессия не найдена: ${sessionId}`);
|
|
1783
|
+
}
|
|
1784
|
+
return session;
|
|
1785
|
+
} finally {
|
|
1786
|
+
db.close();
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function printSessionMessages(sessionId) {
|
|
1791
|
+
const rows = getSessionAiHistory(sessionId).map((row, index) => ({ id: index + 1, ...row }));
|
|
1792
|
+
printTable(rows, [
|
|
1793
|
+
["id", "#"],
|
|
1794
|
+
["role", "Роль"],
|
|
1795
|
+
["content", "Текст"],
|
|
1796
|
+
]);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function forkSessionInDb(sessionId) {
|
|
1800
|
+
initDatabase();
|
|
1801
|
+
const db = openDatabase();
|
|
1802
|
+
try {
|
|
1803
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
1804
|
+
if (!session) {
|
|
1805
|
+
throw new Error(`Сессия не найдена: ${sessionId}`);
|
|
1806
|
+
}
|
|
1807
|
+
const result = db.prepare(`
|
|
1808
|
+
INSERT INTO sessions(parent_id, title, profile, provider, model)
|
|
1809
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1810
|
+
`).run(sessionId, `Fork: ${session.title}`, session.profile, session.provider, session.model);
|
|
1811
|
+
const newId = Number(result.lastInsertRowid);
|
|
1812
|
+
const messages = db.prepare("SELECT role, content, context_json FROM session_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
|
|
1813
|
+
const insert = db.prepare("INSERT INTO session_messages(session_id, role, content, context_json) VALUES (?, ?, ?, ?)");
|
|
1814
|
+
for (const message of messages) {
|
|
1815
|
+
insert.run(newId, message.role, message.content, message.context_json);
|
|
1816
|
+
}
|
|
1817
|
+
return newId;
|
|
1818
|
+
} finally {
|
|
1819
|
+
db.close();
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function listFeatures() {
|
|
1824
|
+
initDatabase();
|
|
1825
|
+
const db = openDatabase();
|
|
1826
|
+
try {
|
|
1827
|
+
return Object.entries(FEATURES).map(([name, meta]) => {
|
|
1828
|
+
const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
|
|
1829
|
+
const enabled = row ? Boolean(row.enabled) : meta.defaultEnabled;
|
|
1830
|
+
return { name, enabled: enabled ? "yes" : "no", stage: meta.stage, description: meta.description };
|
|
1831
|
+
});
|
|
1832
|
+
} finally {
|
|
1833
|
+
db.close();
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function isFeatureEnabled(name) {
|
|
1838
|
+
const meta = FEATURES[name];
|
|
1839
|
+
if (!meta) {
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
initDatabase();
|
|
1843
|
+
const db = openDatabase();
|
|
1844
|
+
try {
|
|
1845
|
+
const row = db.prepare("SELECT enabled FROM feature_flags WHERE name = ?").get(name);
|
|
1846
|
+
return row ? Boolean(row.enabled) : meta.defaultEnabled;
|
|
1847
|
+
} finally {
|
|
1848
|
+
db.close();
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function setFeatureEnabled(name, enabled) {
|
|
1853
|
+
initDatabase();
|
|
1854
|
+
const db = openDatabase();
|
|
1855
|
+
try {
|
|
1856
|
+
db.prepare(`
|
|
1857
|
+
INSERT INTO feature_flags(name, enabled, updated_at) VALUES (?, ?, datetime('now'))
|
|
1858
|
+
ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, updated_at = excluded.updated_at
|
|
1859
|
+
`).run(name, enabled ? 1 : 0);
|
|
1860
|
+
} finally {
|
|
1861
|
+
db.close();
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1253
1865
|
function assertKeyProvider(provider) {
|
|
1254
1866
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
1255
1867
|
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
@@ -1339,9 +1951,43 @@ async function aiAsk(args, context = {}) {
|
|
|
1339
1951
|
|
|
1340
1952
|
const config = await loadConfig();
|
|
1341
1953
|
const providerConfig = resolveAiProfile(config, options);
|
|
1954
|
+
applyRuntimeConfig(providerConfig, options.config);
|
|
1342
1955
|
const dataContext = await buildDataContext(question);
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1956
|
+
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
1957
|
+
const historyEnabled = !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
1958
|
+
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
1959
|
+
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
1960
|
+
const messages = buildAiMessages(question, dataContext, history, options);
|
|
1961
|
+
let answer = "";
|
|
1962
|
+
let errorMessage = "";
|
|
1963
|
+
|
|
1964
|
+
try {
|
|
1965
|
+
emitEvent(options, "provider_selected", { profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model });
|
|
1966
|
+
answer = await callAiProvider(providerConfig, messages);
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1969
|
+
if (historyEnabled) {
|
|
1970
|
+
recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage, sessionId });
|
|
1971
|
+
appendSessionExchange(sessionId, question, "", dataContext, errorMessage);
|
|
1972
|
+
}
|
|
1973
|
+
throw error;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (historyEnabled) {
|
|
1977
|
+
recordAskHistory({ question, answer, providerConfig, dataContext, error: "", sessionId });
|
|
1978
|
+
appendSessionExchange(sessionId, question, answer, dataContext, "");
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
1982
|
+
|
|
1983
|
+
if (options.output) {
|
|
1984
|
+
await writeFile(options.output, answer, "utf8");
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (options.format === "json" || options.schema === "json") {
|
|
1988
|
+
printJson({ answer, profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model, sessionId, context: dataContext });
|
|
1989
|
+
return answer;
|
|
1990
|
+
}
|
|
1345
1991
|
|
|
1346
1992
|
console.log(answer);
|
|
1347
1993
|
return answer;
|
|
@@ -1364,9 +2010,28 @@ function resolveAiProfile(config, options = {}) {
|
|
|
1364
2010
|
provider,
|
|
1365
2011
|
model: options.model || activeProfile.model || config.ai.model,
|
|
1366
2012
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
2013
|
+
temperature: options.temperature || activeProfile.temperature,
|
|
1367
2014
|
};
|
|
1368
2015
|
}
|
|
1369
2016
|
|
|
2017
|
+
function applyRuntimeConfig(target, value) {
|
|
2018
|
+
if (!value) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const [key, ...parts] = String(value).split("=");
|
|
2022
|
+
if (!key || parts.length === 0) {
|
|
2023
|
+
throw new Error("Флаг --config должен быть в формате key=value.");
|
|
2024
|
+
}
|
|
2025
|
+
setConfigValue(target, key, parts.join("="));
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function emitEvent(options, type, data) {
|
|
2029
|
+
if (!options.events) {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
printJson({ type, at: new Date().toISOString(), ...data });
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1370
2035
|
async function buildDataContext(question) {
|
|
1371
2036
|
const apiBaseUrl = await getApiBaseUrl();
|
|
1372
2037
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
@@ -1489,7 +2154,7 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
1489
2154
|
return score;
|
|
1490
2155
|
}
|
|
1491
2156
|
|
|
1492
|
-
function buildAiMessages(question, dataContext, history) {
|
|
2157
|
+
function buildAiMessages(question, dataContext, history, options = {}) {
|
|
1493
2158
|
const sourceLines = buildSourceLines(dataContext);
|
|
1494
2159
|
const system = [
|
|
1495
2160
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
@@ -1498,8 +2163,10 @@ function buildAiMessages(question, dataContext, history) {
|
|
|
1498
2163
|
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
1499
2164
|
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
1500
2165
|
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
2166
|
+
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
2167
|
+
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
1501
2168
|
"Отвечай кратко и по делу.",
|
|
1502
|
-
].join(" ");
|
|
2169
|
+
].filter(Boolean).join(" ");
|
|
1503
2170
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
1504
2171
|
const recentHistory = history.slice(-6);
|
|
1505
2172
|
|
|
@@ -1625,7 +2292,7 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
1625
2292
|
body: JSON.stringify({
|
|
1626
2293
|
model: config.model,
|
|
1627
2294
|
messages,
|
|
1628
|
-
temperature: 0.2,
|
|
2295
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
1629
2296
|
}),
|
|
1630
2297
|
});
|
|
1631
2298
|
|
|
@@ -1815,9 +2482,9 @@ async function setupClient(args) {
|
|
|
1815
2482
|
throw new Error('Only "iola setup codex" is available in this first release.');
|
|
1816
2483
|
}
|
|
1817
2484
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
console.log("
|
|
2485
|
+
await runCommand("codex", ["mcp", "add", "yoshkarOlaPublicData", "--url", `${await getMcpBaseUrl()}/mcp`], { inherit: true });
|
|
2486
|
+
await runCommand("npx", ["-y", "@iola_adm/yoshkar-ola-public-mcp", "install-skill", "codex"], { inherit: true });
|
|
2487
|
+
console.log("Codex MCP и skill установлены.");
|
|
1821
2488
|
}
|
|
1822
2489
|
|
|
1823
2490
|
function parseOptions(args) {
|
|
@@ -1825,12 +2492,12 @@ function parseOptions(args) {
|
|
|
1825
2492
|
|
|
1826
2493
|
for (let index = 0; index < args.length; index += 1) {
|
|
1827
2494
|
const arg = args[index];
|
|
1828
|
-
if (arg === "--json" || arg === "--yes") {
|
|
2495
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all") {
|
|
1829
2496
|
result[arg.slice(2)] = true;
|
|
1830
2497
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
1831
2498
|
result.check = true;
|
|
1832
2499
|
result[arg.slice(2)] = true;
|
|
1833
|
-
} 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") {
|
|
2500
|
+
} 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") {
|
|
1834
2501
|
result[arg.slice(2)] = args[index + 1];
|
|
1835
2502
|
index += 1;
|
|
1836
2503
|
} else {
|