@iola_adm/iola-cli 0.1.12 → 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 +34 -0
- package/bin/iola.js +1 -2
- package/package.json +6 -5
- package/src/cli.js +240 -3
package/README.md
CHANGED
|
@@ -87,6 +87,10 @@ 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
|
|
90
94
|
iola config get
|
|
91
95
|
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
92
96
|
iola config reset
|
|
@@ -139,6 +143,7 @@ iola agent
|
|
|
139
143
|
/help
|
|
140
144
|
/health
|
|
141
145
|
/doctor
|
|
146
|
+
/db status
|
|
142
147
|
/config get
|
|
143
148
|
/layers
|
|
144
149
|
/data schools --limit 10
|
|
@@ -160,6 +165,7 @@ iola agent
|
|
|
160
165
|
/provider
|
|
161
166
|
/config
|
|
162
167
|
/history
|
|
168
|
+
/history --limit 20
|
|
163
169
|
/clear
|
|
164
170
|
/update
|
|
165
171
|
/init
|
|
@@ -271,6 +277,34 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
271
277
|
командам подключения MCP/skill, AI-запросам через Ollama/OpenAI/OpenRouter,
|
|
272
278
|
интерактивному агентному режиму, экспорту данных и проверке обновлений.
|
|
273
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
|
+
|
|
274
308
|
## Переменные окружения
|
|
275
309
|
|
|
276
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",
|
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,8 @@ 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 = 1;
|
|
14
18
|
const DEFAULT_AI_CONFIG = {
|
|
15
19
|
api: {
|
|
16
20
|
baseUrl: "https://apiiola.yasg.ru/api/v1",
|
|
@@ -76,6 +80,8 @@ const COMMANDS = new Map([
|
|
|
76
80
|
["version", showVersion],
|
|
77
81
|
["update", checkUpdate],
|
|
78
82
|
["doctor", doctor],
|
|
83
|
+
["db", handleDb],
|
|
84
|
+
["history", handleHistory],
|
|
79
85
|
["config", handleConfig],
|
|
80
86
|
["banner", showBanner],
|
|
81
87
|
["agent", startAgent],
|
|
@@ -119,6 +125,10 @@ Usage:
|
|
|
119
125
|
iola chat
|
|
120
126
|
iola init
|
|
121
127
|
iola doctor
|
|
128
|
+
iola db status
|
|
129
|
+
iola db init
|
|
130
|
+
iola history [--limit 20]
|
|
131
|
+
iola history clear
|
|
122
132
|
iola config get
|
|
123
133
|
iola config set api.baseUrl URL
|
|
124
134
|
iola config set api.mcpBaseUrl URL
|
|
@@ -225,7 +235,16 @@ async function handleAgentLine(line, state) {
|
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
if (command === "history") {
|
|
228
|
-
|
|
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);
|
|
229
248
|
return false;
|
|
230
249
|
}
|
|
231
250
|
|
|
@@ -307,6 +326,8 @@ async function handleAgentLine(line, state) {
|
|
|
307
326
|
const mapped = {
|
|
308
327
|
health: ["health", args],
|
|
309
328
|
doctor: ["doctor", args],
|
|
329
|
+
db: ["db", args],
|
|
330
|
+
history: ["history", args],
|
|
310
331
|
config: ["config", args],
|
|
311
332
|
layers: ["layers", args],
|
|
312
333
|
data: ["data", args],
|
|
@@ -333,6 +354,7 @@ function printAgentHelp() {
|
|
|
333
354
|
/help
|
|
334
355
|
/health
|
|
335
356
|
/doctor
|
|
357
|
+
/db status
|
|
336
358
|
/config get
|
|
337
359
|
/config set api.baseUrl URL
|
|
338
360
|
/layers
|
|
@@ -357,6 +379,7 @@ function printAgentHelp() {
|
|
|
357
379
|
/provider
|
|
358
380
|
/model
|
|
359
381
|
/history
|
|
382
|
+
/history --limit 20
|
|
360
383
|
/clear
|
|
361
384
|
/banner
|
|
362
385
|
/update
|
|
@@ -474,6 +497,7 @@ async function doctor(args = []) {
|
|
|
474
497
|
nodeRequired: `>=${MIN_NODE_VERSION}`,
|
|
475
498
|
nodeStatus: getNodeRequirementStatus().ok ? "ok" : "upgrade-required",
|
|
476
499
|
},
|
|
500
|
+
db: getDbStatus(),
|
|
477
501
|
api: {
|
|
478
502
|
baseUrl: apiBaseUrl,
|
|
479
503
|
mcpBaseUrl,
|
|
@@ -499,6 +523,9 @@ async function doctor(args = []) {
|
|
|
499
523
|
console.log("CLI");
|
|
500
524
|
printKeyValue(report.cli);
|
|
501
525
|
console.log("");
|
|
526
|
+
console.log("SQLite");
|
|
527
|
+
printKeyValue(report.db);
|
|
528
|
+
console.log("");
|
|
502
529
|
console.log("API/MCP");
|
|
503
530
|
printKeyValue(report.api);
|
|
504
531
|
console.log("");
|
|
@@ -615,6 +642,8 @@ async function initCli(args = []) {
|
|
|
615
642
|
|
|
616
643
|
showBanner();
|
|
617
644
|
console.log("Проверка окружения");
|
|
645
|
+
initDatabase();
|
|
646
|
+
const dbStatus = getDbStatus();
|
|
618
647
|
printKeyValue({
|
|
619
648
|
node: process.version,
|
|
620
649
|
node_required: `>=${MIN_NODE_VERSION}`,
|
|
@@ -622,6 +651,8 @@ async function initCli(args = []) {
|
|
|
622
651
|
npm: await getCommandVersion("npm", ["--version"]),
|
|
623
652
|
api: await probeEndpoint(`${await getMcpBaseUrl()}/mcp-health`),
|
|
624
653
|
mcp: await getMcpBaseUrl(),
|
|
654
|
+
sqlite: dbStatus.status,
|
|
655
|
+
sqlite_file: dbStatus.file,
|
|
625
656
|
});
|
|
626
657
|
console.log("");
|
|
627
658
|
|
|
@@ -763,6 +794,65 @@ async function handleConfig(args) {
|
|
|
763
794
|
throw new Error("Команды config: get, set, reset.");
|
|
764
795
|
}
|
|
765
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
|
+
|
|
766
856
|
async function aiDoctor(args) {
|
|
767
857
|
const options = parseOptions(args);
|
|
768
858
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -1250,6 +1340,142 @@ async function deleteAiKey(provider) {
|
|
|
1250
1340
|
console.log(`Локальный ключ ${provider} удален.`);
|
|
1251
1341
|
}
|
|
1252
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
|
+
|
|
1253
1479
|
function assertKeyProvider(provider) {
|
|
1254
1480
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
1255
1481
|
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
@@ -1341,7 +1567,18 @@ async function aiAsk(args, context = {}) {
|
|
|
1341
1567
|
const providerConfig = resolveAiProfile(config, options);
|
|
1342
1568
|
const dataContext = await buildDataContext(question);
|
|
1343
1569
|
const messages = buildAiMessages(question, dataContext, context.history || []);
|
|
1344
|
-
|
|
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: "" });
|
|
1345
1582
|
|
|
1346
1583
|
console.log(answer);
|
|
1347
1584
|
return answer;
|
|
@@ -1825,7 +2062,7 @@ function parseOptions(args) {
|
|
|
1825
2062
|
|
|
1826
2063
|
for (let index = 0; index < args.length; index += 1) {
|
|
1827
2064
|
const arg = args[index];
|
|
1828
|
-
if (arg === "--json" || arg === "--yes") {
|
|
2065
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent") {
|
|
1829
2066
|
result[arg.slice(2)] = true;
|
|
1830
2067
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
1831
2068
|
result.check = true;
|