@iola_adm/iola-cli 0.1.15 → 0.1.19
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 +130 -0
- package/bin/iola.js +0 -0
- package/package.json +2 -1
- package/src/cli.js +767 -14
- package/wiki/AI-/320/277/321/200/320/276/321/204/320/270/320/273/320/270.md +44 -0
- package/wiki/Home.md +32 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +60 -0
- package/wiki//320/233/320/276/320/272/320/260/320/273/321/214/320/275/321/213/320/271-/320/270/320/275/321/201/321/202/321/200/321/203/320/274/320/265/320/275/321/202/320/260/320/273/321/214/320/275/321/213/320/271-/320/260/320/263/320/265/320/275/321/202.md +36 -0
- package/wiki//320/237/320/265/321/200/320/262/321/213/320/271-/320/267/320/260/320/277/321/203/321/201/320/272.md +38 -0
- package/wiki//320/240/320/265/321/210/320/265/320/275/320/270/320/265-/320/277/321/200/320/276/320/261/320/273/320/265/320/274.md +47 -0
- package/wiki//320/243/321/201/321/202/320/260/320/275/320/276/320/262/320/272/320/260.md +58 -0
package/src/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import readline from "node:readline/promises";
|
|
@@ -14,7 +14,9 @@ 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 = 3;
|
|
18
|
+
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
19
|
+
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
18
20
|
const FEATURES = {
|
|
19
21
|
"sqlite-history": { stage: "stable", defaultEnabled: true, description: "Запись истории AI-запросов в SQLite." },
|
|
20
22
|
sessions: { stage: "stable", defaultEnabled: true, description: "Сессии, resume и fork для AI-диалогов." },
|
|
@@ -58,6 +60,63 @@ const DEFAULT_AI_CONFIG = {
|
|
|
58
60
|
},
|
|
59
61
|
},
|
|
60
62
|
},
|
|
63
|
+
permissions: {
|
|
64
|
+
localTools: {
|
|
65
|
+
search_local: true,
|
|
66
|
+
get_card: true,
|
|
67
|
+
export_data: true,
|
|
68
|
+
run_report: true,
|
|
69
|
+
save_view: true,
|
|
70
|
+
},
|
|
71
|
+
writeFiles: true,
|
|
72
|
+
sync: true,
|
|
73
|
+
externalApi: true,
|
|
74
|
+
externalAi: true,
|
|
75
|
+
codex: true,
|
|
76
|
+
},
|
|
77
|
+
memory: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
},
|
|
80
|
+
hooks: {},
|
|
81
|
+
};
|
|
82
|
+
const AGENTS = {
|
|
83
|
+
"data-analyst": {
|
|
84
|
+
profile: null,
|
|
85
|
+
tools: true,
|
|
86
|
+
reasoning: "verify",
|
|
87
|
+
description: "Анализирует открытые данные, ищет объекты и отвечает с опорой на локальные данные.",
|
|
88
|
+
},
|
|
89
|
+
"quality-checker": {
|
|
90
|
+
profile: "local",
|
|
91
|
+
tools: true,
|
|
92
|
+
reasoning: "verify",
|
|
93
|
+
prefix: "Проверь качество данных и укажи найденные проблемы: ",
|
|
94
|
+
description: "Проверяет телефоны, email, ИНН и неполные карточки.",
|
|
95
|
+
},
|
|
96
|
+
exporter: {
|
|
97
|
+
profile: "local",
|
|
98
|
+
tools: true,
|
|
99
|
+
reasoning: "fast",
|
|
100
|
+
prefix: "Подготовь выгрузку данных: ",
|
|
101
|
+
description: "Готовит CSV/JSON выгрузки через локальные инструменты.",
|
|
102
|
+
},
|
|
103
|
+
"mcp-helper": {
|
|
104
|
+
profile: null,
|
|
105
|
+
tools: false,
|
|
106
|
+
description: "Помогает с MCP, профилями AI и диагностикой подключения.",
|
|
107
|
+
},
|
|
108
|
+
"local-fast": {
|
|
109
|
+
profile: "local",
|
|
110
|
+
tools: true,
|
|
111
|
+
reasoning: "fast",
|
|
112
|
+
description: "Быстрый локальный режим для простых запросов.",
|
|
113
|
+
},
|
|
114
|
+
reviewer: {
|
|
115
|
+
profile: null,
|
|
116
|
+
tools: false,
|
|
117
|
+
prefix: "Проверь ответ и найди слабые места: ",
|
|
118
|
+
description: "Режим проверки и уточнения ответов.",
|
|
119
|
+
},
|
|
61
120
|
};
|
|
62
121
|
const DATASETS = {
|
|
63
122
|
schools: {
|
|
@@ -94,11 +153,18 @@ const COMMANDS = new Map([
|
|
|
94
153
|
["resume", resumeSession],
|
|
95
154
|
["fork", forkSession],
|
|
96
155
|
["features", handleFeatures],
|
|
156
|
+
["permissions", handlePermissions],
|
|
157
|
+
["memory", handleMemory],
|
|
158
|
+
["hooks", handleHooks],
|
|
159
|
+
["agents", handleAgents],
|
|
97
160
|
["mcp", handleMcp],
|
|
98
161
|
["cache", handleCache],
|
|
99
162
|
["sync", handleSync],
|
|
163
|
+
["diff", handleDiff],
|
|
100
164
|
["views", handleViews],
|
|
101
165
|
["view", handleView],
|
|
166
|
+
["card", handleCard],
|
|
167
|
+
["quality", handleQuality],
|
|
102
168
|
["report", handleReport],
|
|
103
169
|
["privacy", handlePrivacy],
|
|
104
170
|
["backup", handleBackup],
|
|
@@ -122,6 +188,28 @@ const COMMANDS = new Map([
|
|
|
122
188
|
]);
|
|
123
189
|
|
|
124
190
|
export async function main(argv) {
|
|
191
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
192
|
+
await showHelp();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const runtime = parseGlobalOptions(argv);
|
|
197
|
+
if (runtime.help) {
|
|
198
|
+
await showHelp();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (runtime.debug) {
|
|
202
|
+
process.env.IOLA_DEBUG = "1";
|
|
203
|
+
}
|
|
204
|
+
if (runtime.debugFile) {
|
|
205
|
+
process.env.IOLA_DEBUG = "1";
|
|
206
|
+
process.env.IOLA_DEBUG_FILE = runtime.debugFile;
|
|
207
|
+
}
|
|
208
|
+
if (runtime.noColor) {
|
|
209
|
+
process.env.NO_COLOR = "1";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
argv = runtime.args;
|
|
125
213
|
const [command = "help", ...args] = argv;
|
|
126
214
|
const nodeStatus = getNodeRequirementStatus();
|
|
127
215
|
if (!nodeStatus.ok && !["help", "version", "doctor", "init"].includes(command)) {
|
|
@@ -139,7 +227,7 @@ export async function main(argv) {
|
|
|
139
227
|
throw new Error(`Unknown command: ${command}\nRun "iola help" to see available commands.`);
|
|
140
228
|
}
|
|
141
229
|
|
|
142
|
-
await handler(args);
|
|
230
|
+
await handler(runtime.debugFile ? [...args, "--debug-file", runtime.debugFile] : args);
|
|
143
231
|
}
|
|
144
232
|
|
|
145
233
|
async function showHelp() {
|
|
@@ -160,9 +248,18 @@ Usage:
|
|
|
160
248
|
iola resume SESSION_ID [TEXT]
|
|
161
249
|
iola fork SESSION_ID [TEXT]
|
|
162
250
|
iola features list|enable|disable
|
|
251
|
+
iola permissions list|allow|deny
|
|
252
|
+
iola memory show|add|set|clear|export
|
|
253
|
+
iola hooks list|add|delete|run
|
|
254
|
+
iola agents list|run
|
|
163
255
|
iola mcp list|status|install|remove
|
|
164
256
|
iola cache status|warm|clear
|
|
165
257
|
iola sync [--dataset schools|kindergartens]
|
|
258
|
+
iola sync status
|
|
259
|
+
iola diff [schools|kindergartens]
|
|
260
|
+
iola card schools 1215067180
|
|
261
|
+
iola card "школа 29"
|
|
262
|
+
iola quality [schools|kindergartens|missing-phones|invalid-emails|duplicate-inn]
|
|
166
263
|
iola views
|
|
167
264
|
iola view NAME [--format table|json|csv] [--output FILE]
|
|
168
265
|
iola report schools-summary|education-contacts|missing-phones|licenses
|
|
@@ -175,7 +272,7 @@ Usage:
|
|
|
175
272
|
iola config set api.mcpBaseUrl URL
|
|
176
273
|
iola config reset
|
|
177
274
|
iola update
|
|
178
|
-
iola ask TEXT [--profile NAME] [--model MODEL] [--output FILE] [--schema json|table] [--events] [--no-history]
|
|
275
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
|
|
179
276
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
180
277
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
181
278
|
iola ai context TEXT [--json]
|
|
@@ -214,6 +311,7 @@ Requirements:
|
|
|
214
311
|
async function startAgent() {
|
|
215
312
|
showBanner();
|
|
216
313
|
console.log("Интерактивный режим. Введите /help для списка команд, /exit для выхода.");
|
|
314
|
+
await runHooks("SessionStart", { mode: "agent" });
|
|
217
315
|
|
|
218
316
|
const rl = readline.createInterface({ input, output, prompt: "iola> " });
|
|
219
317
|
const state = {
|
|
@@ -248,6 +346,7 @@ async function startAgent() {
|
|
|
248
346
|
if (!closed) {
|
|
249
347
|
rl.close();
|
|
250
348
|
}
|
|
349
|
+
await runHooks("SessionEnd", { mode: "agent" });
|
|
251
350
|
}
|
|
252
351
|
|
|
253
352
|
async function handleAgentLine(line, state) {
|
|
@@ -309,6 +408,31 @@ async function handleAgentLine(line, state) {
|
|
|
309
408
|
return false;
|
|
310
409
|
}
|
|
311
410
|
|
|
411
|
+
if (command === "permissions") {
|
|
412
|
+
await handlePermissions(args);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (command === "memory") {
|
|
417
|
+
await handleMemory(args);
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (command === "hooks") {
|
|
422
|
+
await handleHooks(args);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (command === "agents") {
|
|
427
|
+
await handleAgents(args);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (command === "tools") {
|
|
432
|
+
await handlePermissions(["tools"]);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
312
436
|
if (command === "mcp") {
|
|
313
437
|
await handleMcp(args);
|
|
314
438
|
return false;
|
|
@@ -324,7 +448,7 @@ async function handleAgentLine(line, state) {
|
|
|
324
448
|
return false;
|
|
325
449
|
}
|
|
326
450
|
|
|
327
|
-
if (command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
|
|
451
|
+
if (command === "diff" || command === "card" || command === "quality" || command === "views" || command === "view" || command === "report" || command === "privacy" || command === "backup" || command === "alias" || command === "run") {
|
|
328
452
|
await COMMANDS.get(command)(args);
|
|
329
453
|
return false;
|
|
330
454
|
}
|
|
@@ -413,9 +537,15 @@ async function handleAgentLine(line, state) {
|
|
|
413
537
|
resume: ["resume", args],
|
|
414
538
|
fork: ["fork", args],
|
|
415
539
|
features: ["features", args],
|
|
540
|
+
permissions: ["permissions", args],
|
|
541
|
+
memory: ["memory", args],
|
|
542
|
+
hooks: ["hooks", args],
|
|
543
|
+
agents: ["agents", args],
|
|
544
|
+
tools: ["permissions", ["tools", ...args]],
|
|
416
545
|
mcp: ["mcp", args],
|
|
417
546
|
cache: ["cache", args],
|
|
418
547
|
sync: ["sync", args],
|
|
548
|
+
diff: ["diff", args],
|
|
419
549
|
config: ["config", args],
|
|
420
550
|
layers: ["layers", args],
|
|
421
551
|
data: ["data", args],
|
|
@@ -446,9 +576,17 @@ function printAgentHelp() {
|
|
|
446
576
|
/sessions
|
|
447
577
|
/resume SESSION_ID
|
|
448
578
|
/features list
|
|
579
|
+
/permissions
|
|
580
|
+
/tools
|
|
581
|
+
/memory show
|
|
582
|
+
/hooks list
|
|
583
|
+
/agents list
|
|
449
584
|
/mcp status
|
|
450
585
|
/cache status
|
|
451
586
|
/sync
|
|
587
|
+
/diff
|
|
588
|
+
/card школа 29
|
|
589
|
+
/quality
|
|
452
590
|
/views
|
|
453
591
|
/config get
|
|
454
592
|
/config set api.baseUrl URL
|
|
@@ -1051,6 +1189,208 @@ async function handleFeatures(args) {
|
|
|
1051
1189
|
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1052
1190
|
}
|
|
1053
1191
|
|
|
1192
|
+
async function handlePermissions(args) {
|
|
1193
|
+
const [action = "list", name] = args;
|
|
1194
|
+
const config = await loadConfig();
|
|
1195
|
+
|
|
1196
|
+
if (action === "list" || action === "ls" || action === "tools") {
|
|
1197
|
+
const permissions = config.permissions || DEFAULT_AI_CONFIG.permissions;
|
|
1198
|
+
const rows = [
|
|
1199
|
+
...LOCAL_TOOLS.map((tool) => ({
|
|
1200
|
+
permission: `localTools.${tool}`,
|
|
1201
|
+
value: permissions.localTools?.[tool] === false ? "deny" : "allow",
|
|
1202
|
+
scope: "local-tool",
|
|
1203
|
+
})),
|
|
1204
|
+
{ permission: "writeFiles", value: permissions.writeFiles === false ? "deny" : "allow", scope: "runtime" },
|
|
1205
|
+
{ permission: "sync", value: permissions.sync === false ? "deny" : "allow", scope: "runtime" },
|
|
1206
|
+
{ permission: "externalApi", value: permissions.externalApi === false ? "deny" : "allow", scope: "network" },
|
|
1207
|
+
{ permission: "externalAi", value: permissions.externalAi === false ? "deny" : "allow", scope: "network" },
|
|
1208
|
+
{ permission: "codex", value: permissions.codex === false ? "deny" : "allow", scope: "external-cli" },
|
|
1209
|
+
];
|
|
1210
|
+
printTable(rows, [
|
|
1211
|
+
["permission", "Разрешение"],
|
|
1212
|
+
["value", "Статус"],
|
|
1213
|
+
["scope", "Область"],
|
|
1214
|
+
]);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (action === "allow" || action === "deny") {
|
|
1219
|
+
if (!name) {
|
|
1220
|
+
throw new Error("Пример: iola permissions deny export_data");
|
|
1221
|
+
}
|
|
1222
|
+
const allow = action === "allow";
|
|
1223
|
+
const next = { ...(config.permissions || DEFAULT_AI_CONFIG.permissions) };
|
|
1224
|
+
next.localTools = { ...(next.localTools || {}) };
|
|
1225
|
+
if (LOCAL_TOOLS.includes(name)) {
|
|
1226
|
+
next.localTools[name] = allow;
|
|
1227
|
+
} else if (name in DEFAULT_AI_CONFIG.permissions) {
|
|
1228
|
+
next[name] = allow;
|
|
1229
|
+
} else {
|
|
1230
|
+
throw new Error(`Неизвестное разрешение: ${name}. Доступно: ${[...LOCAL_TOOLS, "writeFiles", "sync", "externalApi", "externalAi", "codex"].join(", ")}`);
|
|
1231
|
+
}
|
|
1232
|
+
await saveConfig({ permissions: next });
|
|
1233
|
+
console.log(`${name}: ${allow ? "allow" : "deny"}`);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
throw new Error("Команды permissions: list, tools, allow NAME, deny NAME.");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function handleMemory(args) {
|
|
1241
|
+
const [action = "show", ...rest] = args;
|
|
1242
|
+
const options = parseOptions(rest);
|
|
1243
|
+
|
|
1244
|
+
if (action === "show" || action === "list" || action === "ls") {
|
|
1245
|
+
const rows = listMemory(Number(options.limit || 50));
|
|
1246
|
+
if (options.json) {
|
|
1247
|
+
printJson(rows);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
printTable(rows, [
|
|
1251
|
+
["id", "ID"],
|
|
1252
|
+
["scope", "Область"],
|
|
1253
|
+
["content", "Память"],
|
|
1254
|
+
["created_at", "Дата"],
|
|
1255
|
+
]);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (action === "add" || action === "set") {
|
|
1260
|
+
const text = rest.join(" ").trim();
|
|
1261
|
+
if (!text) {
|
|
1262
|
+
throw new Error('Пример: iola memory add "Отвечай кратко и по данным Йошкар-Олы"');
|
|
1263
|
+
}
|
|
1264
|
+
const id = addMemory(text, options.scope || "user");
|
|
1265
|
+
console.log(`Память сохранена: ${id}`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (action === "delete" || action === "remove" || action === "rm") {
|
|
1270
|
+
const id = rest[0];
|
|
1271
|
+
if (!id) throw new Error("Пример: iola memory delete 1");
|
|
1272
|
+
deleteMemory(Number(id));
|
|
1273
|
+
console.log(`Память удалена: ${id}`);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (action === "clear") {
|
|
1278
|
+
clearMemory();
|
|
1279
|
+
console.log("Память очищена.");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (action === "export") {
|
|
1284
|
+
const rows = listMemory(1000);
|
|
1285
|
+
const file = rest[0] || path.join(CONFIG_DIR, "memory-export.json");
|
|
1286
|
+
await writeFile(file, `${JSON.stringify(rows, null, 2)}\n`, "utf8");
|
|
1287
|
+
console.log(`Память экспортирована: ${file}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
throw new Error("Команды memory: show, add TEXT, delete ID, clear, export [FILE].");
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function handleHooks(args) {
|
|
1295
|
+
const [action = "list", event, ...commandParts] = args;
|
|
1296
|
+
const config = await loadConfig();
|
|
1297
|
+
|
|
1298
|
+
if (action === "list" || action === "ls") {
|
|
1299
|
+
const rows = Object.entries(config.hooks || {}).flatMap(([hookEvent, commands]) =>
|
|
1300
|
+
(commands || []).map((command, index) => ({ event: hookEvent, index, command })));
|
|
1301
|
+
printTable(rows, [
|
|
1302
|
+
["event", "Событие"],
|
|
1303
|
+
["index", "#"],
|
|
1304
|
+
["command", "Команда"],
|
|
1305
|
+
]);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (action === "events") {
|
|
1310
|
+
printTable(HOOK_EVENTS.map((name) => ({ name })), [["name", "Событие"]]);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (action === "add") {
|
|
1315
|
+
if (!HOOK_EVENTS.includes(event) || commandParts.length === 0) {
|
|
1316
|
+
throw new Error(`Пример: iola hooks add AfterSync "iola quality" Доступно: ${HOOK_EVENTS.join(", ")}`);
|
|
1317
|
+
}
|
|
1318
|
+
const hooks = { ...(config.hooks || {}) };
|
|
1319
|
+
hooks[event] = [...(hooks[event] || []), commandParts.join(" ")];
|
|
1320
|
+
await saveConfig({ hooks });
|
|
1321
|
+
console.log(`Hook добавлен: ${event}`);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (action === "delete" || action === "remove") {
|
|
1326
|
+
const index = Number(commandParts[0] ?? event);
|
|
1327
|
+
const hookEvent = Number.isFinite(Number(event)) ? null : event;
|
|
1328
|
+
const hooks = { ...(config.hooks || {}) };
|
|
1329
|
+
if (hookEvent) {
|
|
1330
|
+
hooks[hookEvent] = (hooks[hookEvent] || []).filter((_, itemIndex) => itemIndex !== index);
|
|
1331
|
+
} else {
|
|
1332
|
+
for (const key of Object.keys(hooks)) hooks[key] = (hooks[key] || []).filter((_, itemIndex) => itemIndex !== index);
|
|
1333
|
+
}
|
|
1334
|
+
await saveConfig({ hooks });
|
|
1335
|
+
console.log("Hook удален.");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (action === "run") {
|
|
1340
|
+
if (!HOOK_EVENTS.includes(event)) throw new Error(`Событие обязательно: ${HOOK_EVENTS.join(", ")}`);
|
|
1341
|
+
await runHooks(event, { manual: true });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
throw new Error("Команды hooks: list, events, add EVENT COMMAND, delete EVENT INDEX, run EVENT.");
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function handleAgents(args) {
|
|
1349
|
+
const [action = "list", name, ...rest] = args;
|
|
1350
|
+
|
|
1351
|
+
if (action === "list" || action === "ls") {
|
|
1352
|
+
const rows = Object.entries(AGENTS).map(([agent, meta]) => ({
|
|
1353
|
+
agent,
|
|
1354
|
+
profile: meta.profile || "active",
|
|
1355
|
+
tools: meta.tools ? "yes" : "no",
|
|
1356
|
+
reasoning: meta.reasoning || "-",
|
|
1357
|
+
description: meta.description,
|
|
1358
|
+
}));
|
|
1359
|
+
printTable(rows, [
|
|
1360
|
+
["agent", "Агент"],
|
|
1361
|
+
["profile", "Профиль"],
|
|
1362
|
+
["tools", "Tools"],
|
|
1363
|
+
["reasoning", "Reasoning"],
|
|
1364
|
+
["description", "Описание"],
|
|
1365
|
+
]);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (action === "run") {
|
|
1370
|
+
if (!AGENTS[name]) {
|
|
1371
|
+
throw new Error(`Неизвестный агент: ${name}. Доступно: ${Object.keys(AGENTS).join(", ")}`);
|
|
1372
|
+
}
|
|
1373
|
+
const agent = AGENTS[name];
|
|
1374
|
+
const options = parseOptions(rest);
|
|
1375
|
+
const question = options._.join(" ").trim();
|
|
1376
|
+
if (!question) throw new Error(`Пример: iola agents run ${name} "найди школы на Петрова"`);
|
|
1377
|
+
const askArgs = [agent.prefix ? `${agent.prefix}${question}` : question, "--agent", name];
|
|
1378
|
+
if (agent.profile) askArgs.push("--profile", agent.profile);
|
|
1379
|
+
if (agent.tools) askArgs.push("--tools");
|
|
1380
|
+
if (agent.reasoning) askArgs.push("--reasoning", agent.reasoning);
|
|
1381
|
+
for (const flag of ["no-history", "quiet", "bare", "events", "fail-on-empty"]) {
|
|
1382
|
+
if (options[flag]) askArgs.push(`--${flag}`);
|
|
1383
|
+
}
|
|
1384
|
+
for (const flag of ["profile", "model", "output", "schema", "format", "reasoning"]) {
|
|
1385
|
+
if (options[flag]) askArgs.push(`--${flag}`, options[flag]);
|
|
1386
|
+
}
|
|
1387
|
+
await aiAsk(askArgs);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
throw new Error("Команды agents: list, run NAME TEXT.");
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1054
1394
|
async function handleMcp(args) {
|
|
1055
1395
|
const [action = "status", target = "codex"] = args;
|
|
1056
1396
|
|
|
@@ -1109,12 +1449,24 @@ async function handleCache(args) {
|
|
|
1109
1449
|
}
|
|
1110
1450
|
|
|
1111
1451
|
async function handleSync(args) {
|
|
1452
|
+
const [action] = args;
|
|
1453
|
+
if (action === "status") {
|
|
1454
|
+
printTable(getSyncStatus(), [
|
|
1455
|
+
["dataset", "Слой"],
|
|
1456
|
+
["records", "Записей"],
|
|
1457
|
+
["last_sync", "Последний sync"],
|
|
1458
|
+
["status", "Статус"],
|
|
1459
|
+
]);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
await assertPermission("sync");
|
|
1112
1463
|
const options = parseOptions(args);
|
|
1113
1464
|
const datasets = options.dataset ? [options.dataset] : Object.keys(DATASETS);
|
|
1114
1465
|
const rows = [];
|
|
1115
1466
|
for (const dataset of datasets) {
|
|
1116
1467
|
rows.push(await syncDataset(dataset));
|
|
1117
1468
|
}
|
|
1469
|
+
await runHooks("AfterSync", { datasets, rows });
|
|
1118
1470
|
printTable(rows, [
|
|
1119
1471
|
["dataset", "Слой"],
|
|
1120
1472
|
["records", "Записей"],
|
|
@@ -1123,6 +1475,44 @@ async function handleSync(args) {
|
|
|
1123
1475
|
]);
|
|
1124
1476
|
}
|
|
1125
1477
|
|
|
1478
|
+
async function handleDiff(args) {
|
|
1479
|
+
const [dataset] = args;
|
|
1480
|
+
const rows = listSyncChanges(dataset);
|
|
1481
|
+
printTable(rows, [
|
|
1482
|
+
["created_at", "Дата"],
|
|
1483
|
+
["dataset", "Слой"],
|
|
1484
|
+
["change_type", "Тип"],
|
|
1485
|
+
["record_key", "Ключ"],
|
|
1486
|
+
["summary", "Сводка"],
|
|
1487
|
+
]);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async function handleCard(args) {
|
|
1491
|
+
await ensureLocalData();
|
|
1492
|
+
const options = parseOptions(args);
|
|
1493
|
+
const query = args.join(" ").trim();
|
|
1494
|
+
if (!query) throw new Error('Пример: iola card "школа 29"');
|
|
1495
|
+
const item = findCard(query);
|
|
1496
|
+
if (!item) throw new Error(`Объект не найден: ${query}`);
|
|
1497
|
+
if (options.json) {
|
|
1498
|
+
printJson(item);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
printKeyValue(item);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function handleQuality(args) {
|
|
1505
|
+
const [scope = "all"] = args;
|
|
1506
|
+
await ensureLocalData();
|
|
1507
|
+
const rows = runQuality(scope);
|
|
1508
|
+
printTable(rows, [
|
|
1509
|
+
["check", "Проверка"],
|
|
1510
|
+
["dataset", "Слой"],
|
|
1511
|
+
["count", "Кол-во"],
|
|
1512
|
+
["sample", "Пример"],
|
|
1513
|
+
]);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1126
1516
|
async function handleViews(args) {
|
|
1127
1517
|
const [action, name] = args;
|
|
1128
1518
|
if (action === "delete" || action === "remove") {
|
|
@@ -1803,11 +2193,28 @@ function initDatabase() {
|
|
|
1803
2193
|
message TEXT,
|
|
1804
2194
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1805
2195
|
);
|
|
2196
|
+
CREATE TABLE IF NOT EXISTS sync_changes (
|
|
2197
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2198
|
+
dataset TEXT NOT NULL,
|
|
2199
|
+
record_key TEXT NOT NULL,
|
|
2200
|
+
change_type TEXT NOT NULL,
|
|
2201
|
+
old_json TEXT,
|
|
2202
|
+
new_json TEXT,
|
|
2203
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2204
|
+
);
|
|
2205
|
+
CREATE INDEX IF NOT EXISTS idx_sync_changes_dataset_created_at ON sync_changes(dataset, created_at DESC);
|
|
1806
2206
|
CREATE TABLE IF NOT EXISTS aliases (
|
|
1807
2207
|
name TEXT PRIMARY KEY,
|
|
1808
2208
|
command TEXT NOT NULL,
|
|
1809
2209
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1810
2210
|
);
|
|
2211
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
2212
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2213
|
+
scope TEXT NOT NULL DEFAULT 'user',
|
|
2214
|
+
content TEXT NOT NULL,
|
|
2215
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2216
|
+
);
|
|
2217
|
+
CREATE INDEX IF NOT EXISTS idx_memory_created_at ON memory(created_at DESC);
|
|
1811
2218
|
`);
|
|
1812
2219
|
db.prepare(`
|
|
1813
2220
|
INSERT INTO meta(key, value) VALUES ('schema_version', ?)
|
|
@@ -1836,6 +2243,7 @@ function getDbStatus() {
|
|
|
1836
2243
|
const sessions = db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
1837
2244
|
const local = db.prepare("SELECT COUNT(*) AS count FROM local_records").get();
|
|
1838
2245
|
const cache = db.prepare("SELECT COUNT(*) AS count FROM api_cache").get();
|
|
2246
|
+
const memory = db.prepare("SELECT COUNT(*) AS count FROM memory").get();
|
|
1839
2247
|
return {
|
|
1840
2248
|
status: "ok",
|
|
1841
2249
|
file: DB_FILE,
|
|
@@ -1844,6 +2252,7 @@ function getDbStatus() {
|
|
|
1844
2252
|
sessions: sessions?.count ?? 0,
|
|
1845
2253
|
local_records: local?.count ?? 0,
|
|
1846
2254
|
cache: cache?.count ?? 0,
|
|
2255
|
+
memory: memory?.count ?? 0,
|
|
1847
2256
|
};
|
|
1848
2257
|
} finally {
|
|
1849
2258
|
db.close();
|
|
@@ -2157,6 +2566,7 @@ async function syncDataset(dataset) {
|
|
|
2157
2566
|
if (!DATASETS[dataset]) {
|
|
2158
2567
|
throw new Error(`Неизвестный слой: ${dataset}`);
|
|
2159
2568
|
}
|
|
2569
|
+
await assertPermission("externalApi");
|
|
2160
2570
|
try {
|
|
2161
2571
|
const payload = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
2162
2572
|
const items = normalizeItems(payload);
|
|
@@ -2174,17 +2584,29 @@ function saveLocalRecords(dataset, items) {
|
|
|
2174
2584
|
initDatabase();
|
|
2175
2585
|
const db = openDatabase();
|
|
2176
2586
|
try {
|
|
2587
|
+
const oldRows = db.prepare("SELECT record_key, record_json FROM local_records WHERE dataset = ?").all(dataset);
|
|
2588
|
+
const oldMap = new Map(oldRows.map((row) => [row.record_key, row.record_json]));
|
|
2589
|
+
const newKeys = new Set();
|
|
2177
2590
|
db.prepare("DELETE FROM local_records WHERE dataset = ?").run(dataset);
|
|
2178
2591
|
db.prepare("DELETE FROM local_records_fts WHERE dataset = ?").run(dataset);
|
|
2179
2592
|
const insert = db.prepare("INSERT INTO local_records(dataset, record_key, record_json, searchable_text, synced_at) VALUES (?, ?, ?, ?, datetime('now'))");
|
|
2180
2593
|
const insertFts = db.prepare("INSERT INTO local_records_fts(dataset, record_key, searchable_text) VALUES (?, ?, ?)");
|
|
2594
|
+
const insertChange = db.prepare("INSERT INTO sync_changes(dataset, record_key, change_type, old_json, new_json) VALUES (?, ?, ?, ?, ?)");
|
|
2181
2595
|
for (const item of items) {
|
|
2182
2596
|
const summary = selectPublicSummary(item);
|
|
2183
2597
|
const key = String(summary.inn || item.id || `${dataset}-${Math.random()}`);
|
|
2598
|
+
newKeys.add(key);
|
|
2599
|
+
const newJson = JSON.stringify(item);
|
|
2600
|
+
const oldJson = oldMap.get(key);
|
|
2601
|
+
if (!oldJson) insertChange.run(dataset, key, "added", null, newJson);
|
|
2602
|
+
else if (oldJson !== newJson) insertChange.run(dataset, key, "changed", oldJson, newJson);
|
|
2184
2603
|
const text = JSON.stringify(summary).toLocaleLowerCase("ru-RU");
|
|
2185
|
-
insert.run(dataset, key,
|
|
2604
|
+
insert.run(dataset, key, newJson, text);
|
|
2186
2605
|
insertFts.run(dataset, key, text);
|
|
2187
2606
|
}
|
|
2607
|
+
for (const [key, oldJson] of oldMap.entries()) {
|
|
2608
|
+
if (!newKeys.has(key)) insertChange.run(dataset, key, "removed", oldJson, null);
|
|
2609
|
+
}
|
|
2188
2610
|
} finally {
|
|
2189
2611
|
db.close();
|
|
2190
2612
|
}
|
|
@@ -2213,6 +2635,14 @@ function searchLocalRecords(query, options = {}) {
|
|
|
2213
2635
|
const dataset = options.dataset || "all";
|
|
2214
2636
|
const limit = Number(options.limit || 20);
|
|
2215
2637
|
try {
|
|
2638
|
+
if (options.fts && query) {
|
|
2639
|
+
const ftsQuery = query.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, "")}"`).join(" ");
|
|
2640
|
+
const params = dataset === "all" ? [ftsQuery, limit] : [ftsQuery, dataset, limit];
|
|
2641
|
+
const sql = dataset === "all"
|
|
2642
|
+
? "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? LIMIT ?"
|
|
2643
|
+
: "SELECT r.record_json FROM local_records_fts f JOIN local_records r ON r.dataset=f.dataset AND r.record_key=f.record_key WHERE local_records_fts MATCH ? AND f.dataset = ? LIMIT ?";
|
|
2644
|
+
return db.prepare(sql).all(...params).map((row) => selectPublicSummary(JSON.parse(row.record_json)));
|
|
2645
|
+
}
|
|
2216
2646
|
const params = [];
|
|
2217
2647
|
let sql = "SELECT dataset, record_json FROM local_records";
|
|
2218
2648
|
const where = [];
|
|
@@ -2233,6 +2663,76 @@ function searchLocalRecords(query, options = {}) {
|
|
|
2233
2663
|
}
|
|
2234
2664
|
}
|
|
2235
2665
|
|
|
2666
|
+
function getSyncStatus() {
|
|
2667
|
+
initDatabase();
|
|
2668
|
+
const db = openDatabase();
|
|
2669
|
+
try {
|
|
2670
|
+
return Object.keys(DATASETS).map((dataset) => {
|
|
2671
|
+
const records = db.prepare("SELECT COUNT(*) AS count FROM local_records WHERE dataset = ?").get(dataset);
|
|
2672
|
+
const run = db.prepare("SELECT status, created_at FROM sync_runs WHERE dataset = ? ORDER BY id DESC LIMIT 1").get(dataset);
|
|
2673
|
+
return { dataset, records: records?.count || 0, last_sync: run?.created_at || "-", status: run?.status || "never" };
|
|
2674
|
+
});
|
|
2675
|
+
} finally {
|
|
2676
|
+
db.close();
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function listSyncChanges(dataset) {
|
|
2681
|
+
initDatabase();
|
|
2682
|
+
const db = openDatabase();
|
|
2683
|
+
try {
|
|
2684
|
+
const rows = dataset
|
|
2685
|
+
? db.prepare("SELECT * FROM sync_changes WHERE dataset = ? ORDER BY id DESC LIMIT 50").all(dataset)
|
|
2686
|
+
: db.prepare("SELECT * FROM sync_changes ORDER BY id DESC LIMIT 50").all();
|
|
2687
|
+
return rows.map((row) => ({
|
|
2688
|
+
...row,
|
|
2689
|
+
summary: summarizeChange(row),
|
|
2690
|
+
}));
|
|
2691
|
+
} finally {
|
|
2692
|
+
db.close();
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function summarizeChange(row) {
|
|
2697
|
+
const payload = row.new_json || row.old_json;
|
|
2698
|
+
if (!payload) return "-";
|
|
2699
|
+
try {
|
|
2700
|
+
const item = selectPublicSummary(JSON.parse(payload));
|
|
2701
|
+
return item.name || item.inn || "-";
|
|
2702
|
+
} catch {
|
|
2703
|
+
return "-";
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function findCard(query) {
|
|
2708
|
+
const normalized = query.toLocaleLowerCase("ru-RU");
|
|
2709
|
+
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
2710
|
+
const inn = normalized.match(/\b\d{10,12}\b/)?.[0];
|
|
2711
|
+
const number = normalized.match(/\b\d{1,3}\b/)?.[0];
|
|
2712
|
+
const rows = searchLocalRecords(inn || number || query, { dataset, limit: 20, fts: false });
|
|
2713
|
+
if (inn) return rows.find((row) => String(row.inn) === inn) || null;
|
|
2714
|
+
if (number) return rows.find((row) => String(row.name || "").includes(`№ ${number}`) || String(row.name || "").includes(`№${number}`)) || rows[0] || null;
|
|
2715
|
+
return rows[0] || null;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function runQuality(scope) {
|
|
2719
|
+
const datasets = ["schools", "kindergartens"];
|
|
2720
|
+
const rows = [];
|
|
2721
|
+
for (const dataset of datasets) {
|
|
2722
|
+
if (scope !== "all" && scope !== dataset && !scope.includes("-")) continue;
|
|
2723
|
+
const records = searchLocalRecords("", { dataset, limit: 1000 });
|
|
2724
|
+
const missingPhones = records.filter((item) => !item.phone || item.phone === "-");
|
|
2725
|
+
const invalidEmails = records.filter((item) => item.email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(item.email));
|
|
2726
|
+
const innCounts = new Map();
|
|
2727
|
+
for (const item of records) innCounts.set(item.inn, (innCounts.get(item.inn) || 0) + 1);
|
|
2728
|
+
const duplicateInn = records.filter((item) => item.inn && innCounts.get(item.inn) > 1);
|
|
2729
|
+
if (scope === "all" || scope === dataset || scope === "missing-phones") rows.push({ check: "missing-phones", dataset, count: missingPhones.length, sample: missingPhones[0]?.name || "-" });
|
|
2730
|
+
if (scope === "all" || scope === dataset || scope === "invalid-emails") rows.push({ check: "invalid-emails", dataset, count: invalidEmails.length, sample: invalidEmails[0]?.email || "-" });
|
|
2731
|
+
if (scope === "all" || scope === dataset || scope === "duplicate-inn") rows.push({ check: "duplicate-inn", dataset, count: duplicateInn.length, sample: duplicateInn[0]?.inn || "-" });
|
|
2732
|
+
}
|
|
2733
|
+
return rows;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2236
2736
|
function saveView(name, dataset, args) {
|
|
2237
2737
|
initDatabase();
|
|
2238
2738
|
const db = openDatabase();
|
|
@@ -2298,11 +2798,58 @@ function exportDbSnapshot() {
|
|
|
2298
2798
|
views: listSavedViews(),
|
|
2299
2799
|
aliases: listAliases(),
|
|
2300
2800
|
features: listFeatures(),
|
|
2801
|
+
memory: listMemory(1000),
|
|
2301
2802
|
sessions: listSessions(100),
|
|
2302
2803
|
history: listHistory(100),
|
|
2303
2804
|
};
|
|
2304
2805
|
}
|
|
2305
2806
|
|
|
2807
|
+
function listMemory(limit = 50) {
|
|
2808
|
+
initDatabase();
|
|
2809
|
+
const db = openDatabase();
|
|
2810
|
+
try {
|
|
2811
|
+
return db.prepare("SELECT id, scope, content, created_at FROM memory ORDER BY id DESC LIMIT ?").all(limit);
|
|
2812
|
+
} finally {
|
|
2813
|
+
db.close();
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
function addMemory(content, scope = "user") {
|
|
2818
|
+
initDatabase();
|
|
2819
|
+
const db = openDatabase();
|
|
2820
|
+
try {
|
|
2821
|
+
const result = db.prepare("INSERT INTO memory(scope, content) VALUES (?, ?)").run(scope, content);
|
|
2822
|
+
return Number(result.lastInsertRowid);
|
|
2823
|
+
} finally {
|
|
2824
|
+
db.close();
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function deleteMemory(id) {
|
|
2829
|
+
initDatabase();
|
|
2830
|
+
const db = openDatabase();
|
|
2831
|
+
try {
|
|
2832
|
+
db.prepare("DELETE FROM memory WHERE id = ?").run(id);
|
|
2833
|
+
} finally {
|
|
2834
|
+
db.close();
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
function clearMemory() {
|
|
2839
|
+
initDatabase();
|
|
2840
|
+
const db = openDatabase();
|
|
2841
|
+
try {
|
|
2842
|
+
db.exec("DELETE FROM memory");
|
|
2843
|
+
} finally {
|
|
2844
|
+
db.close();
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function buildMemoryText(limit = 20) {
|
|
2849
|
+
const rows = listMemory(limit).reverse();
|
|
2850
|
+
return rows.map((row) => `- ${row.content}`).join("\n");
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2306
2853
|
function listAliases() {
|
|
2307
2854
|
initDatabase();
|
|
2308
2855
|
const db = openDatabase();
|
|
@@ -2363,6 +2910,7 @@ function inferCommandFromText(text) {
|
|
|
2363
2910
|
async function outputData(value, options, format) {
|
|
2364
2911
|
const text = format === "csv" ? toCsv(value) : `${JSON.stringify(value, null, 2)}\n`;
|
|
2365
2912
|
if (options.output) {
|
|
2913
|
+
await assertPermission("writeFiles");
|
|
2366
2914
|
await writeFile(options.output, text, "utf8");
|
|
2367
2915
|
console.log(`Файл сохранен: ${options.output}`);
|
|
2368
2916
|
return;
|
|
@@ -2466,10 +3014,15 @@ async function aiAsk(args, context = {}) {
|
|
|
2466
3014
|
|
|
2467
3015
|
const config = await loadConfig();
|
|
2468
3016
|
const providerConfig = resolveAiProfile(config, options);
|
|
3017
|
+
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
3018
|
+
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
3019
|
+
if (options.tools && providerConfig.provider === "ollama") {
|
|
3020
|
+
return localToolAsk(question, providerConfig, options);
|
|
3021
|
+
}
|
|
2469
3022
|
applyRuntimeConfig(providerConfig, options.config);
|
|
2470
|
-
const dataContext = await buildDataContext(question);
|
|
3023
|
+
const dataContext = options.bare ? { layers: [], query: { text: question, terms: [], patterns: { numbers: [], inns: [], streets: [], targetLayers: [] } }, schools: [], kindergartens: [] } : await buildDataContext(question);
|
|
2471
3024
|
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
2472
|
-
const historyEnabled = !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
3025
|
+
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
2473
3026
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
2474
3027
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
2475
3028
|
const messages = buildAiMessages(question, dataContext, history, options);
|
|
@@ -2496,15 +3049,20 @@ async function aiAsk(args, context = {}) {
|
|
|
2496
3049
|
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
2497
3050
|
|
|
2498
3051
|
if (options.output) {
|
|
3052
|
+
await assertPermission("writeFiles");
|
|
2499
3053
|
await writeFile(options.output, answer, "utf8");
|
|
2500
3054
|
}
|
|
2501
3055
|
|
|
3056
|
+
if (options["fail-on-empty"] && !answer.trim()) {
|
|
3057
|
+
throw new Error("AI вернул пустой ответ.");
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2502
3060
|
if (options.format === "json" || options.schema === "json") {
|
|
2503
3061
|
printJson({ answer, profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model, sessionId, context: dataContext });
|
|
2504
3062
|
return answer;
|
|
2505
3063
|
}
|
|
2506
3064
|
|
|
2507
|
-
console.log(answer);
|
|
3065
|
+
if (!options.quiet) console.log(answer);
|
|
2508
3066
|
return answer;
|
|
2509
3067
|
}
|
|
2510
3068
|
|
|
@@ -2529,6 +3087,140 @@ function resolveAiProfile(config, options = {}) {
|
|
|
2529
3087
|
};
|
|
2530
3088
|
}
|
|
2531
3089
|
|
|
3090
|
+
async function localToolAsk(question, providerConfig, options) {
|
|
3091
|
+
await ensureLocalData();
|
|
3092
|
+
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
3093
|
+
const validated = validateToolPlan(plan);
|
|
3094
|
+
const result = await executeToolPlan(validated);
|
|
3095
|
+
const answer = formatToolResult(result, options);
|
|
3096
|
+
|
|
3097
|
+
if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
|
|
3098
|
+
recordAskHistory({
|
|
3099
|
+
question,
|
|
3100
|
+
answer,
|
|
3101
|
+
providerConfig,
|
|
3102
|
+
dataContext: { tool_plan: validated, tool_result: result },
|
|
3103
|
+
error: "",
|
|
3104
|
+
sessionId: null,
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
emitEvent(options, "tool_plan", { plan: validated });
|
|
3109
|
+
if (options.output) {
|
|
3110
|
+
await assertPermission("writeFiles");
|
|
3111
|
+
await writeFile(options.output, answer, "utf8");
|
|
3112
|
+
}
|
|
3113
|
+
if (options.format === "json" || options.schema === "json") {
|
|
3114
|
+
printJson({ answer, plan: validated, result });
|
|
3115
|
+
} else {
|
|
3116
|
+
if (!options.quiet) console.log(answer);
|
|
3117
|
+
}
|
|
3118
|
+
return answer;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
async function buildLocalToolPlan(question, providerConfig, options) {
|
|
3122
|
+
const mode = options.reasoning || "verify";
|
|
3123
|
+
const prompt = [
|
|
3124
|
+
"Ты планировщик CLI iola. Верни только JSON.",
|
|
3125
|
+
"Доступные tools: search_local, get_card, export_data, run_report, save_view.",
|
|
3126
|
+
"Схема: {\"steps\":[{\"tool\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
3127
|
+
"Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
|
|
3128
|
+
`Вопрос: ${question}`,
|
|
3129
|
+
].join("\n");
|
|
3130
|
+
|
|
3131
|
+
try {
|
|
3132
|
+
const raw = await callOllama(providerConfig, [{ role: "user", content: prompt }]);
|
|
3133
|
+
const parsed = parseJsonObject(raw);
|
|
3134
|
+
if (mode === "vote") {
|
|
3135
|
+
return chooseBestPlan([parsed, inferToolPlan(question)]);
|
|
3136
|
+
}
|
|
3137
|
+
return parsed;
|
|
3138
|
+
} catch {
|
|
3139
|
+
return inferToolPlan(question);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function parseJsonObject(text) {
|
|
3144
|
+
const match = String(text).match(/\{[\s\S]*\}/);
|
|
3145
|
+
if (!match) throw new Error("JSON-план не найден.");
|
|
3146
|
+
return JSON.parse(match[0]);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
function inferToolPlan(question) {
|
|
3150
|
+
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
3151
|
+
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
3152
|
+
const steps = [];
|
|
3153
|
+
if (normalized.includes("без телефона")) {
|
|
3154
|
+
steps.push({ tool: "run_report", args: { name: "missing-phones" } });
|
|
3155
|
+
} else {
|
|
3156
|
+
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
3157
|
+
steps.push({ tool: "search_local", args: { dataset, query, limit: 20 } });
|
|
3158
|
+
}
|
|
3159
|
+
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
3160
|
+
steps.push({ tool: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
3161
|
+
}
|
|
3162
|
+
return { steps };
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function chooseBestPlan(plans) {
|
|
3166
|
+
return plans.find((plan) => {
|
|
3167
|
+
try {
|
|
3168
|
+
validateToolPlan(plan);
|
|
3169
|
+
return true;
|
|
3170
|
+
} catch {
|
|
3171
|
+
return false;
|
|
3172
|
+
}
|
|
3173
|
+
}) || plans.at(-1);
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function validateToolPlan(plan) {
|
|
3177
|
+
const allowed = new Set(LOCAL_TOOLS);
|
|
3178
|
+
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
3179
|
+
for (const step of plan.steps) {
|
|
3180
|
+
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
3181
|
+
}
|
|
3182
|
+
return plan;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
async function executeToolPlan(plan) {
|
|
3186
|
+
let current = [];
|
|
3187
|
+
const outputs = [];
|
|
3188
|
+
for (const step of plan.steps) {
|
|
3189
|
+
await assertPermission(step.tool);
|
|
3190
|
+
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
3191
|
+
if (step.tool === "search_local") {
|
|
3192
|
+
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
3193
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
3194
|
+
} else if (step.tool === "get_card") {
|
|
3195
|
+
const card = findCard(step.args?.query || "");
|
|
3196
|
+
current = card ? [card] : [];
|
|
3197
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
3198
|
+
} else if (step.tool === "run_report") {
|
|
3199
|
+
current = runQuality(step.args?.name || "all");
|
|
3200
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
3201
|
+
} else if (step.tool === "save_view") {
|
|
3202
|
+
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
3203
|
+
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
3204
|
+
} else if (step.tool === "export_data") {
|
|
3205
|
+
await assertPermission("writeFiles");
|
|
3206
|
+
await runHooks("BeforeExport", { output: step.args?.output || "iola-export.csv", format: step.args?.format || "csv", rows: current.length });
|
|
3207
|
+
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
3208
|
+
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
3209
|
+
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
3210
|
+
}
|
|
3211
|
+
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
3212
|
+
}
|
|
3213
|
+
return { rows: current, outputs };
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function formatToolResult(result, options) {
|
|
3217
|
+
if (options.schema === "json") return JSON.stringify(result, null, 2);
|
|
3218
|
+
const exported = result.outputs.find((item) => item.output);
|
|
3219
|
+
if (exported) return `Готово. Файл сохранен: ${exported.output}. Записей: ${exported.rows}`;
|
|
3220
|
+
if (!result.rows.length) return "Данных не найдено.";
|
|
3221
|
+
return result.rows.slice(0, 10).map((row) => `${row.name || row.check}: ${row.address || row.count || ""}`).join("\n");
|
|
3222
|
+
}
|
|
3223
|
+
|
|
2532
3224
|
function applyRuntimeConfig(target, value) {
|
|
2533
3225
|
if (!value) {
|
|
2534
3226
|
return;
|
|
@@ -2540,6 +3232,36 @@ function applyRuntimeConfig(target, value) {
|
|
|
2540
3232
|
setConfigValue(target, key, parts.join("="));
|
|
2541
3233
|
}
|
|
2542
3234
|
|
|
3235
|
+
async function runHooks(event, payload = {}) {
|
|
3236
|
+
const config = await loadConfig();
|
|
3237
|
+
const commands = config.hooks?.[event] || [];
|
|
3238
|
+
for (const command of commands) {
|
|
3239
|
+
const parts = splitCommandLine(command);
|
|
3240
|
+
if (parts.length === 0) continue;
|
|
3241
|
+
await runCommand(parts[0], parts.slice(1), {
|
|
3242
|
+
inherit: true,
|
|
3243
|
+
env: {
|
|
3244
|
+
IOLA_HOOK_EVENT: event,
|
|
3245
|
+
IOLA_HOOK_PAYLOAD: JSON.stringify(payload),
|
|
3246
|
+
},
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
async function assertPermission(name) {
|
|
3252
|
+
const config = await loadConfig();
|
|
3253
|
+
const permissions = config.permissions || DEFAULT_AI_CONFIG.permissions;
|
|
3254
|
+
if (LOCAL_TOOLS.includes(name)) {
|
|
3255
|
+
if (permissions.localTools?.[name] === false) {
|
|
3256
|
+
throw new Error(`Tool запрещен политикой permissions: ${name}`);
|
|
3257
|
+
}
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (permissions[name] === false) {
|
|
3261
|
+
throw new Error(`Действие запрещено политикой permissions: ${name}`);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
2543
3265
|
function emitEvent(options, type, data) {
|
|
2544
3266
|
if (!options.events) {
|
|
2545
3267
|
return;
|
|
@@ -2548,6 +3270,7 @@ function emitEvent(options, type, data) {
|
|
|
2548
3270
|
}
|
|
2549
3271
|
|
|
2550
3272
|
async function buildDataContext(question) {
|
|
3273
|
+
await assertPermission("externalApi");
|
|
2551
3274
|
const apiBaseUrl = await getApiBaseUrl();
|
|
2552
3275
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
2553
3276
|
const [layers, schools, kindergartens] = await Promise.all([
|
|
@@ -2671,6 +3394,7 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
2671
3394
|
|
|
2672
3395
|
function buildAiMessages(question, dataContext, history, options = {}) {
|
|
2673
3396
|
const sourceLines = buildSourceLines(dataContext);
|
|
3397
|
+
const memoryText = options.bare ? "" : buildMemoryText();
|
|
2674
3398
|
const system = [
|
|
2675
3399
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
2676
3400
|
"Отвечай на русском языке.",
|
|
@@ -2680,6 +3404,7 @@ function buildAiMessages(question, dataContext, history, options = {}) {
|
|
|
2680
3404
|
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
2681
3405
|
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
2682
3406
|
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
3407
|
+
memoryText ? `Учитывай пользовательскую память:\n${memoryText}` : "",
|
|
2683
3408
|
"Отвечай кратко и по делу.",
|
|
2684
3409
|
].filter(Boolean).join(" ");
|
|
2685
3410
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
@@ -2912,7 +3637,7 @@ async function listDataset(dataset, args) {
|
|
|
2912
3637
|
params.set("offset", options.offset || "0");
|
|
2913
3638
|
|
|
2914
3639
|
const data = options.local
|
|
2915
|
-
|
|
3640
|
+
? searchLocalRecords(options.search || options._.join(" ") || "", { dataset, limit: Number(options.limit || 20), fts: options.fts })
|
|
2916
3641
|
: normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
|
|
2917
3642
|
const items = data;
|
|
2918
3643
|
const filtered = applyDatasetFilters(items, options);
|
|
@@ -2968,8 +3693,8 @@ async function searchAll(args) {
|
|
|
2968
3693
|
const limit = Number(options.limit || 5);
|
|
2969
3694
|
const [schools, kindergartens] = options.local
|
|
2970
3695
|
? [
|
|
2971
|
-
searchLocalRecords(query, { dataset: "schools", limit }),
|
|
2972
|
-
searchLocalRecords(query, { dataset: "kindergartens", limit }),
|
|
3696
|
+
searchLocalRecords(query, { dataset: "schools", limit, fts: options.fts }),
|
|
3697
|
+
searchLocalRecords(query, { dataset: "kindergartens", limit, fts: options.fts }),
|
|
2973
3698
|
]
|
|
2974
3699
|
: await Promise.all([
|
|
2975
3700
|
fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
|
|
@@ -3018,12 +3743,12 @@ function parseOptions(args) {
|
|
|
3018
3743
|
|
|
3019
3744
|
for (let index = 0; index < args.length; index += 1) {
|
|
3020
3745
|
const arg = args[index];
|
|
3021
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache") {
|
|
3746
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug") {
|
|
3022
3747
|
result[arg.slice(2)] = true;
|
|
3023
3748
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
3024
3749
|
result.check = true;
|
|
3025
3750
|
result[arg.slice(2)] = true;
|
|
3026
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save") {
|
|
3751
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--base-url" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--debug-file") {
|
|
3027
3752
|
result[arg.slice(2)] = args[index + 1];
|
|
3028
3753
|
index += 1;
|
|
3029
3754
|
} else {
|
|
@@ -3034,6 +3759,21 @@ function parseOptions(args) {
|
|
|
3034
3759
|
return result;
|
|
3035
3760
|
}
|
|
3036
3761
|
|
|
3762
|
+
function parseGlobalOptions(argv) {
|
|
3763
|
+
const result = { args: [] };
|
|
3764
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
3765
|
+
const arg = argv[index];
|
|
3766
|
+
if (arg === "--help" || arg === "-h") result.help = true;
|
|
3767
|
+
else if (arg === "--debug") result.debug = true;
|
|
3768
|
+
else if (arg === "--no-color") result.noColor = true;
|
|
3769
|
+
else if (arg === "--debug-file") {
|
|
3770
|
+
result.debugFile = argv[index + 1];
|
|
3771
|
+
index += 1;
|
|
3772
|
+
} else result.args.push(arg);
|
|
3773
|
+
}
|
|
3774
|
+
return result;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3037
3777
|
function splitCommandLine(line) {
|
|
3038
3778
|
const result = [];
|
|
3039
3779
|
let current = "";
|
|
@@ -3497,9 +4237,17 @@ async function printAiConfigField(field) {
|
|
|
3497
4237
|
|
|
3498
4238
|
function runCommand(command, args, options = {}) {
|
|
3499
4239
|
return new Promise((resolve, reject) => {
|
|
4240
|
+
if (process.env.IOLA_DEBUG) {
|
|
4241
|
+
console.error(`[debug] run: ${command} ${args.join(" ")}`);
|
|
4242
|
+
debugLog(`run: ${command} ${args.join(" ")}`);
|
|
4243
|
+
}
|
|
3500
4244
|
const child = execFile(command, args, {
|
|
3501
4245
|
windowsHide: true,
|
|
3502
4246
|
maxBuffer: 1024 * 1024 * 5,
|
|
4247
|
+
env: {
|
|
4248
|
+
...process.env,
|
|
4249
|
+
...(options.env || {}),
|
|
4250
|
+
},
|
|
3503
4251
|
}, (error, stdout, stderr) => {
|
|
3504
4252
|
if (error) {
|
|
3505
4253
|
if (process.platform === "win32" && (error.code === "ENOENT" || error.code === "EINVAL") && !options.cmdFallback) {
|
|
@@ -3528,6 +4276,11 @@ function runCommand(command, args, options = {}) {
|
|
|
3528
4276
|
});
|
|
3529
4277
|
}
|
|
3530
4278
|
|
|
4279
|
+
function debugLog(message) {
|
|
4280
|
+
if (!process.env.IOLA_DEBUG_FILE) return;
|
|
4281
|
+
appendFile(process.env.IOLA_DEBUG_FILE, `[${new Date().toISOString()}] ${message}\n`, "utf8").catch(() => {});
|
|
4282
|
+
}
|
|
4283
|
+
|
|
3531
4284
|
function quoteWindowsCommand(command, args) {
|
|
3532
4285
|
return [command, ...args].map((value) => {
|
|
3533
4286
|
const text = String(value);
|