@iola_adm/iola-cli 0.1.13 → 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 +45 -0
- package/package.json +1 -1
- package/src/cli.js +444 -14
package/README.md
CHANGED
|
@@ -91,12 +91,22 @@ 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
|
|
94
102
|
iola config get
|
|
95
103
|
iola config set api.baseUrl https://apiiola.yasg.ru/api/v1
|
|
96
104
|
iola config reset
|
|
97
105
|
iola update
|
|
98
106
|
iola version --check
|
|
99
107
|
iola ask "Найди школу 29"
|
|
108
|
+
iola ask "Найди школу 29" --profile codex --events --output answer.txt
|
|
109
|
+
iola ask "Найди школу 29" --schema json --no-history
|
|
100
110
|
iola data schools --limit 10
|
|
101
111
|
iola data kindergartens --search "29"
|
|
102
112
|
iola data schools --where address=Петрова --columns name,address,phone
|
|
@@ -144,6 +154,10 @@ iola agent
|
|
|
144
154
|
/health
|
|
145
155
|
/doctor
|
|
146
156
|
/db status
|
|
157
|
+
/sessions
|
|
158
|
+
/resume 1
|
|
159
|
+
/features list
|
|
160
|
+
/mcp status
|
|
147
161
|
/config get
|
|
148
162
|
/layers
|
|
149
163
|
/data schools --limit 10
|
|
@@ -300,11 +314,42 @@ iola db reset
|
|
|
300
314
|
iola history --limit 20
|
|
301
315
|
iola history --json
|
|
302
316
|
iola history clear
|
|
317
|
+
iola sessions --limit 20
|
|
318
|
+
iola sessions clear
|
|
319
|
+
iola resume 1 "продолжи"
|
|
320
|
+
iola fork 1 "новая ветка разговора"
|
|
303
321
|
```
|
|
304
322
|
|
|
305
323
|
Ключи OpenAI/OpenRouter в SQLite не сохраняются. Они остаются в локальном
|
|
306
324
|
`secrets.json` или в переменных окружения.
|
|
307
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
|
+
|
|
308
353
|
## Переменные окружения
|
|
309
354
|
|
|
310
355
|
```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,11 @@ 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],
|
|
85
98
|
["config", handleConfig],
|
|
86
99
|
["banner", showBanner],
|
|
87
100
|
["agent", startAgent],
|
|
@@ -129,12 +142,17 @@ Usage:
|
|
|
129
142
|
iola db init
|
|
130
143
|
iola history [--limit 20]
|
|
131
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
|
|
132
150
|
iola config get
|
|
133
151
|
iola config set api.baseUrl URL
|
|
134
152
|
iola config set api.mcpBaseUrl URL
|
|
135
153
|
iola config reset
|
|
136
154
|
iola update
|
|
137
|
-
iola ask TEXT
|
|
155
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
138
156
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
139
157
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
140
158
|
iola ai context TEXT [--json]
|
|
@@ -248,6 +266,31 @@ async function handleAgentLine(line, state) {
|
|
|
248
266
|
return false;
|
|
249
267
|
}
|
|
250
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);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
251
294
|
if (command === "config") {
|
|
252
295
|
await handleConfig(args.length > 0 ? args : ["get"]);
|
|
253
296
|
return false;
|
|
@@ -328,6 +371,11 @@ async function handleAgentLine(line, state) {
|
|
|
328
371
|
doctor: ["doctor", args],
|
|
329
372
|
db: ["db", args],
|
|
330
373
|
history: ["history", args],
|
|
374
|
+
sessions: ["sessions", args],
|
|
375
|
+
resume: ["resume", args],
|
|
376
|
+
fork: ["fork", args],
|
|
377
|
+
features: ["features", args],
|
|
378
|
+
mcp: ["mcp", args],
|
|
331
379
|
config: ["config", args],
|
|
332
380
|
layers: ["layers", args],
|
|
333
381
|
data: ["data", args],
|
|
@@ -355,6 +403,10 @@ function printAgentHelp() {
|
|
|
355
403
|
/health
|
|
356
404
|
/doctor
|
|
357
405
|
/db status
|
|
406
|
+
/sessions
|
|
407
|
+
/resume SESSION_ID
|
|
408
|
+
/features list
|
|
409
|
+
/mcp status
|
|
358
410
|
/config get
|
|
359
411
|
/config set api.baseUrl URL
|
|
360
412
|
/layers
|
|
@@ -520,6 +572,20 @@ async function doctor(args = []) {
|
|
|
520
572
|
return;
|
|
521
573
|
}
|
|
522
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
|
+
|
|
523
589
|
console.log("CLI");
|
|
524
590
|
printKeyValue(report.cli);
|
|
525
591
|
console.log("");
|
|
@@ -533,6 +599,11 @@ async function doctor(args = []) {
|
|
|
533
599
|
printKeyValue(report.ai);
|
|
534
600
|
console.log("");
|
|
535
601
|
printDiagnostics(diagnostics, recommendOllamaModel(diagnostics));
|
|
602
|
+
if (options.all) {
|
|
603
|
+
console.log("");
|
|
604
|
+
console.log("Фичи");
|
|
605
|
+
await handleFeatures(["list"]);
|
|
606
|
+
}
|
|
536
607
|
}
|
|
537
608
|
|
|
538
609
|
function getUpdateStatus(current, latest) {
|
|
@@ -853,6 +924,128 @@ async function handleHistory(args) {
|
|
|
853
924
|
]);
|
|
854
925
|
}
|
|
855
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
|
+
|
|
856
1049
|
async function aiDoctor(args) {
|
|
857
1050
|
const options = parseOptions(args);
|
|
858
1051
|
const diagnostics = await getLocalDiagnostics();
|
|
@@ -1367,6 +1560,32 @@ function initDatabase() {
|
|
|
1367
1560
|
error TEXT
|
|
1368
1561
|
);
|
|
1369
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
|
+
);
|
|
1370
1589
|
CREATE TABLE IF NOT EXISTS api_cache (
|
|
1371
1590
|
key TEXT PRIMARY KEY,
|
|
1372
1591
|
url TEXT NOT NULL,
|
|
@@ -1406,11 +1625,13 @@ function getDbStatus() {
|
|
|
1406
1625
|
try {
|
|
1407
1626
|
const schema = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
1408
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();
|
|
1409
1629
|
return {
|
|
1410
1630
|
status: "ok",
|
|
1411
1631
|
file: DB_FILE,
|
|
1412
1632
|
schema: schema?.value || "-",
|
|
1413
1633
|
history: history?.count ?? 0,
|
|
1634
|
+
sessions: sessions?.count ?? 0,
|
|
1414
1635
|
};
|
|
1415
1636
|
} finally {
|
|
1416
1637
|
db.close();
|
|
@@ -1426,7 +1647,7 @@ function getDbStatus() {
|
|
|
1426
1647
|
}
|
|
1427
1648
|
}
|
|
1428
1649
|
|
|
1429
|
-
function recordAskHistory({ question, answer, providerConfig, dataContext, error }) {
|
|
1650
|
+
function recordAskHistory({ question, answer, providerConfig, dataContext, error, sessionId }) {
|
|
1430
1651
|
try {
|
|
1431
1652
|
initDatabase();
|
|
1432
1653
|
const db = openDatabase();
|
|
@@ -1476,6 +1697,171 @@ function clearHistory() {
|
|
|
1476
1697
|
}
|
|
1477
1698
|
}
|
|
1478
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
|
+
|
|
1479
1865
|
function assertKeyProvider(provider) {
|
|
1480
1866
|
if (provider !== "openai" && provider !== "openrouter") {
|
|
1481
1867
|
throw new Error("Провайдер должен быть openai или openrouter.");
|
|
@@ -1565,20 +1951,43 @@ async function aiAsk(args, context = {}) {
|
|
|
1565
1951
|
|
|
1566
1952
|
const config = await loadConfig();
|
|
1567
1953
|
const providerConfig = resolveAiProfile(config, options);
|
|
1954
|
+
applyRuntimeConfig(providerConfig, options.config);
|
|
1568
1955
|
const dataContext = await buildDataContext(question);
|
|
1569
|
-
|
|
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);
|
|
1570
1961
|
let answer = "";
|
|
1571
1962
|
let errorMessage = "";
|
|
1572
1963
|
|
|
1573
1964
|
try {
|
|
1965
|
+
emitEvent(options, "provider_selected", { profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model });
|
|
1574
1966
|
answer = await callAiProvider(providerConfig, messages);
|
|
1575
1967
|
} catch (error) {
|
|
1576
1968
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1577
|
-
|
|
1969
|
+
if (historyEnabled) {
|
|
1970
|
+
recordAskHistory({ question, answer: "", providerConfig, dataContext, error: errorMessage, sessionId });
|
|
1971
|
+
appendSessionExchange(sessionId, question, "", dataContext, errorMessage);
|
|
1972
|
+
}
|
|
1578
1973
|
throw error;
|
|
1579
1974
|
}
|
|
1580
1975
|
|
|
1581
|
-
|
|
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
|
+
}
|
|
1582
1991
|
|
|
1583
1992
|
console.log(answer);
|
|
1584
1993
|
return answer;
|
|
@@ -1601,9 +2010,28 @@ function resolveAiProfile(config, options = {}) {
|
|
|
1601
2010
|
provider,
|
|
1602
2011
|
model: options.model || activeProfile.model || config.ai.model,
|
|
1603
2012
|
baseUrl: options["base-url"] || activeProfile.baseUrl || config.ai.baseUrl,
|
|
2013
|
+
temperature: options.temperature || activeProfile.temperature,
|
|
1604
2014
|
};
|
|
1605
2015
|
}
|
|
1606
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
|
+
|
|
1607
2035
|
async function buildDataContext(question) {
|
|
1608
2036
|
const apiBaseUrl = await getApiBaseUrl();
|
|
1609
2037
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
@@ -1726,7 +2154,7 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
1726
2154
|
return score;
|
|
1727
2155
|
}
|
|
1728
2156
|
|
|
1729
|
-
function buildAiMessages(question, dataContext, history) {
|
|
2157
|
+
function buildAiMessages(question, dataContext, history, options = {}) {
|
|
1730
2158
|
const sourceLines = buildSourceLines(dataContext);
|
|
1731
2159
|
const system = [
|
|
1732
2160
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
@@ -1735,8 +2163,10 @@ function buildAiMessages(question, dataContext, history) {
|
|
|
1735
2163
|
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
1736
2164
|
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
1737
2165
|
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
2166
|
+
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
2167
|
+
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
1738
2168
|
"Отвечай кратко и по делу.",
|
|
1739
|
-
].join(" ");
|
|
2169
|
+
].filter(Boolean).join(" ");
|
|
1740
2170
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
1741
2171
|
const recentHistory = history.slice(-6);
|
|
1742
2172
|
|
|
@@ -1862,7 +2292,7 @@ async function callOpenAiCompatible(config, messages, apiKey, providerName) {
|
|
|
1862
2292
|
body: JSON.stringify({
|
|
1863
2293
|
model: config.model,
|
|
1864
2294
|
messages,
|
|
1865
|
-
temperature: 0.2,
|
|
2295
|
+
temperature: Number(config.temperature ?? 0.2),
|
|
1866
2296
|
}),
|
|
1867
2297
|
});
|
|
1868
2298
|
|
|
@@ -2052,9 +2482,9 @@ async function setupClient(args) {
|
|
|
2052
2482
|
throw new Error('Only "iola setup codex" is available in this first release.');
|
|
2053
2483
|
}
|
|
2054
2484
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
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 установлены.");
|
|
2058
2488
|
}
|
|
2059
2489
|
|
|
2060
2490
|
function parseOptions(args) {
|
|
@@ -2062,12 +2492,12 @@ function parseOptions(args) {
|
|
|
2062
2492
|
|
|
2063
2493
|
for (let index = 0; index < args.length; index += 1) {
|
|
2064
2494
|
const arg = args[index];
|
|
2065
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent") {
|
|
2495
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all") {
|
|
2066
2496
|
result[arg.slice(2)] = true;
|
|
2067
2497
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
2068
2498
|
result.check = true;
|
|
2069
2499
|
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") {
|
|
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") {
|
|
2071
2501
|
result[arg.slice(2)] = args[index + 1];
|
|
2072
2502
|
index += 1;
|
|
2073
2503
|
} else {
|