@iola_adm/iola-cli 0.1.11 → 0.1.13
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 +41 -1
- package/bin/iola.js +1 -2
- package/package.json +7 -6
- package/src/cli.js +325 -4
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@ node --version
|
|
|
18
18
|
npm --version
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Нужен Node.js `
|
|
21
|
+
Нужен Node.js `22.5.0` или новее. Это нужно для встроенной SQLite-БД
|
|
22
|
+
`node:sqlite`, которую CLI будет использовать для локальной истории, кеша и
|
|
23
|
+
сессий без дополнительных нативных зависимостей.
|
|
24
|
+
|
|
25
|
+
Если Node.js не установлен или версия ниже `22.5.0`:
|
|
22
26
|
|
|
23
27
|
```bash
|
|
24
28
|
# Windows
|
|
@@ -54,6 +58,8 @@ curl -fsSL https://ollama.com/install.sh | sh
|
|
|
54
58
|
Диагностика ПК и подбор локальной модели:
|
|
55
59
|
|
|
56
60
|
```bash
|
|
61
|
+
npx -y @iola_adm/iola-cli init
|
|
62
|
+
npx -y @iola_adm/iola-cli init --upgrade-node
|
|
57
63
|
npx -y @iola_adm/iola-cli ai doctor
|
|
58
64
|
npx -y @iola_adm/iola-cli ai setup ollama
|
|
59
65
|
```
|
|
@@ -81,6 +87,10 @@ iola agent
|
|
|
81
87
|
iola chat
|
|
82
88
|
iola init
|
|
83
89
|
iola doctor
|
|
90
|
+
iola db status
|
|
91
|
+
iola db init
|
|
92
|
+
iola history --limit 20
|
|
93
|
+
iola history clear
|
|
84
94
|
iola config get
|
|
85
95
|
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
86
96
|
iola config reset
|
|
@@ -133,6 +143,7 @@ iola agent
|
|
|
133
143
|
/help
|
|
134
144
|
/health
|
|
135
145
|
/doctor
|
|
146
|
+
/db status
|
|
136
147
|
/config get
|
|
137
148
|
/layers
|
|
138
149
|
/data schools --limit 10
|
|
@@ -154,6 +165,7 @@ iola agent
|
|
|
154
165
|
/provider
|
|
155
166
|
/config
|
|
156
167
|
/history
|
|
168
|
+
/history --limit 20
|
|
157
169
|
/clear
|
|
158
170
|
/update
|
|
159
171
|
/init
|
|
@@ -265,6 +277,34 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
265
277
|
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter,
|
|
266
278
|
интерактивному агентному режиму, экспорту данных и проверке обновлений.
|
|
267
279
|
|
|
280
|
+
## Локальная SQLite-БД
|
|
281
|
+
|
|
282
|
+
CLI использует встроенный `node:sqlite` и хранит локальную БД в профиле
|
|
283
|
+
пользователя:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
%USERPROFILE%\.iola\iola.db
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
БД создается автоматически при установке npm-пакета и при `iola init`.
|
|
290
|
+
В ней хранятся история AI-запросов, контекст ответа, ошибки выполнения,
|
|
291
|
+
служебная таблица версии схемы, а также подготовлены таблицы для кеша API и
|
|
292
|
+
сохраненных выборок.
|
|
293
|
+
|
|
294
|
+
Команды:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
iola db status
|
|
298
|
+
iola db init
|
|
299
|
+
iola db reset
|
|
300
|
+
iola history --limit 20
|
|
301
|
+
iola history --json
|
|
302
|
+
iola history clear
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Ключи OpenAI/OpenRouter в SQLite не сохраняются. Они остаются в локальном
|
|
306
|
+
`secrets.json` или в переменных окружения.
|
|
307
|
+
|
|
268
308
|
## Переменные окружения
|
|
269
309
|
|
|
270
310
|
```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.13",
|
|
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",
|
|
@@ -36,6 +37,6 @@
|
|
|
36
37
|
"cli"
|
|
37
38
|
],
|
|
38
39
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
40
|
+
"node": ">=22.5.0"
|
|
40
41
|
}
|
|
41
42
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
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";
|
|
12
|
+
const MIN_NODE_VERSION = "22.5.0";
|
|
10
13
|
const CONFIG_DIR = path.join(os.homedir(), ".iola");
|
|
11
14
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
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 = 1;
|
|
13
18
|
const DEFAULT_AI_CONFIG = {
|
|
14
19
|
api: {
|
|
15
20
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -75,6 +80,8 @@ const COMMANDS = new Map([
|
|
|
75
80
|
["version", showVersion],
|
|
76
81
|
["update", checkUpdate],
|
|
77
82
|
["doctor", doctor],
|
|
83
|
+
["db", handleDb],
|
|
84
|
+
["history", handleHistory],
|
|
78
85
|
["config", handleConfig],
|
|
79
86
|
["banner", showBanner],
|
|
80
87
|
["agent", startAgent],
|
|
@@ -94,6 +101,11 @@ const COMMANDS = new Map([
|
|
|
94
101
|
|
|
95
102
|
export async function main(argv) {
|
|
96
103
|
const [command = "help", ...args] = argv;
|
|
104
|
+
const nodeStatus = getNodeRequirementStatus();
|
|
105
|
+
if (!nodeStatus.ok && !["help", "version", "doctor", "init"].includes(command)) {
|
|
106
|
+
throw new Error(`Нужен Node.js ${MIN_NODE_VERSION} или новее. Сейчас: ${nodeStatus.current}. Запустите: iola init --upgrade-node`);
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
const handler = COMMANDS.get(command);
|
|
98
110
|
|
|
99
111
|
if (!handler) {
|
|
@@ -113,6 +125,10 @@ Usage:
|
|
|
113
125
|
iola chat
|
|
114
126
|
iola init
|
|
115
127
|
iola doctor
|
|
128
|
+
iola db status
|
|
129
|
+
iola db init
|
|
130
|
+
iola history [--limit 20]
|
|
131
|
+
iola history clear
|
|
116
132
|
iola config get
|
|
117
133
|
iola config set api.baseUrl URL
|
|
118
134
|
iola config set api.mcpBaseUrl URL
|
|
@@ -148,6 +164,9 @@ Usage:
|
|
|
148
164
|
Environment:
|
|
149
165
|
IOLA_API_BASE_URL default: ${API_BASE_URL}
|
|
150
166
|
IOLA_MCP_BASE_URL default: ${MCP_BASE_URL}
|
|
167
|
+
|
|
168
|
+
Requirements:
|
|
169
|
+
Node.js >= ${MIN_NODE_VERSION}
|
|
151
170
|
`);
|
|
152
171
|
}
|
|
153
172
|
|
|
@@ -216,7 +235,16 @@ async function handleAgentLine(line, state) {
|
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
if (command === "history") {
|
|
219
|
-
|
|
238
|
+
if (args.length > 0) {
|
|
239
|
+
await handleHistory(args);
|
|
240
|
+
} else {
|
|
241
|
+
printAgentHistory(state.history);
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (command === "db") {
|
|
247
|
+
await handleDb(args);
|
|
220
248
|
return false;
|
|
221
249
|
}
|
|
222
250
|
|
|
@@ -298,6 +326,8 @@ async function handleAgentLine(line, state) {
|
|
|
298
326
|
const mapped = {
|
|
299
327
|
health: ["health", args],
|
|
300
328
|
doctor: ["doctor", args],
|
|
329
|
+
db: ["db", args],
|
|
330
|
+
history: ["history", args],
|
|
301
331
|
config: ["config", args],
|
|
302
332
|
layers: ["layers", args],
|
|
303
333
|
data: ["data", args],
|
|
@@ -324,6 +354,7 @@ function printAgentHelp() {
|
|
|
324
354
|
/help
|
|
325
355
|
/health
|
|
326
356
|
/doctor
|
|
357
|
+
/db status
|
|
327
358
|
/config get
|
|
328
359
|
/config set api.baseUrl URL
|
|
329
360
|
/layers
|
|
@@ -348,6 +379,7 @@ function printAgentHelp() {
|
|
|
348
379
|
/provider
|
|
349
380
|
/model
|
|
350
381
|
/history
|
|
382
|
+
/history --limit 20
|
|
351
383
|
/clear
|
|
352
384
|
/banner
|
|
353
385
|
/update
|
|
@@ -461,7 +493,11 @@ async function doctor(args = []) {
|
|
|
461
493
|
version: packageJson.default.version,
|
|
462
494
|
npmLatest: latest || "-",
|
|
463
495
|
update: getUpdateStatus(packageJson.default.version, latest),
|
|
496
|
+
node: process.version,
|
|
497
|
+
nodeRequired: `>=${MIN_NODE_VERSION}`,
|
|
498
|
+
nodeStatus: getNodeRequirementStatus().ok ? "ok" : "upgrade-required",
|
|
464
499
|
},
|
|
500
|
+
db: getDbStatus(),
|
|
465
501
|
api: {
|
|
466
502
|
baseUrl: apiBaseUrl,
|
|
467
503
|
mcpBaseUrl,
|
|
@@ -487,6 +523,9 @@ async function doctor(args = []) {
|
|
|
487
523
|
console.log("CLI");
|
|
488
524
|
printKeyValue(report.cli);
|
|
489
525
|
console.log("");
|
|
526
|
+
console.log("SQLite");
|
|
527
|
+
printKeyValue(report.db);
|
|
528
|
+
console.log("");
|
|
490
529
|
console.log("API/MCP");
|
|
491
530
|
printKeyValue(report.api);
|
|
492
531
|
console.log("");
|
|
@@ -514,6 +553,69 @@ function getUpdateStatus(current, latest) {
|
|
|
514
553
|
return "ok";
|
|
515
554
|
}
|
|
516
555
|
|
|
556
|
+
function getNodeRequirementStatus() {
|
|
557
|
+
const current = process.versions.node;
|
|
558
|
+
return {
|
|
559
|
+
current,
|
|
560
|
+
required: MIN_NODE_VERSION,
|
|
561
|
+
ok: compareVersions(current, MIN_NODE_VERSION) >= 0,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function offerNodeUpgrade(options, status) {
|
|
566
|
+
console.log(`Текущая версия Node.js: ${status.current}. Нужна ${MIN_NODE_VERSION} или новее.`);
|
|
567
|
+
|
|
568
|
+
if (!process.stdin.isTTY && !options["upgrade-node"]) {
|
|
569
|
+
printNodeUpgradeInstructions();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const shouldUpgrade = options["upgrade-node"] || (await confirm("Обновить Node.js установщиком сейчас? [y/N] "));
|
|
574
|
+
|
|
575
|
+
if (!shouldUpgrade) {
|
|
576
|
+
printNodeUpgradeInstructions();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
await upgradeNodeWithInstaller();
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log("После обновления перезапустите терминал и проверьте:");
|
|
583
|
+
console.log(" node --version");
|
|
584
|
+
console.log(" iola init");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function printNodeUpgradeInstructions() {
|
|
588
|
+
console.log("Обновите Node.js:");
|
|
589
|
+
console.log(" Windows: winget install OpenJS.NodeJS.LTS");
|
|
590
|
+
console.log(" macOS: brew install node");
|
|
591
|
+
console.log(" Linux: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function upgradeNodeWithInstaller() {
|
|
595
|
+
if (process.platform === "win32") {
|
|
596
|
+
try {
|
|
597
|
+
await runCommand("winget", ["upgrade", "OpenJS.NodeJS.LTS", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
598
|
+
} catch {
|
|
599
|
+
await runCommand("winget", ["install", "OpenJS.NodeJS.LTS", "--accept-package-agreements", "--accept-source-agreements"], { inherit: true });
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (process.platform === "darwin") {
|
|
605
|
+
try {
|
|
606
|
+
await runCommand("brew", ["upgrade", "node"], { inherit: true });
|
|
607
|
+
} catch {
|
|
608
|
+
await runCommand("brew", ["install", "node"], { inherit: true });
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
await runCommand("sh", [
|
|
614
|
+
"-c",
|
|
615
|
+
"curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs",
|
|
616
|
+
], { inherit: true });
|
|
617
|
+
}
|
|
618
|
+
|
|
517
619
|
async function checkConfiguredModel(config) {
|
|
518
620
|
if (config.ai.provider !== "ollama") {
|
|
519
621
|
return "external-api";
|
|
@@ -536,17 +638,29 @@ async function checkConfiguredModel(config) {
|
|
|
536
638
|
|
|
537
639
|
async function initCli(args = []) {
|
|
538
640
|
const options = parseOptions(args);
|
|
641
|
+
const nodeStatus = getNodeRequirementStatus();
|
|
539
642
|
|
|
540
643
|
showBanner();
|
|
541
644
|
console.log("Проверка окружения");
|
|
645
|
+
initDatabase();
|
|
646
|
+
const dbStatus = getDbStatus();
|
|
542
647
|
printKeyValue({
|
|
543
648
|
node: process.version,
|
|
649
|
+
node_required: `>=${MIN_NODE_VERSION}`,
|
|
650
|
+
node_status: nodeStatus.ok ? "ok" : "нужно обновить",
|
|
544
651
|
npm: await getCommandVersion("npm", ["--version"]),
|
|
545
652
|
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
546
653
|
mcp: await getMcpBaseUrl(),
|
|
654
|
+
sqlite: dbStatus.status,
|
|
655
|
+
sqlite_file: dbStatus.file,
|
|
547
656
|
});
|
|
548
657
|
console.log("");
|
|
549
658
|
|
|
659
|
+
if (!nodeStatus.ok) {
|
|
660
|
+
await offerNodeUpgrade(options, nodeStatus);
|
|
661
|
+
console.log("");
|
|
662
|
+
}
|
|
663
|
+
|
|
550
664
|
await aiDoctor(options.json ? ["--json"] : []);
|
|
551
665
|
|
|
552
666
|
if (!process.stdin.isTTY || options.yes) {
|
|
@@ -680,6 +794,65 @@ async function handleConfig(args) {
|
|
|
680
794
|
throw new Error("Команды config: get, set, reset.");
|
|
681
795
|
}
|
|
682
796
|
|
|
797
|
+
async function handleDb(args) {
|
|
798
|
+
const [action = "status"] = args;
|
|
799
|
+
const options = parseOptions(args);
|
|
800
|
+
|
|
801
|
+
if (action === "init") {
|
|
802
|
+
initDatabase();
|
|
803
|
+
if (!options.silent) {
|
|
804
|
+
console.log(`SQLite-БД готова: ${DB_FILE}`);
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (action === "status") {
|
|
810
|
+
printKeyValue(getDbStatus());
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (action === "reset") {
|
|
815
|
+
const shouldReset = await confirm("Удалить локальную SQLite-БД iola.db? [y/N] ");
|
|
816
|
+
if (!shouldReset) {
|
|
817
|
+
console.log("Сброс отменен.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
await rm(DB_FILE, { force: true });
|
|
821
|
+
initDatabase();
|
|
822
|
+
console.log(`SQLite-БД пересоздана: ${DB_FILE}`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
throw new Error("Команды db: status, init, reset.");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function handleHistory(args) {
|
|
830
|
+
const [action] = args;
|
|
831
|
+
const options = parseOptions(args);
|
|
832
|
+
|
|
833
|
+
if (action === "clear") {
|
|
834
|
+
clearHistory();
|
|
835
|
+
console.log("История очищена.");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const rows = listHistory(Number(options.limit || 20));
|
|
840
|
+
|
|
841
|
+
if (options.json) {
|
|
842
|
+
printJson(rows);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
printTable(rows, [
|
|
847
|
+
["id", "ID"],
|
|
848
|
+
["created_at", "Дата"],
|
|
849
|
+
["profile", "Профиль"],
|
|
850
|
+
["provider", "Провайдер"],
|
|
851
|
+
["question", "Вопрос"],
|
|
852
|
+
["answer", "Ответ"],
|
|
853
|
+
]);
|
|
854
|
+
}
|
|
855
|
+
|
|
683
856
|
async function aiDoctor(args) {
|
|
684
857
|
const options = parseOptions(args);
|
|
685
858
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -1167,6 +1340,142 @@ async function deleteAiKey(provider) {
|
|
|
1167
1340
|
console.log(`Локальный ключ ${provider} удален.`);
|
|
1168
1341
|
}
|
|
1169
1342
|
|
|
1343
|
+
function openDatabase() {
|
|
1344
|
+
const db = new DatabaseSync(DB_FILE);
|
|
1345
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
1346
|
+
return db;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function initDatabase() {
|
|
1350
|
+
mkdirSyncSafe(CONFIG_DIR);
|
|
1351
|
+
const db = openDatabase();
|
|
1352
|
+
try {
|
|
1353
|
+
db.exec(`
|
|
1354
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
1355
|
+
key TEXT PRIMARY KEY,
|
|
1356
|
+
value TEXT NOT NULL
|
|
1357
|
+
);
|
|
1358
|
+
CREATE TABLE IF NOT EXISTS ask_history (
|
|
1359
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1360
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1361
|
+
profile TEXT,
|
|
1362
|
+
provider TEXT,
|
|
1363
|
+
model TEXT,
|
|
1364
|
+
question TEXT NOT NULL,
|
|
1365
|
+
answer TEXT NOT NULL,
|
|
1366
|
+
context_json TEXT NOT NULL,
|
|
1367
|
+
error TEXT
|
|
1368
|
+
);
|
|
1369
|
+
CREATE INDEX IF NOT EXISTS idx_ask_history_created_at ON ask_history(created_at DESC);
|
|
1370
|
+
CREATE TABLE IF NOT EXISTS api_cache (
|
|
1371
|
+
key TEXT PRIMARY KEY,
|
|
1372
|
+
url TEXT NOT NULL,
|
|
1373
|
+
response_json TEXT NOT NULL,
|
|
1374
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1375
|
+
expires_at TEXT
|
|
1376
|
+
);
|
|
1377
|
+
CREATE TABLE IF NOT EXISTS saved_views (
|
|
1378
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1379
|
+
name TEXT NOT NULL UNIQUE,
|
|
1380
|
+
dataset TEXT NOT NULL,
|
|
1381
|
+
query_json TEXT NOT NULL,
|
|
1382
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1383
|
+
);
|
|
1384
|
+
`);
|
|
1385
|
+
db.prepare(`
|
|
1386
|
+
INSERT INTO meta(key, value) VALUES ('schema_version', ?)
|
|
1387
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1388
|
+
`).run(String(DB_SCHEMA_VERSION));
|
|
1389
|
+
} finally {
|
|
1390
|
+
db.close();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function mkdirSyncSafe(directory) {
|
|
1395
|
+
try {
|
|
1396
|
+
mkdirSync(directory, { recursive: true });
|
|
1397
|
+
} catch {
|
|
1398
|
+
// Directory creation is retried by write operations where needed.
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function getDbStatus() {
|
|
1403
|
+
try {
|
|
1404
|
+
initDatabase();
|
|
1405
|
+
const db = openDatabase();
|
|
1406
|
+
try {
|
|
1407
|
+
const schema = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
1408
|
+
const history = db.prepare("SELECT COUNT(*) AS count FROM ask_history").get();
|
|
1409
|
+
return {
|
|
1410
|
+
status: "ok",
|
|
1411
|
+
file: DB_FILE,
|
|
1412
|
+
schema: schema?.value || "-",
|
|
1413
|
+
history: history?.count ?? 0,
|
|
1414
|
+
};
|
|
1415
|
+
} finally {
|
|
1416
|
+
db.close();
|
|
1417
|
+
}
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
return {
|
|
1420
|
+
status: "error",
|
|
1421
|
+
file: DB_FILE,
|
|
1422
|
+
schema: "-",
|
|
1423
|
+
history: "-",
|
|
1424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function recordAskHistory({ question, answer, providerConfig, dataContext, error }) {
|
|
1430
|
+
try {
|
|
1431
|
+
initDatabase();
|
|
1432
|
+
const db = openDatabase();
|
|
1433
|
+
try {
|
|
1434
|
+
db.prepare(`
|
|
1435
|
+
INSERT INTO ask_history(profile, provider, model, question, answer, context_json, error)
|
|
1436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1437
|
+
`).run(
|
|
1438
|
+
providerConfig.name || "",
|
|
1439
|
+
providerConfig.provider || "",
|
|
1440
|
+
providerConfig.model || "",
|
|
1441
|
+
question,
|
|
1442
|
+
answer,
|
|
1443
|
+
JSON.stringify(dataContext),
|
|
1444
|
+
error || "",
|
|
1445
|
+
);
|
|
1446
|
+
} finally {
|
|
1447
|
+
db.close();
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
// History must never break the main answer path.
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function listHistory(limit) {
|
|
1455
|
+
initDatabase();
|
|
1456
|
+
const db = openDatabase();
|
|
1457
|
+
try {
|
|
1458
|
+
return db.prepare(`
|
|
1459
|
+
SELECT id, created_at, profile, provider, model, question, answer, error
|
|
1460
|
+
FROM ask_history
|
|
1461
|
+
ORDER BY id DESC
|
|
1462
|
+
LIMIT ?
|
|
1463
|
+
`).all(limit);
|
|
1464
|
+
} finally {
|
|
1465
|
+
db.close();
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function clearHistory() {
|
|
1470
|
+
initDatabase();
|
|
1471
|
+
const db = openDatabase();
|
|
1472
|
+
try {
|
|
1473
|
+
db.exec("DELETE FROM ask_history");
|
|
1474
|
+
} finally {
|
|
1475
|
+
db.close();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1170
1479
|
function assertKeyProvider(provider) {
|
|
1171
1480
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
1172
1481
|
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
@@ -1258,7 +1567,18 @@ async function aiAsk(args, context = {}) {
|
|
|
1258
1567
|
const providerConfig = resolveAiProfile(config, options);
|
|
1259
1568
|
const dataContext = await buildDataContext(question);
|
|
1260
1569
|
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
1261
|
-
|
|
1570
|
+
let answer = "";
|
|
1571
|
+
let errorMessage = "";
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
answer = await callAiProvider(providerConfig, messages);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1577
|
+
recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage });
|
|
1578
|
+
throw error;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
recordAskHistory({ question, answer, providerConfig, dataContext, error: "" });
|
|
1262
1582
|
|
|
1263
1583
|
console.log(answer);
|
|
1264
1584
|
return answer;
|
|
@@ -1742,10 +2062,11 @@ function parseOptions(args) {
|
|
|
1742
2062
|
|
|
1743
2063
|
for (let index = 0; index < args.length; index += 1) {
|
|
1744
2064
|
const arg = args[index];
|
|
1745
|
-
if (arg === "--json" || arg === "--yes") {
|
|
2065
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent") {
|
|
1746
2066
|
result[arg.slice(2)] = true;
|
|
1747
|
-
} else if (arg === "--check") {
|
|
2067
|
+
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
1748
2068
|
result.check = true;
|
|
2069
|
+
result[arg.slice(2)] = true;
|
|
1749
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") {
|
|
1750
2071
|
result[arg.slice(2)] = args[index + 1];
|
|
1751
2072
|
index += 1;
|