@iola_adm/iola-cli 0.1.17 → 0.1.20
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 +89 -0
- package/bin/iola.js +0 -0
- package/package.json +2 -1
- package/src/cli.js +476 -12
- 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/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
CLI для работы с открытыми данными городского округа "Город Йошкар-Ола".
|
|
6
6
|
|
|
7
|
+
Подробная документация: [GitHub Wiki](https://github.com/adm-iola/iola-cli/wiki).
|
|
8
|
+
|
|
7
9
|
Проект использует публичные endpoints:
|
|
8
10
|
|
|
9
11
|
- `https://apiiola.yasg.ru/api/v1`
|
|
@@ -81,6 +83,8 @@ iola init
|
|
|
81
83
|
|
|
82
84
|
## Команды
|
|
83
85
|
|
|
86
|
+
Полный справочник команд: [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды).
|
|
87
|
+
|
|
84
88
|
```bash
|
|
85
89
|
iola banner
|
|
86
90
|
iola agent
|
|
@@ -96,6 +100,15 @@ iola resume 1 "продолжи"
|
|
|
96
100
|
iola fork 1 "новый вопрос"
|
|
97
101
|
iola features list
|
|
98
102
|
iola features enable api-cache
|
|
103
|
+
iola permissions list
|
|
104
|
+
iola permissions deny export_data
|
|
105
|
+
iola permissions allow export_data
|
|
106
|
+
iola memory add "Отвечай кратко и указывай источник данных"
|
|
107
|
+
iola memory show
|
|
108
|
+
iola hooks events
|
|
109
|
+
iola hooks add AfterSync "iola quality"
|
|
110
|
+
iola agents list
|
|
111
|
+
iola agents run quality-checker "проверь школы"
|
|
99
112
|
iola mcp status
|
|
100
113
|
iola mcp list
|
|
101
114
|
iola mcp install codex
|
|
@@ -127,6 +140,7 @@ iola version --check
|
|
|
127
140
|
iola ask "Найди школу 29"
|
|
128
141
|
iola ask "Найди школу 29" --profile codex --events --output answer.txt
|
|
129
142
|
iola ask "Найди школу 29" --schema json --no-history
|
|
143
|
+
iola ask "Найди школу 29" --bare --quiet
|
|
130
144
|
iola ask "выгрузи школы на Петрова в csv" --profile local --tools --reasoning verify
|
|
131
145
|
iola data schools --format csv --output schools.csv
|
|
132
146
|
iola data schools --limit 10
|
|
@@ -165,6 +179,8 @@ iola setup codex
|
|
|
165
179
|
|
|
166
180
|
## Интерактивный режим
|
|
167
181
|
|
|
182
|
+
Подробнее: [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды).
|
|
183
|
+
|
|
168
184
|
```bash
|
|
169
185
|
iola agent
|
|
170
186
|
```
|
|
@@ -179,6 +195,11 @@ iola agent
|
|
|
179
195
|
/sessions
|
|
180
196
|
/resume 1
|
|
181
197
|
/features list
|
|
198
|
+
/permissions
|
|
199
|
+
/tools
|
|
200
|
+
/memory show
|
|
201
|
+
/hooks list
|
|
202
|
+
/agents list
|
|
182
203
|
/mcp status
|
|
183
204
|
/config get
|
|
184
205
|
/layers
|
|
@@ -213,6 +234,8 @@ iola agent
|
|
|
213
234
|
|
|
214
235
|
## AI-запросы
|
|
215
236
|
|
|
237
|
+
Подробнее: [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили).
|
|
238
|
+
|
|
216
239
|
Локальная модель через Ollama:
|
|
217
240
|
|
|
218
241
|
```bash
|
|
@@ -315,6 +338,9 @@ CLI дает прямой терминальный доступ к открыт
|
|
|
315
338
|
|
|
316
339
|
## Локальная SQLite-БД
|
|
317
340
|
|
|
341
|
+
Подробнее: [Первый запуск](https://github.com/adm-iola/iola-cli/wiki/Первый-запуск) и
|
|
342
|
+
[Команды](https://github.com/adm-iola/iola-cli/wiki/Команды).
|
|
343
|
+
|
|
318
344
|
CLI использует встроенный `node:sqlite` и хранит локальную БД в профиле
|
|
319
345
|
пользователя:
|
|
320
346
|
|
|
@@ -421,6 +447,8 @@ iola run "выгрузи школы на Петрова в csv"
|
|
|
421
447
|
|
|
422
448
|
## Локальный tool-agent для слабых моделей
|
|
423
449
|
|
|
450
|
+
Подробнее: [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент).
|
|
451
|
+
|
|
424
452
|
Для локального профиля Ollama доступен режим `--tools`. Он сделан специально
|
|
425
453
|
для маленьких моделей, которые хуже отвечают свободным текстом, но могут быть
|
|
426
454
|
полезны как планировщик действий.
|
|
@@ -468,6 +496,67 @@ iola diff
|
|
|
468
496
|
iola diff schools
|
|
469
497
|
```
|
|
470
498
|
|
|
499
|
+
## Permissions, memory, hooks и agents
|
|
500
|
+
|
|
501
|
+
Подробнее: [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
|
|
502
|
+
и [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды).
|
|
503
|
+
|
|
504
|
+
Permissions ограничивают, что может делать локальный tool-agent:
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
iola permissions list
|
|
508
|
+
iola permissions deny export_data
|
|
509
|
+
iola permissions allow export_data
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Memory хранит пользовательские предпочтения в локальной SQLite-БД и добавляет их
|
|
513
|
+
в AI-контекст, кроме режима `--bare`:
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
iola memory add "Если найден конкретный объект, показывай ИНН"
|
|
517
|
+
iola memory show
|
|
518
|
+
iola memory export
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Hooks запускают локальные команды на события CLI:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
iola hooks events
|
|
525
|
+
iola hooks add AfterSync "iola quality"
|
|
526
|
+
iola hooks list
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Поддерживаемые события: `SessionStart`, `BeforeTool`, `AfterTool`,
|
|
530
|
+
`AfterSync`, `BeforeExport`, `SessionEnd`.
|
|
531
|
+
|
|
532
|
+
Agents - готовые режимы работы поверх AI-профилей и локальных инструментов:
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
iola agents list
|
|
536
|
+
iola agents run quality-checker "проверь школы"
|
|
537
|
+
iola agents run exporter "выгрузи школы на Петрова в csv"
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Для скриптов доступны минимальные режимы:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
iola ask "Найди школу 29" --bare
|
|
544
|
+
iola ask "Найди школу 29" --quiet
|
|
545
|
+
iola ask "Найди школу 29" --schema json --fail-on-empty
|
|
546
|
+
iola --debug --debug-file iola-debug.log doctor
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Wiki
|
|
550
|
+
|
|
551
|
+
Подробные пользовательские инструкции вынесены в GitHub Wiki:
|
|
552
|
+
|
|
553
|
+
- [Установка](https://github.com/adm-iola/iola-cli/wiki/Установка)
|
|
554
|
+
- [Первый запуск](https://github.com/adm-iola/iola-cli/wiki/Первый-запуск)
|
|
555
|
+
- [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили)
|
|
556
|
+
- [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
|
|
557
|
+
- [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды)
|
|
558
|
+
- [Решение проблем](https://github.com/adm-iola/iola-cli/wiki/Решение-проблем)
|
|
559
|
+
|
|
471
560
|
## Переменные окружения
|
|
472
561
|
|
|
473
562
|
```bash
|
package/bin/iola.js
CHANGED
|
File without changes
|
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.20",
|
|
4
4
|
"description": "CLI и AI-агент для работы с открытыми данными городского округа Йошкар-Ола.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/adm-iola/iola-cli#readme",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"bin",
|
|
25
25
|
"src",
|
|
26
|
+
"wiki",
|
|
26
27
|
"docs/assets/readme-header.png",
|
|
27
28
|
"README.md",
|
|
28
29
|
"LICENSE"
|
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,6 +153,10 @@ 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],
|
|
@@ -125,6 +188,28 @@ const COMMANDS = new Map([
|
|
|
125
188
|
]);
|
|
126
189
|
|
|
127
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;
|
|
128
213
|
const [command = "help", ...args] = argv;
|
|
129
214
|
const nodeStatus = getNodeRequirementStatus();
|
|
130
215
|
if (!nodeStatus.ok && !["help", "version", "doctor", "init"].includes(command)) {
|
|
@@ -142,7 +227,7 @@ export async function main(argv) {
|
|
|
142
227
|
throw new Error(`Unknown command: ${command}\nRun "iola help" to see available commands.`);
|
|
143
228
|
}
|
|
144
229
|
|
|
145
|
-
await handler(args);
|
|
230
|
+
await handler(runtime.debugFile ? [...args, "--debug-file", runtime.debugFile] : args);
|
|
146
231
|
}
|
|
147
232
|
|
|
148
233
|
async function showHelp() {
|
|
@@ -163,6 +248,10 @@ Usage:
|
|
|
163
248
|
iola resume SESSION_ID [TEXT]
|
|
164
249
|
iola fork SESSION_ID [TEXT]
|
|
165
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
|
|
166
255
|
iola mcp list|status|install|remove
|
|
167
256
|
iola cache status|warm|clear
|
|
168
257
|
iola sync [--dataset schools|kindergartens]
|
|
@@ -183,7 +272,7 @@ Usage:
|
|
|
183
272
|
iola config set api.mcpBaseUrl URL
|
|
184
273
|
iola config reset
|
|
185
274
|
iola update
|
|
186
|
-
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--reasoning fast|verify|vote] [--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]
|
|
187
276
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
188
277
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
189
278
|
iola ai context TEXT [--json]
|
|
@@ -222,6 +311,7 @@ Requirements:
|
|
|
222
311
|
async function startAgent() {
|
|
223
312
|
showBanner();
|
|
224
313
|
console.log("Интерактивный режим. Введите /help для списка команд, /exit для выхода.");
|
|
314
|
+
await runHooks("SessionStart", { mode: "agent" });
|
|
225
315
|
|
|
226
316
|
const rl = readline.createInterface({ input, output, prompt: "iola> " });
|
|
227
317
|
const state = {
|
|
@@ -256,6 +346,7 @@ async function startAgent() {
|
|
|
256
346
|
if (!closed) {
|
|
257
347
|
rl.close();
|
|
258
348
|
}
|
|
349
|
+
await runHooks("SessionEnd", { mode: "agent" });
|
|
259
350
|
}
|
|
260
351
|
|
|
261
352
|
async function handleAgentLine(line, state) {
|
|
@@ -317,6 +408,31 @@ async function handleAgentLine(line, state) {
|
|
|
317
408
|
return false;
|
|
318
409
|
}
|
|
319
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
|
+
|
|
320
436
|
if (command === "mcp") {
|
|
321
437
|
await handleMcp(args);
|
|
322
438
|
return false;
|
|
@@ -421,6 +537,11 @@ async function handleAgentLine(line, state) {
|
|
|
421
537
|
resume: ["resume", args],
|
|
422
538
|
fork: ["fork", args],
|
|
423
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]],
|
|
424
545
|
mcp: ["mcp", args],
|
|
425
546
|
cache: ["cache", args],
|
|
426
547
|
sync: ["sync", args],
|
|
@@ -455,6 +576,11 @@ function printAgentHelp() {
|
|
|
455
576
|
/sessions
|
|
456
577
|
/resume SESSION_ID
|
|
457
578
|
/features list
|
|
579
|
+
/permissions
|
|
580
|
+
/tools
|
|
581
|
+
/memory show
|
|
582
|
+
/hooks list
|
|
583
|
+
/agents list
|
|
458
584
|
/mcp status
|
|
459
585
|
/cache status
|
|
460
586
|
/sync
|
|
@@ -1063,6 +1189,208 @@ async function handleFeatures(args) {
|
|
|
1063
1189
|
throw new Error("Команды features: list, enable NAME, disable NAME.");
|
|
1064
1190
|
}
|
|
1065
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
|
+
|
|
1066
1394
|
async function handleMcp(args) {
|
|
1067
1395
|
const [action = "status", target = "codex"] = args;
|
|
1068
1396
|
|
|
@@ -1131,12 +1459,14 @@ async function handleSync(args) {
|
|
|
1131
1459
|
]);
|
|
1132
1460
|
return;
|
|
1133
1461
|
}
|
|
1462
|
+
await assertPermission("sync");
|
|
1134
1463
|
const options = parseOptions(args);
|
|
1135
1464
|
const datasets = options.dataset ? [options.dataset] : Object.keys(DATASETS);
|
|
1136
1465
|
const rows = [];
|
|
1137
1466
|
for (const dataset of datasets) {
|
|
1138
1467
|
rows.push(await syncDataset(dataset));
|
|
1139
1468
|
}
|
|
1469
|
+
await runHooks("AfterSync", { datasets, rows });
|
|
1140
1470
|
printTable(rows, [
|
|
1141
1471
|
["dataset", "Слой"],
|
|
1142
1472
|
["records", "Записей"],
|
|
@@ -1878,6 +2208,13 @@ function initDatabase() {
|
|
|
1878
2208
|
command TEXT NOT NULL,
|
|
1879
2209
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1880
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);
|
|
1881
2218
|
`);
|
|
1882
2219
|
db.prepare(`
|
|
1883
2220
|
INSERT INTO meta(key, value) VALUES ('schema_version', ?)
|
|
@@ -1906,6 +2243,7 @@ function getDbStatus() {
|
|
|
1906
2243
|
const sessions = db.prepare("SELECT COUNT(*) AS count FROM sessions").get();
|
|
1907
2244
|
const local = db.prepare("SELECT COUNT(*) AS count FROM local_records").get();
|
|
1908
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();
|
|
1909
2247
|
return {
|
|
1910
2248
|
status: "ok",
|
|
1911
2249
|
file: DB_FILE,
|
|
@@ -1914,6 +2252,7 @@ function getDbStatus() {
|
|
|
1914
2252
|
sessions: sessions?.count ?? 0,
|
|
1915
2253
|
local_records: local?.count ?? 0,
|
|
1916
2254
|
cache: cache?.count ?? 0,
|
|
2255
|
+
memory: memory?.count ?? 0,
|
|
1917
2256
|
};
|
|
1918
2257
|
} finally {
|
|
1919
2258
|
db.close();
|
|
@@ -2227,6 +2566,7 @@ async function syncDataset(dataset) {
|
|
|
2227
2566
|
if (!DATASETS[dataset]) {
|
|
2228
2567
|
throw new Error(`Неизвестный слой: ${dataset}`);
|
|
2229
2568
|
}
|
|
2569
|
+
await assertPermission("externalApi");
|
|
2230
2570
|
try {
|
|
2231
2571
|
const payload = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
|
|
2232
2572
|
const items = normalizeItems(payload);
|
|
@@ -2458,11 +2798,58 @@ function exportDbSnapshot() {
|
|
|
2458
2798
|
views: listSavedViews(),
|
|
2459
2799
|
aliases: listAliases(),
|
|
2460
2800
|
features: listFeatures(),
|
|
2801
|
+
memory: listMemory(1000),
|
|
2461
2802
|
sessions: listSessions(100),
|
|
2462
2803
|
history: listHistory(100),
|
|
2463
2804
|
};
|
|
2464
2805
|
}
|
|
2465
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
|
+
|
|
2466
2853
|
function listAliases() {
|
|
2467
2854
|
initDatabase();
|
|
2468
2855
|
const db = openDatabase();
|
|
@@ -2523,6 +2910,7 @@ function inferCommandFromText(text) {
|
|
|
2523
2910
|
async function outputData(value, options, format) {
|
|
2524
2911
|
const text = format === "csv" ? toCsv(value) : `${JSON.stringify(value, null, 2)}\n`;
|
|
2525
2912
|
if (options.output) {
|
|
2913
|
+
await assertPermission("writeFiles");
|
|
2526
2914
|
await writeFile(options.output, text, "utf8");
|
|
2527
2915
|
console.log(`Файл сохранен: ${options.output}`);
|
|
2528
2916
|
return;
|
|
@@ -2626,13 +3014,15 @@ async function aiAsk(args, context = {}) {
|
|
|
2626
3014
|
|
|
2627
3015
|
const config = await loadConfig();
|
|
2628
3016
|
const providerConfig = resolveAiProfile(config, options);
|
|
3017
|
+
if (providerConfig.provider === "codex") await assertPermission("codex");
|
|
3018
|
+
if (providerConfig.provider !== "ollama") await assertPermission("externalAi");
|
|
2629
3019
|
if (options.tools && providerConfig.provider === "ollama") {
|
|
2630
3020
|
return localToolAsk(question, providerConfig, options);
|
|
2631
3021
|
}
|
|
2632
3022
|
applyRuntimeConfig(providerConfig, options.config);
|
|
2633
|
-
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);
|
|
2634
3024
|
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
2635
|
-
const historyEnabled = !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
3025
|
+
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
2636
3026
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
2637
3027
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
2638
3028
|
const messages = buildAiMessages(question, dataContext, history, options);
|
|
@@ -2659,15 +3049,20 @@ async function aiAsk(args, context = {}) {
|
|
|
2659
3049
|
emitEvent(options, "answer", { length: answer.length, sessionId });
|
|
2660
3050
|
|
|
2661
3051
|
if (options.output) {
|
|
3052
|
+
await assertPermission("writeFiles");
|
|
2662
3053
|
await writeFile(options.output, answer, "utf8");
|
|
2663
3054
|
}
|
|
2664
3055
|
|
|
3056
|
+
if (options["fail-on-empty"] && !answer.trim()) {
|
|
3057
|
+
throw new Error("AI вернул пустой ответ.");
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2665
3060
|
if (options.format === "json" || options.schema === "json") {
|
|
2666
3061
|
printJson({ answer, profile: providerConfig.name, provider: providerConfig.provider, model: providerConfig.model, sessionId, context: dataContext });
|
|
2667
3062
|
return answer;
|
|
2668
3063
|
}
|
|
2669
3064
|
|
|
2670
|
-
console.log(answer);
|
|
3065
|
+
if (!options.quiet) console.log(answer);
|
|
2671
3066
|
return answer;
|
|
2672
3067
|
}
|
|
2673
3068
|
|
|
@@ -2711,11 +3106,14 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
2711
3106
|
}
|
|
2712
3107
|
|
|
2713
3108
|
emitEvent(options, "tool_plan", { plan: validated });
|
|
2714
|
-
if (options.output)
|
|
3109
|
+
if (options.output) {
|
|
3110
|
+
await assertPermission("writeFiles");
|
|
3111
|
+
await writeFile(options.output, answer, "utf8");
|
|
3112
|
+
}
|
|
2715
3113
|
if (options.format === "json" || options.schema === "json") {
|
|
2716
3114
|
printJson({ answer, plan: validated, result });
|
|
2717
3115
|
} else {
|
|
2718
|
-
console.log(answer);
|
|
3116
|
+
if (!options.quiet) console.log(answer);
|
|
2719
3117
|
}
|
|
2720
3118
|
return answer;
|
|
2721
3119
|
}
|
|
@@ -2776,7 +3174,7 @@ function chooseBestPlan(plans) {
|
|
|
2776
3174
|
}
|
|
2777
3175
|
|
|
2778
3176
|
function validateToolPlan(plan) {
|
|
2779
|
-
const allowed = new Set(
|
|
3177
|
+
const allowed = new Set(LOCAL_TOOLS);
|
|
2780
3178
|
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
2781
3179
|
for (const step of plan.steps) {
|
|
2782
3180
|
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
@@ -2788,6 +3186,8 @@ async function executeToolPlan(plan) {
|
|
|
2788
3186
|
let current = [];
|
|
2789
3187
|
const outputs = [];
|
|
2790
3188
|
for (const step of plan.steps) {
|
|
3189
|
+
await assertPermission(step.tool);
|
|
3190
|
+
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
2791
3191
|
if (step.tool === "search_local") {
|
|
2792
3192
|
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
2793
3193
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
@@ -2802,10 +3202,13 @@ async function executeToolPlan(plan) {
|
|
|
2802
3202
|
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
2803
3203
|
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
2804
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 });
|
|
2805
3207
|
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
2806
3208
|
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
2807
3209
|
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
2808
3210
|
}
|
|
3211
|
+
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
2809
3212
|
}
|
|
2810
3213
|
return { rows: current, outputs };
|
|
2811
3214
|
}
|
|
@@ -2829,6 +3232,36 @@ function applyRuntimeConfig(target, value) {
|
|
|
2829
3232
|
setConfigValue(target, key, parts.join("="));
|
|
2830
3233
|
}
|
|
2831
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
|
+
|
|
2832
3265
|
function emitEvent(options, type, data) {
|
|
2833
3266
|
if (!options.events) {
|
|
2834
3267
|
return;
|
|
@@ -2837,6 +3270,7 @@ function emitEvent(options, type, data) {
|
|
|
2837
3270
|
}
|
|
2838
3271
|
|
|
2839
3272
|
async function buildDataContext(question) {
|
|
3273
|
+
await assertPermission("externalApi");
|
|
2840
3274
|
const apiBaseUrl = await getApiBaseUrl();
|
|
2841
3275
|
const mcpBaseUrl = await getMcpBaseUrl();
|
|
2842
3276
|
const [layers, schools, kindergartens] = await Promise.all([
|
|
@@ -2960,6 +3394,7 @@ function scoreItem(item, terms, patterns, layer) {
|
|
|
2960
3394
|
|
|
2961
3395
|
function buildAiMessages(question, dataContext, history, options = {}) {
|
|
2962
3396
|
const sourceLines = buildSourceLines(dataContext);
|
|
3397
|
+
const memoryText = options.bare ? "" : buildMemoryText();
|
|
2963
3398
|
const system = [
|
|
2964
3399
|
"Ты терминальный AI-ассистент CLI-проекта Йошкар-Олы.",
|
|
2965
3400
|
"Отвечай на русском языке.",
|
|
@@ -2969,6 +3404,7 @@ function buildAiMessages(question, dataContext, history, options = {}) {
|
|
|
2969
3404
|
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
2970
3405
|
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
2971
3406
|
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
3407
|
+
memoryText ? `Учитывай пользовательскую память:\n${memoryText}` : "",
|
|
2972
3408
|
"Отвечай кратко и по делу.",
|
|
2973
3409
|
].filter(Boolean).join(" ");
|
|
2974
3410
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
@@ -3307,12 +3743,12 @@ function parseOptions(args) {
|
|
|
3307
3743
|
|
|
3308
3744
|
for (let index = 0; index < args.length; index += 1) {
|
|
3309
3745
|
const arg = args[index];
|
|
3310
|
-
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--fts") {
|
|
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") {
|
|
3311
3747
|
result[arg.slice(2)] = true;
|
|
3312
3748
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
3313
3749
|
result.check = true;
|
|
3314
3750
|
result[arg.slice(2)] = true;
|
|
3315
|
-
} 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") {
|
|
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") {
|
|
3316
3752
|
result[arg.slice(2)] = args[index + 1];
|
|
3317
3753
|
index += 1;
|
|
3318
3754
|
} else {
|
|
@@ -3323,6 +3759,21 @@ function parseOptions(args) {
|
|
|
3323
3759
|
return result;
|
|
3324
3760
|
}
|
|
3325
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
|
+
|
|
3326
3777
|
function splitCommandLine(line) {
|
|
3327
3778
|
const result = [];
|
|
3328
3779
|
let current = "";
|
|
@@ -3786,9 +4237,17 @@ async function printAiConfigField(field) {
|
|
|
3786
4237
|
|
|
3787
4238
|
function runCommand(command, args, options = {}) {
|
|
3788
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
|
+
}
|
|
3789
4244
|
const child = execFile(command, args, {
|
|
3790
4245
|
windowsHide: true,
|
|
3791
4246
|
maxBuffer: 1024 * 1024 * 5,
|
|
4247
|
+
env: {
|
|
4248
|
+
...process.env,
|
|
4249
|
+
...(options.env || {}),
|
|
4250
|
+
},
|
|
3792
4251
|
}, (error, stdout, stderr) => {
|
|
3793
4252
|
if (error) {
|
|
3794
4253
|
if (process.platform === "win32" && (error.code === "ENOENT" || error.code === "EINVAL") && !options.cmdFallback) {
|
|
@@ -3817,6 +4276,11 @@ function runCommand(command, args, options = {}) {
|
|
|
3817
4276
|
});
|
|
3818
4277
|
}
|
|
3819
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
|
+
|
|
3820
4284
|
function quoteWindowsCommand(command, args) {
|
|
3821
4285
|
return [command, ...args].map((value) => {
|
|
3822
4286
|
const text = String(value);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AI profiles
|
|
2
|
+
|
|
3
|
+
CLI поддерживает несколько AI-профилей одновременно.
|
|
4
|
+
|
|
5
|
+
## Локальная модель
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
iola ai setup ollama
|
|
9
|
+
iola ai profile use local
|
|
10
|
+
iola ask "найди школы на Петрова"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## OpenAI
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
iola ai key set openai
|
|
17
|
+
iola ai setup openai --model gpt-4.1-mini
|
|
18
|
+
iola ask "найди школу 29" --profile openai
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## OpenRouter
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
iola ai key set openrouter
|
|
25
|
+
iola ai setup openrouter --model openai/gpt-4.1-mini
|
|
26
|
+
iola ai models openrouter --search qwen
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Codex CLI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
codex login
|
|
33
|
+
iola ai setup codex --model gpt-5.5
|
|
34
|
+
iola setup codex
|
|
35
|
+
iola ask "проверь данные школы 29" --profile codex
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Переключение
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
iola ai profiles
|
|
42
|
+
iola ai profile use local
|
|
43
|
+
iola ai profile use openrouter
|
|
44
|
+
```
|
package/wiki/Home.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# iola-cli
|
|
2
|
+
|
|
3
|
+
`iola-cli` - терминальный инструмент и AI-агент для работы с открытыми данными городского округа "Город Йошкар-Ола".
|
|
4
|
+
|
|
5
|
+
Основные сценарии:
|
|
6
|
+
|
|
7
|
+
- поиск школ и детских садов;
|
|
8
|
+
- просмотр карточек организаций;
|
|
9
|
+
- локальная синхронизация данных в SQLite;
|
|
10
|
+
- проверка качества данных;
|
|
11
|
+
- выгрузка CSV/JSON;
|
|
12
|
+
- работа с локальной моделью Ollama;
|
|
13
|
+
- работа с OpenAI, OpenRouter и Codex CLI;
|
|
14
|
+
- подключение публичного MCP-сервера.
|
|
15
|
+
|
|
16
|
+
Быстрый старт:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @iola_adm/iola-cli
|
|
20
|
+
iola init
|
|
21
|
+
iola search "Петрова"
|
|
22
|
+
iola ask "найди школу 29"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Подробные страницы:
|
|
26
|
+
|
|
27
|
+
- [Установка](Установка)
|
|
28
|
+
- [Первый запуск](Первый-запуск)
|
|
29
|
+
- [AI-профили](AI-профили)
|
|
30
|
+
- [Локальный инструментальный агент](Локальный-инструментальный-агент)
|
|
31
|
+
- [Команды](Команды)
|
|
32
|
+
- [Решение проблем](Решение-проблем)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
Основные команды:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
iola --help
|
|
7
|
+
iola init
|
|
8
|
+
iola doctor
|
|
9
|
+
iola update
|
|
10
|
+
iola version --check
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Данные:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
iola search "Петрова"
|
|
17
|
+
iola search "Петрова" --local --fts
|
|
18
|
+
iola schools --limit 10
|
|
19
|
+
iola kindergartens --search "29"
|
|
20
|
+
iola card "школа 29"
|
|
21
|
+
iola data schools --format csv --output schools.csv
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Локальная БД:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
iola db status
|
|
28
|
+
iola sync
|
|
29
|
+
iola sync status
|
|
30
|
+
iola diff
|
|
31
|
+
iola quality
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
AI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
iola ask "найди школу 29"
|
|
38
|
+
iola ask "найди школу 29" --schema json
|
|
39
|
+
iola ask "найди школу 29" --bare --quiet
|
|
40
|
+
iola agents list
|
|
41
|
+
iola agents run quality-checker "проверь школы"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Интерактивный режим:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
iola agent
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Внутри agent:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
/help
|
|
54
|
+
/tools
|
|
55
|
+
/memory show
|
|
56
|
+
/permissions
|
|
57
|
+
/quality
|
|
58
|
+
/sync
|
|
59
|
+
/exit
|
|
60
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Local tool-agent
|
|
2
|
+
|
|
3
|
+
Режим `--tools` предназначен для локальных моделей Ollama. Маленькая модель не выполняет действия напрямую, а предлагает JSON-план. CLI проверяет план и выполняет только разрешенные встроенные tools.
|
|
4
|
+
|
|
5
|
+
Пример:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
iola ask "выгрузи школы на Петрова в csv" --profile local --tools
|
|
9
|
+
iola ask "найди детсады без телефона" --profile local --tools --reasoning verify
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Доступные tools:
|
|
13
|
+
|
|
14
|
+
- `search_local`
|
|
15
|
+
- `get_card`
|
|
16
|
+
- `export_data`
|
|
17
|
+
- `run_report`
|
|
18
|
+
- `save_view`
|
|
19
|
+
|
|
20
|
+
Управление разрешениями:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
iola permissions list
|
|
24
|
+
iola permissions deny export_data
|
|
25
|
+
iola permissions allow export_data
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Режимы планирования:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
--reasoning fast
|
|
32
|
+
--reasoning verify
|
|
33
|
+
--reasoning vote
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Если Ollama недоступна или модель вернула некорректный JSON, CLI использует fallback-планировщик для типовых запросов.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# First run
|
|
2
|
+
|
|
3
|
+
Первый запуск:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
iola init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Команда проверит:
|
|
10
|
+
|
|
11
|
+
- Node.js и npm;
|
|
12
|
+
- локальную SQLite-БД;
|
|
13
|
+
- доступность публичного API/MCP;
|
|
14
|
+
- Ollama и подходящую локальную модель;
|
|
15
|
+
- обновления npm-пакета.
|
|
16
|
+
|
|
17
|
+
Диагностика:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
iola doctor
|
|
21
|
+
iola doctor --summary
|
|
22
|
+
iola ai doctor
|
|
23
|
+
iola db status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Синхронизация локальной базы:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
iola sync
|
|
30
|
+
iola sync status
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Проверка данных:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
iola quality
|
|
37
|
+
iola diff
|
|
38
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Команда iola не найдена
|
|
4
|
+
|
|
5
|
+
Проверьте глобальную установку:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @iola_adm/iola-cli
|
|
9
|
+
npm bin -g
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Нужна новая версия Node.js
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node --version
|
|
16
|
+
iola init --upgrade-node
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Ollama недоступна
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ollama --version
|
|
23
|
+
ollama serve
|
|
24
|
+
iola ai doctor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## OpenAI/OpenRouter key не найден
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
iola ai key status
|
|
31
|
+
iola ai key set openai
|
|
32
|
+
iola ai key set openrouter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Нет локальных данных
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
iola sync
|
|
39
|
+
iola sync status
|
|
40
|
+
iola search "школа" --local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Нужен подробный лог
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
iola --debug --debug-file iola-debug.log doctor
|
|
47
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Требования
|
|
4
|
+
|
|
5
|
+
Нужен Node.js `22.5.0` или новее:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node --version
|
|
9
|
+
npm --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Установка Node.js:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Windows
|
|
16
|
+
winget install OpenJS.NodeJS.LTS
|
|
17
|
+
|
|
18
|
+
# macOS
|
|
19
|
+
brew install node
|
|
20
|
+
|
|
21
|
+
# Linux
|
|
22
|
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
|
23
|
+
sudo apt-get install -y nodejs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Установка CLI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @iola_adm/iola-cli
|
|
30
|
+
iola --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Без глобальной установки:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y @iola_adm/iola-cli init
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Ollama для локальной модели
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Windows
|
|
43
|
+
winget install Ollama.Ollama
|
|
44
|
+
|
|
45
|
+
# macOS
|
|
46
|
+
brew install --cask ollama
|
|
47
|
+
|
|
48
|
+
# Linux
|
|
49
|
+
curl -fsSL https://ollama.com/install.sh | sh
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Проверка:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ollama --version
|
|
56
|
+
iola ai doctor
|
|
57
|
+
iola ai setup ollama
|
|
58
|
+
```
|