@iola_adm/iola-cli 0.1.24 → 0.1.26
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 +11 -0
- package/package.json +1 -1
- package/src/cli.js +883 -43
- package/wiki/Home.md +2 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +10 -0
- package/wiki//320/233/320/276/320/272/320/260/320/273/321/214/320/275/321/213/320/265-/321/204/320/260/320/271/320/273/321/213.md +46 -0
- package/wiki//320/240/320/260/320/261/320/276/321/207/320/260/321/217-/321/201/321/200/320/265/320/264/320/260-/320/260/320/263/320/265/320/275/321/202/320/260.md +67 -0
package/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { appendFile, copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, copyFile, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import readline from "node:readline/promises";
|
|
@@ -17,8 +17,10 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
17
17
|
const LAST_GOOD_CONFIG_FILE = path.join(CONFIG_DIR, "config.last-good.json");
|
|
18
18
|
const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
19
19
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
20
|
-
const DB_SCHEMA_VERSION =
|
|
20
|
+
const DB_SCHEMA_VERSION = 5;
|
|
21
21
|
const LOCAL_TOOLS = ["search_local", "get_card", "export_data", "run_report", "save_view"];
|
|
22
|
+
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
23
|
+
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
22
24
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
23
25
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
24
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -43,9 +45,17 @@ const TOOLSETS = {
|
|
|
43
45
|
description: "Внешние AI-провайдеры и Codex CLI.",
|
|
44
46
|
permissions: { externalAi: true, codex: true },
|
|
45
47
|
},
|
|
48
|
+
"local-files-read": {
|
|
49
|
+
description: "Чтение файлов, дерево папок и поиск внутри workspace.",
|
|
50
|
+
permissions: { readFiles: true, localTools: { files_tree: true, files_read: true, files_search: true } },
|
|
51
|
+
},
|
|
52
|
+
"local-files-write": {
|
|
53
|
+
description: "Запись и patch файлов внутри workspace с учетом approvals.",
|
|
54
|
+
permissions: { readFiles: true, writeFiles: true, editFiles: true, localTools: { files_write: true, files_patch: true } },
|
|
55
|
+
},
|
|
46
56
|
safe: {
|
|
47
57
|
description: "Безопасный режим: чтение данных без записи файлов и без sync.",
|
|
48
|
-
permissions: { writeFiles: false, sync: false, externalApi: true, externalAi: true, codex: false },
|
|
58
|
+
permissions: { readFiles: true, writeFiles: false, editFiles: false, deleteFiles: false, sync: false, externalApi: true, externalAi: true, codex: false },
|
|
49
59
|
},
|
|
50
60
|
full: {
|
|
51
61
|
description: "Полный локальный режим для доверенного пользователя.",
|
|
@@ -55,7 +65,10 @@ const TOOLSETS = {
|
|
|
55
65
|
externalApi: true,
|
|
56
66
|
externalAi: true,
|
|
57
67
|
codex: true,
|
|
58
|
-
|
|
68
|
+
readFiles: true,
|
|
69
|
+
editFiles: true,
|
|
70
|
+
deleteFiles: false,
|
|
71
|
+
localTools: Object.fromEntries(ALL_LOCAL_TOOLS.map((tool) => [tool, true])),
|
|
59
72
|
},
|
|
60
73
|
},
|
|
61
74
|
};
|
|
@@ -109,8 +122,16 @@ const DEFAULT_AI_CONFIG = {
|
|
|
109
122
|
export_data: true,
|
|
110
123
|
run_report: true,
|
|
111
124
|
save_view: true,
|
|
125
|
+
files_tree: false,
|
|
126
|
+
files_read: false,
|
|
127
|
+
files_search: false,
|
|
128
|
+
files_write: false,
|
|
129
|
+
files_patch: false,
|
|
112
130
|
},
|
|
131
|
+
readFiles: false,
|
|
113
132
|
writeFiles: true,
|
|
133
|
+
editFiles: false,
|
|
134
|
+
deleteFiles: false,
|
|
114
135
|
sync: true,
|
|
115
136
|
externalApi: true,
|
|
116
137
|
externalAi: true,
|
|
@@ -119,6 +140,13 @@ const DEFAULT_AI_CONFIG = {
|
|
|
119
140
|
toolsets: {
|
|
120
141
|
enabled: ["data-read", "reports", "sync", "ai"],
|
|
121
142
|
},
|
|
143
|
+
files: {
|
|
144
|
+
mode: "locked",
|
|
145
|
+
approvals: "on-write",
|
|
146
|
+
workspaceRoot: ".",
|
|
147
|
+
maxReadBytes: 200000,
|
|
148
|
+
blockedGlobs: [".env", "*.pem", "*.key", "secrets", ".git", ".ssh", "AppData", "node_modules"],
|
|
149
|
+
},
|
|
122
150
|
memory: {
|
|
123
151
|
enabled: true,
|
|
124
152
|
suggestions: true,
|
|
@@ -213,6 +241,14 @@ const COMMANDS = new Map([
|
|
|
213
241
|
["context", handleContext],
|
|
214
242
|
["skills", handleSkills],
|
|
215
243
|
["tools", handleTools],
|
|
244
|
+
["files", handleFiles],
|
|
245
|
+
["workspace", handleWorkspace],
|
|
246
|
+
["tasks", handleTasks],
|
|
247
|
+
["artifacts", handleArtifacts],
|
|
248
|
+
["snapshot", handleSnapshot],
|
|
249
|
+
["trace", handleTrace],
|
|
250
|
+
["policy", handlePolicy],
|
|
251
|
+
["export", handleExport],
|
|
216
252
|
["cron", handleCron],
|
|
217
253
|
["daemon", handleDaemon],
|
|
218
254
|
["rpc", handleRpc],
|
|
@@ -315,6 +351,14 @@ Usage:
|
|
|
315
351
|
iola context list|show|init
|
|
316
352
|
iola skills list|show|paths|enable|disable
|
|
317
353
|
iola tools list|toolsets|enable|disable|profile
|
|
354
|
+
iola files status|mode|approvals|tree|read|search|write|patch
|
|
355
|
+
iola workspace init|status|use|list
|
|
356
|
+
iola tasks list|add|done|run
|
|
357
|
+
iola artifacts list|show|open
|
|
358
|
+
iola snapshot create|list|restore
|
|
359
|
+
iola trace last|show
|
|
360
|
+
iola policy use safe|analyst|developer|full
|
|
361
|
+
iola export REPORT --format docx|xlsx --output FILE
|
|
318
362
|
iola cron list|add|delete|run|tick
|
|
319
363
|
iola daemon start|status
|
|
320
364
|
iola rpc call METHOD [ARGS] [--json]
|
|
@@ -344,7 +388,7 @@ Usage:
|
|
|
344
388
|
iola config set api.mcpBaseUrl URL
|
|
345
389
|
iola config reset
|
|
346
390
|
iola update
|
|
347
|
-
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]
|
|
391
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--plan] [--trace] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
|
|
348
392
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
349
393
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
350
394
|
iola ai context TEXT [--json]
|
|
@@ -555,6 +599,41 @@ async function handleAgentLine(line, state) {
|
|
|
555
599
|
return false;
|
|
556
600
|
}
|
|
557
601
|
|
|
602
|
+
if (command === "files") {
|
|
603
|
+
await handleFiles(args);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (command === "workspace") {
|
|
608
|
+
await handleWorkspace(args);
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (command === "tasks" || command === "todos") {
|
|
613
|
+
await handleTasks(args);
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (command === "artifacts") {
|
|
618
|
+
await handleArtifacts(args);
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (command === "snapshot") {
|
|
623
|
+
await handleSnapshot(args);
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (command === "trace") {
|
|
628
|
+
await handleTrace(args);
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (command === "policy") {
|
|
633
|
+
await handlePolicy(args);
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
558
637
|
if (command === "cron") {
|
|
559
638
|
await handleCron(args);
|
|
560
639
|
return false;
|
|
@@ -677,6 +756,14 @@ async function handleAgentLine(line, state) {
|
|
|
677
756
|
wiki: ["wiki", args],
|
|
678
757
|
context: ["context", args],
|
|
679
758
|
skills: ["skills", args],
|
|
759
|
+
files: ["files", args],
|
|
760
|
+
workspace: ["workspace", args],
|
|
761
|
+
tasks: ["tasks", args],
|
|
762
|
+
todos: ["tasks", args],
|
|
763
|
+
artifacts: ["artifacts", args],
|
|
764
|
+
snapshot: ["snapshot", args],
|
|
765
|
+
trace: ["trace", args],
|
|
766
|
+
policy: ["policy", args],
|
|
680
767
|
cron: ["cron", args],
|
|
681
768
|
daemon: ["daemon", args],
|
|
682
769
|
rpc: ["rpc", args],
|
|
@@ -724,6 +811,12 @@ function printAgentHelp() {
|
|
|
724
811
|
/skills list
|
|
725
812
|
/permissions
|
|
726
813
|
/tools
|
|
814
|
+
/files status
|
|
815
|
+
/workspace status
|
|
816
|
+
/tasks list
|
|
817
|
+
/artifacts list
|
|
818
|
+
/trace last
|
|
819
|
+
/policy use safe
|
|
727
820
|
/cron list
|
|
728
821
|
/daemon status
|
|
729
822
|
/rpc call status
|
|
@@ -1453,6 +1546,8 @@ async function handleWiki(args) {
|
|
|
1453
1546
|
["AI-профили", `${base}/AI-профили`],
|
|
1454
1547
|
["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
|
|
1455
1548
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1549
|
+
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1550
|
+
["Рабочая среда агента", `${base}/Рабочая-среда-агента`],
|
|
1456
1551
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
1457
1552
|
["Контекст и память", `${base}/Контекст-и-память`],
|
|
1458
1553
|
["Команды", `${base}/Команды`],
|
|
@@ -1617,6 +1712,242 @@ async function handleTools(args) {
|
|
|
1617
1712
|
throw new Error("Команды tools: list, toolsets, enable NAME, disable NAME, profile NAME.");
|
|
1618
1713
|
}
|
|
1619
1714
|
|
|
1715
|
+
async function handleFiles(args) {
|
|
1716
|
+
const [action = "status", target, ...rest] = args;
|
|
1717
|
+
const options = parseOptions(rest);
|
|
1718
|
+
const config = await loadConfig();
|
|
1719
|
+
|
|
1720
|
+
if (action === "status") {
|
|
1721
|
+
printKeyValue({
|
|
1722
|
+
mode: config.files?.mode || "locked",
|
|
1723
|
+
approvals: config.files?.approvals || "on-write",
|
|
1724
|
+
workspaceRoot: resolveWorkspaceRoot(config),
|
|
1725
|
+
maxReadBytes: config.files?.maxReadBytes || 200000,
|
|
1726
|
+
readFiles: config.permissions?.readFiles ? "allow" : "deny",
|
|
1727
|
+
writeFiles: config.permissions?.writeFiles ? "allow" : "deny",
|
|
1728
|
+
editFiles: config.permissions?.editFiles ? "allow" : "deny",
|
|
1729
|
+
});
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (action === "mode") {
|
|
1734
|
+
if (!["locked", "read-only", "workspace-write", "full-access"].includes(target)) {
|
|
1735
|
+
throw new Error("Режимы файлов: locked, read-only, workspace-write, full-access.");
|
|
1736
|
+
}
|
|
1737
|
+
await setFilesMode(target, config);
|
|
1738
|
+
console.log(`Файловый режим: ${target}`);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (action === "approvals") {
|
|
1743
|
+
if (!["never", "on-write", "on-danger", "always"].includes(target)) {
|
|
1744
|
+
throw new Error("Политики approvals: never, on-write, on-danger, always.");
|
|
1745
|
+
}
|
|
1746
|
+
await saveConfig({ files: { ...(config.files || {}), approvals: target } });
|
|
1747
|
+
console.log(`Файловые подтверждения: ${target}`);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (action === "tree") {
|
|
1752
|
+
const rows = await filesTree(target || ".", options);
|
|
1753
|
+
if (options.json) printJson(rows);
|
|
1754
|
+
else printTable(rows, [["type", "Тип"], ["path", "Путь"], ["size", "Размер"]]);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (action === "read") {
|
|
1759
|
+
if (!target) throw new Error("Пример: iola files read README.md");
|
|
1760
|
+
console.log(await filesRead(target, options));
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (action === "search") {
|
|
1765
|
+
const query = target;
|
|
1766
|
+
if (!query) throw new Error('Пример: iola files search "Петрова" --path .');
|
|
1767
|
+
const rows = await filesSearch(query, options);
|
|
1768
|
+
if (options.json) printJson(rows);
|
|
1769
|
+
else printTable(rows, [["file", "Файл"], ["line", "Строка"], ["text", "Текст"]]);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (action === "write") {
|
|
1774
|
+
if (!target) throw new Error('Пример: iola files write report.md --text "..."');
|
|
1775
|
+
const text = options.text ?? rest.join(" ");
|
|
1776
|
+
if (!text) throw new Error('Для записи нужен --text "..." или текст после пути.');
|
|
1777
|
+
await filesWrite(target, text, { append: Boolean(options.append) });
|
|
1778
|
+
console.log(`Файл записан: ${target}`);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
if (action === "patch") {
|
|
1783
|
+
if (!target) throw new Error('Пример: iola files patch README.md --search old --replace new');
|
|
1784
|
+
if (!options.search || options.replace === undefined) throw new Error("Для patch нужны --search и --replace.");
|
|
1785
|
+
const result = await filesPatch(target, options.search, options.replace);
|
|
1786
|
+
printKeyValue(result);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
throw new Error("Команды files: status, mode MODE, approvals POLICY, tree [PATH], read FILE, search TEXT, write FILE --text TEXT, patch FILE --search OLD --replace NEW.");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
async function handleWorkspace(args) {
|
|
1794
|
+
const [action = "status", nameOrPath] = args;
|
|
1795
|
+
const config = await loadConfig();
|
|
1796
|
+
if (action === "status") {
|
|
1797
|
+
printKeyValue({ root: resolveWorkspaceRoot(config), fileMode: config.files?.mode, approvals: config.files?.approvals });
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (action === "init") {
|
|
1801
|
+
await handleContext(["init"]);
|
|
1802
|
+
await mkdir(path.join(process.cwd(), ".iola", "skills"), { recursive: true });
|
|
1803
|
+
console.log(`Workspace готов: ${process.cwd()}`);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (action === "list") {
|
|
1807
|
+
const rows = Object.entries(config.workspaces || {}).map(([name, value]) => ({ name, path: value.path }));
|
|
1808
|
+
printTable(rows, [["name", "Workspace"], ["path", "Путь"]]);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (action === "use") {
|
|
1812
|
+
if (!nameOrPath) throw new Error("Пример: iola workspace use D:\\project");
|
|
1813
|
+
const root = path.resolve(nameOrPath);
|
|
1814
|
+
const name = path.basename(root);
|
|
1815
|
+
await saveConfig({ workspaces: { ...(config.workspaces || {}), [name]: { path: root } }, files: { ...(config.files || {}), workspaceRoot: root } });
|
|
1816
|
+
console.log(`Workspace выбран: ${root}`);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
throw new Error("Команды workspace: init, status, list, use PATH.");
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
async function handleTasks(args) {
|
|
1823
|
+
const [action = "list", idOrText, ...rest] = args;
|
|
1824
|
+
if (action === "list" || action === "ls") {
|
|
1825
|
+
printTable(listTasks(), [["id", "ID"], ["status", "Статус"], ["title", "Задача"], ["command", "Команда"]]);
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
if (action === "add") {
|
|
1829
|
+
const title = [idOrText, ...rest].filter(Boolean).join(" ");
|
|
1830
|
+
if (!title) throw new Error('Пример: iola tasks add "проверить школы"');
|
|
1831
|
+
const id = addTask(title);
|
|
1832
|
+
console.log(`Задача добавлена: ${id}`);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (action === "done") {
|
|
1836
|
+
updateTaskStatus(Number(idOrText), "done");
|
|
1837
|
+
console.log(`Задача выполнена: ${idOrText}`);
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (action === "run") {
|
|
1841
|
+
const task = getTask(Number(idOrText));
|
|
1842
|
+
if (!task.command) throw new Error("У задачи нет команды. Добавьте command через SQLite пока не реализовано редактирование.");
|
|
1843
|
+
await main(splitCommandLine(task.command));
|
|
1844
|
+
updateTaskStatus(task.id, "done");
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
throw new Error("Команды tasks: list, add TEXT, done ID, run ID.");
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
async function handleArtifacts(args) {
|
|
1851
|
+
const [action = "list", id] = args;
|
|
1852
|
+
if (action === "list" || action === "ls") {
|
|
1853
|
+
printTable(listArtifacts(), [["id", "ID"], ["kind", "Тип"], ["title", "Название"], ["file", "Файл"], ["created_at", "Дата"]]);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (action === "show") {
|
|
1857
|
+
const artifact = getArtifact(Number(id));
|
|
1858
|
+
if (artifact.file && existsSync(artifact.file)) console.log(await readFile(artifact.file, "utf8"));
|
|
1859
|
+
else printJson(artifact);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (action === "open") {
|
|
1863
|
+
const artifact = getArtifact(Number(id));
|
|
1864
|
+
if (!artifact.file) throw new Error("У artifact нет файла.");
|
|
1865
|
+
await openUrl(artifact.file);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
throw new Error("Команды artifacts: list, show ID, open ID.");
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async function handleSnapshot(args) {
|
|
1872
|
+
const [action = "list", id] = args;
|
|
1873
|
+
if (action === "create") {
|
|
1874
|
+
const result = await createSnapshot();
|
|
1875
|
+
printKeyValue(result);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (action === "list" || action === "ls") {
|
|
1879
|
+
printTable(listSnapshots(), [["id", "ID"], ["workspace", "Workspace"], ["path", "Папка"], ["created_at", "Дата"]]);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (action === "restore") {
|
|
1883
|
+
await restoreSnapshot(Number(id));
|
|
1884
|
+
console.log(`Snapshot восстановлен: ${id}`);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
throw new Error("Команды snapshot: create, list, restore ID.");
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function handleTrace(args) {
|
|
1891
|
+
const [action = "last", id] = args;
|
|
1892
|
+
if (action === "last") {
|
|
1893
|
+
printTable(listTrace(Number(id || 20)), [["id", "ID"], ["run_id", "Run"], ["tool", "Tool"], ["status", "Статус"], ["summary", "Сводка"]]);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (action === "show") {
|
|
1897
|
+
printJson(getTraceRun(id));
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
throw new Error("Команды trace: last [LIMIT], show RUN_ID.");
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
async function handlePolicy(args) {
|
|
1904
|
+
const [action = "list", name] = args;
|
|
1905
|
+
const policies = {
|
|
1906
|
+
safe: { fileMode: "read-only", approvals: "always", toolProfile: "safe" },
|
|
1907
|
+
analyst: { fileMode: "read-only", approvals: "on-danger", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read"] },
|
|
1908
|
+
developer: { fileMode: "workspace-write", approvals: "on-write", toolsets: ["data-read", "reports", "sync", "ai", "local-files-read", "local-files-write"] },
|
|
1909
|
+
full: { fileMode: "full-access", approvals: "on-danger", toolProfile: "full" },
|
|
1910
|
+
};
|
|
1911
|
+
if (action === "list") {
|
|
1912
|
+
printTable(Object.entries(policies).map(([policy, value]) => ({ policy, ...value, toolsets: value.toolsets?.join(", ") || value.toolProfile })), [["policy", "Policy"], ["fileMode", "Files"], ["approvals", "Approvals"], ["toolsets", "Toolsets"]]);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
if (action === "use") {
|
|
1916
|
+
const policy = policies[name];
|
|
1917
|
+
if (!policy) throw new Error(`Policy неизвестна: ${Object.keys(policies).join(", ")}`);
|
|
1918
|
+
const config = await loadConfig();
|
|
1919
|
+
if (policy.toolProfile) {
|
|
1920
|
+
await handleTools(["profile", policy.toolProfile]);
|
|
1921
|
+
} else {
|
|
1922
|
+
await saveConfig({ toolsets: { ...(config.toolsets || {}), enabled: policy.toolsets } });
|
|
1923
|
+
}
|
|
1924
|
+
const next = await loadConfig();
|
|
1925
|
+
await saveConfig({ files: { ...(next.files || {}), mode: policy.fileMode, approvals: policy.approvals } });
|
|
1926
|
+
await setFilesMode(policy.fileMode, await loadConfig());
|
|
1927
|
+
console.log(`Policy применена: ${name}`);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
throw new Error("Команды policy: list, use NAME.");
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
async function handleExport(args) {
|
|
1934
|
+
const [name] = args;
|
|
1935
|
+
const options = parseOptions(args.slice(1));
|
|
1936
|
+
const format = options.format || "xlsx";
|
|
1937
|
+
const output = options.output || `${name || "iola-export"}.${format}`;
|
|
1938
|
+
await ensureLocalData();
|
|
1939
|
+
const rows = buildReportRows(name || "education-contacts");
|
|
1940
|
+
if (format === "xlsx") {
|
|
1941
|
+
await writeFile(output, toSpreadsheetXml(rows), "utf8");
|
|
1942
|
+
} else if (format === "docx" || format === "doc") {
|
|
1943
|
+
await writeFile(output, toWordHtml(name || "Отчет", rows), "utf8");
|
|
1944
|
+
} else {
|
|
1945
|
+
await outputData(rows, { output }, format);
|
|
1946
|
+
}
|
|
1947
|
+
saveArtifact("export", name || "export", output, { format, rows: rows.length });
|
|
1948
|
+
console.log(`Экспорт создан: ${output}`);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1620
1951
|
async function handleCron(args) {
|
|
1621
1952
|
const [action = "list", ...rest] = args;
|
|
1622
1953
|
const options = parseOptions(rest);
|
|
@@ -1722,6 +2053,14 @@ async function handlePermissions(args) {
|
|
|
1722
2053
|
value: permissions.localTools?.[tool] === false ? "deny" : "allow",
|
|
1723
2054
|
scope: "local-tool",
|
|
1724
2055
|
})),
|
|
2056
|
+
...FILE_TOOLS.map((tool) => ({
|
|
2057
|
+
permission: `localTools.${tool}`,
|
|
2058
|
+
value: permissions.localTools?.[tool] === true ? "allow" : "deny",
|
|
2059
|
+
scope: "file-tool",
|
|
2060
|
+
})),
|
|
2061
|
+
{ permission: "readFiles", value: permissions.readFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
2062
|
+
{ permission: "editFiles", value: permissions.editFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
2063
|
+
{ permission: "deleteFiles", value: permissions.deleteFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
1725
2064
|
{ permission: "writeFiles", value: permissions.writeFiles === false ? "deny" : "allow", scope: "runtime" },
|
|
1726
2065
|
{ permission: "sync", value: permissions.sync === false ? "deny" : "allow", scope: "runtime" },
|
|
1727
2066
|
{ permission: "externalApi", value: permissions.externalApi === false ? "deny" : "allow", scope: "network" },
|
|
@@ -1743,12 +2082,12 @@ async function handlePermissions(args) {
|
|
|
1743
2082
|
const allow = action === "allow";
|
|
1744
2083
|
const next = { ...(config.permissions || DEFAULT_AI_CONFIG.permissions) };
|
|
1745
2084
|
next.localTools = { ...(next.localTools || {}) };
|
|
1746
|
-
if (
|
|
2085
|
+
if (ALL_LOCAL_TOOLS.includes(name)) {
|
|
1747
2086
|
next.localTools[name] = allow;
|
|
1748
2087
|
} else if (name in DEFAULT_AI_CONFIG.permissions) {
|
|
1749
2088
|
next[name] = allow;
|
|
1750
2089
|
} else {
|
|
1751
|
-
throw new Error(`Неизвестное разрешение: ${name}. Доступно: ${[...
|
|
2090
|
+
throw new Error(`Неизвестное разрешение: ${name}. Доступно: ${[...ALL_LOCAL_TOOLS, "readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"].join(", ")}`);
|
|
1752
2091
|
}
|
|
1753
2092
|
await saveConfig({ permissions: next });
|
|
1754
2093
|
console.log(`${name}: ${allow ? "allow" : "deny"}`);
|
|
@@ -2085,6 +2424,11 @@ async function handleView(args) {
|
|
|
2085
2424
|
async function handleReport(args) {
|
|
2086
2425
|
const [name] = args;
|
|
2087
2426
|
await ensureLocalData();
|
|
2427
|
+
const options = parseOptions(args.slice(1));
|
|
2428
|
+
if (options.format === "docx" || options.format === "xlsx") {
|
|
2429
|
+
await handleExport([name || "education-contacts", "--format", options.format, "--output", options.output || `${name || "report"}.${options.format}`]);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2088
2432
|
if (name === "schools-summary") {
|
|
2089
2433
|
printTable(getLocalSummaryRows("schools"), [["metric", "Показатель"], ["value", "Значение"]]);
|
|
2090
2434
|
return;
|
|
@@ -2104,6 +2448,43 @@ async function handleReport(args) {
|
|
|
2104
2448
|
throw new Error("Отчеты: schools-summary, education-contacts, missing-phones, licenses.");
|
|
2105
2449
|
}
|
|
2106
2450
|
|
|
2451
|
+
function buildReportRows(name) {
|
|
2452
|
+
const reportName = name || "education-contacts";
|
|
2453
|
+
if (reportName === "schools-summary") return getLocalSummaryRows("schools");
|
|
2454
|
+
if (reportName === "missing-phones") return searchLocalRecords("", { dataset: "all", limit: 500 }).filter((item) => !item.phone || item.phone === "-");
|
|
2455
|
+
if (reportName === "licenses") return searchLocalRecords("", { dataset: "all", limit: 500 }).map((item) => ({ name: item.name, license_number: item.license_number, license_status: item.license_status }));
|
|
2456
|
+
return searchLocalRecords("", { dataset: "all", limit: 500 });
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function toSpreadsheetXml(rows) {
|
|
2460
|
+
const columns = Object.keys(rows[0] || { empty: "" });
|
|
2461
|
+
const cell = (value) => `<Cell><Data ss:Type="String">${escapeXml(value ?? "")}</Data></Cell>`;
|
|
2462
|
+
return `<?xml version="1.0"?>
|
|
2463
|
+
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
|
2464
|
+
<Worksheet ss:Name="IOLA"><Table>
|
|
2465
|
+
<Row>${columns.map(cell).join("")}</Row>
|
|
2466
|
+
${rows.map((row) => `<Row>${columns.map((column) => cell(row[column])).join("")}</Row>`).join("\n")}
|
|
2467
|
+
</Table></Worksheet></Workbook>`;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function toWordHtml(title, rows) {
|
|
2471
|
+
const columns = Object.keys(rows[0] || { empty: "" });
|
|
2472
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>
|
|
2473
|
+
<h1>${escapeHtml(title)}</h1>
|
|
2474
|
+
<table border="1" cellspacing="0" cellpadding="4">
|
|
2475
|
+
<thead><tr>${columns.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead>
|
|
2476
|
+
<tbody>${rows.map((row) => `<tr>${columns.map((column) => `<td>${escapeHtml(row[column] ?? "")}</td>`).join("")}</tr>`).join("\n")}</tbody>
|
|
2477
|
+
</table></body></html>`;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function escapeXml(value) {
|
|
2481
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function escapeHtml(value) {
|
|
2485
|
+
return escapeXml(value);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2107
2488
|
async function handlePrivacy() {
|
|
2108
2489
|
printKeyValue({
|
|
2109
2490
|
config: CONFIG_FILE,
|
|
@@ -2782,6 +3163,38 @@ function initDatabase() {
|
|
|
2782
3163
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2783
3164
|
);
|
|
2784
3165
|
CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled, last_run_at);
|
|
3166
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
3167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3168
|
+
title TEXT NOT NULL,
|
|
3169
|
+
command TEXT,
|
|
3170
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
3171
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3172
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3173
|
+
);
|
|
3174
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
3175
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3176
|
+
kind TEXT NOT NULL,
|
|
3177
|
+
title TEXT NOT NULL,
|
|
3178
|
+
file TEXT,
|
|
3179
|
+
meta_json TEXT NOT NULL DEFAULT '{}',
|
|
3180
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3181
|
+
);
|
|
3182
|
+
CREATE TABLE IF NOT EXISTS tool_traces (
|
|
3183
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3184
|
+
run_id TEXT NOT NULL,
|
|
3185
|
+
tool TEXT NOT NULL,
|
|
3186
|
+
args_json TEXT NOT NULL,
|
|
3187
|
+
status TEXT NOT NULL,
|
|
3188
|
+
summary TEXT,
|
|
3189
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3190
|
+
);
|
|
3191
|
+
CREATE INDEX IF NOT EXISTS idx_tool_traces_run_id ON tool_traces(run_id, id);
|
|
3192
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
3193
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3194
|
+
workspace TEXT NOT NULL,
|
|
3195
|
+
path TEXT NOT NULL,
|
|
3196
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
3197
|
+
);
|
|
2785
3198
|
`);
|
|
2786
3199
|
rebuildFtsIfEmpty(db);
|
|
2787
3200
|
db.prepare(`
|
|
@@ -2834,6 +3247,8 @@ function getDbStatus() {
|
|
|
2834
3247
|
const memory = db.prepare("SELECT COUNT(*) AS count FROM memory").get();
|
|
2835
3248
|
const memorySuggestions = db.prepare("SELECT COUNT(*) AS count FROM memory_suggestions WHERE status = 'pending'").get();
|
|
2836
3249
|
const cron = db.prepare("SELECT COUNT(*) AS count FROM cron_jobs").get();
|
|
3250
|
+
const tasks = db.prepare("SELECT COUNT(*) AS count FROM tasks WHERE status != 'done'").get();
|
|
3251
|
+
const artifacts = db.prepare("SELECT COUNT(*) AS count FROM artifacts").get();
|
|
2837
3252
|
return {
|
|
2838
3253
|
status: "ok",
|
|
2839
3254
|
file: DB_FILE,
|
|
@@ -2845,6 +3260,8 @@ function getDbStatus() {
|
|
|
2845
3260
|
memory: memory?.count ?? 0,
|
|
2846
3261
|
memory_suggestions: memorySuggestions?.count ?? 0,
|
|
2847
3262
|
cron_jobs: cron?.count ?? 0,
|
|
3263
|
+
open_tasks: tasks?.count ?? 0,
|
|
3264
|
+
artifacts: artifacts?.count ?? 0,
|
|
2848
3265
|
};
|
|
2849
3266
|
} finally {
|
|
2850
3267
|
db.close();
|
|
@@ -3910,8 +4327,17 @@ function resolveAiProfile(config, options = {}) {
|
|
|
3910
4327
|
async function localToolAsk(question, providerConfig, options) {
|
|
3911
4328
|
await ensureLocalData();
|
|
3912
4329
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
3913
|
-
const validated = validateToolPlan(plan);
|
|
3914
|
-
|
|
4330
|
+
const validated = validateToolPlan(plan, options);
|
|
4331
|
+
if (options.plan) {
|
|
4332
|
+
printToolPlan(validated);
|
|
4333
|
+
const shouldRun = await confirm("Выполнить план? [y/N] ");
|
|
4334
|
+
if (!shouldRun) {
|
|
4335
|
+
saveArtifact("plan", question.slice(0, 80), "", { plan: validated });
|
|
4336
|
+
return "План построен, выполнение отменено.";
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
const runId = `run-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
4340
|
+
const result = await executeToolPlan(validated, { ...options, runId });
|
|
3915
4341
|
const answer = formatToolResult(result, options);
|
|
3916
4342
|
|
|
3917
4343
|
if (!options["no-history"] && isFeatureEnabled("sqlite-history")) {
|
|
@@ -3925,7 +4351,8 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
3925
4351
|
});
|
|
3926
4352
|
}
|
|
3927
4353
|
|
|
3928
|
-
emitEvent(options, "tool_plan", { plan: validated });
|
|
4354
|
+
emitEvent(options, "tool_plan", { plan: validated, runId });
|
|
4355
|
+
saveArtifact("tool-result", question.slice(0, 80), "", { runId, plan: validated, outputs: result.outputs });
|
|
3929
4356
|
if (options.output) {
|
|
3930
4357
|
await assertPermission("writeFiles");
|
|
3931
4358
|
await writeFile(options.output, answer, "utf8");
|
|
@@ -3938,12 +4365,20 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
3938
4365
|
return answer;
|
|
3939
4366
|
}
|
|
3940
4367
|
|
|
4368
|
+
function printToolPlan(plan) {
|
|
4369
|
+
console.log("План выполнения:");
|
|
4370
|
+
plan.steps.forEach((step, index) => {
|
|
4371
|
+
console.log(`${index + 1}. ${step.tool} ${JSON.stringify(step.args || {})}`);
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
|
|
3941
4375
|
async function buildLocalToolPlan(question, providerConfig, options) {
|
|
3942
4376
|
const mode = options.reasoning || "verify";
|
|
3943
4377
|
const prompt = [
|
|
3944
4378
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
3945
|
-
|
|
4379
|
+
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
3946
4380
|
"Схема: {\"steps\":[{\"tool\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
4381
|
+
options.files ? "Файловые tools: files_tree {path,depth,limit}, files_read {path}, files_search {query,path}, files_write {path,text}, files_patch {path,search,replace}." : "",
|
|
3947
4382
|
"Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
|
|
3948
4383
|
`Вопрос: ${question}`,
|
|
3949
4384
|
].join("\n");
|
|
@@ -3952,11 +4387,11 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
3952
4387
|
const raw = await callOllama(providerConfig, [{ role: "user", content: prompt }]);
|
|
3953
4388
|
const parsed = parseJsonObject(raw);
|
|
3954
4389
|
if (mode === "vote") {
|
|
3955
|
-
return chooseBestPlan([parsed, inferToolPlan(question)]);
|
|
4390
|
+
return chooseBestPlan([parsed, inferToolPlan(question, options)], options);
|
|
3956
4391
|
}
|
|
3957
4392
|
return parsed;
|
|
3958
4393
|
} catch {
|
|
3959
|
-
return inferToolPlan(question);
|
|
4394
|
+
return inferToolPlan(question, options);
|
|
3960
4395
|
}
|
|
3961
4396
|
}
|
|
3962
4397
|
|
|
@@ -3966,7 +4401,7 @@ function parseJsonObject(text) {
|
|
|
3966
4401
|
return JSON.parse(match[0]);
|
|
3967
4402
|
}
|
|
3968
4403
|
|
|
3969
|
-
function inferToolPlan(question) {
|
|
4404
|
+
function inferToolPlan(question, options = {}) {
|
|
3970
4405
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
3971
4406
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
3972
4407
|
const steps = [];
|
|
@@ -3979,13 +4414,20 @@ function inferToolPlan(question) {
|
|
|
3979
4414
|
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
3980
4415
|
steps.push({ tool: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
3981
4416
|
}
|
|
4417
|
+
if (options.files || normalized.includes("файл") || normalized.includes("папк") || normalized.includes("readme")) {
|
|
4418
|
+
if (normalized.includes("найди") || normalized.includes("поиск")) {
|
|
4419
|
+
steps.unshift({ tool: "files_search", args: { query: question, path: "." } });
|
|
4420
|
+
} else {
|
|
4421
|
+
steps.unshift({ tool: "files_tree", args: { path: ".", depth: 2, limit: 80 } });
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
3982
4424
|
return { steps };
|
|
3983
4425
|
}
|
|
3984
4426
|
|
|
3985
|
-
function chooseBestPlan(plans) {
|
|
4427
|
+
function chooseBestPlan(plans, options = {}) {
|
|
3986
4428
|
return plans.find((plan) => {
|
|
3987
4429
|
try {
|
|
3988
|
-
validateToolPlan(plan);
|
|
4430
|
+
validateToolPlan(plan, options);
|
|
3989
4431
|
return true;
|
|
3990
4432
|
} catch {
|
|
3991
4433
|
return false;
|
|
@@ -3993,8 +4435,8 @@ function chooseBestPlan(plans) {
|
|
|
3993
4435
|
}) || plans.at(-1);
|
|
3994
4436
|
}
|
|
3995
4437
|
|
|
3996
|
-
function validateToolPlan(plan) {
|
|
3997
|
-
const allowed = new Set(
|
|
4438
|
+
function validateToolPlan(plan, options = {}) {
|
|
4439
|
+
const allowed = new Set(availableToolNames(options));
|
|
3998
4440
|
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
3999
4441
|
for (const step of plan.steps) {
|
|
4000
4442
|
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
@@ -4002,32 +4444,66 @@ function validateToolPlan(plan) {
|
|
|
4002
4444
|
return plan;
|
|
4003
4445
|
}
|
|
4004
4446
|
|
|
4005
|
-
|
|
4447
|
+
function availableToolNames(options = {}) {
|
|
4448
|
+
return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
async function executeToolPlan(plan, options = {}) {
|
|
4006
4452
|
let current = [];
|
|
4007
4453
|
const outputs = [];
|
|
4008
4454
|
for (const step of plan.steps) {
|
|
4455
|
+
let status = "ok";
|
|
4456
|
+
let summary = "";
|
|
4009
4457
|
await assertPermission(step.tool);
|
|
4010
4458
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4459
|
+
try {
|
|
4460
|
+
if (step.tool === "search_local") {
|
|
4461
|
+
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
4462
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4463
|
+
} else if (step.tool === "get_card") {
|
|
4464
|
+
const card = findCard(step.args?.query || "");
|
|
4465
|
+
current = card ? [card] : [];
|
|
4466
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4467
|
+
} else if (step.tool === "run_report") {
|
|
4468
|
+
current = runQuality(step.args?.name || "all");
|
|
4469
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4470
|
+
} else if (step.tool === "save_view") {
|
|
4471
|
+
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
4472
|
+
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
4473
|
+
} else if (step.tool === "export_data") {
|
|
4474
|
+
await assertPermission("writeFiles");
|
|
4475
|
+
await runHooks("BeforeExport", { output: step.args?.output || "iola-export.csv", format: step.args?.format || "csv", rows: current.length });
|
|
4476
|
+
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
4477
|
+
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
4478
|
+
saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
|
|
4479
|
+
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
4480
|
+
} else if (step.tool === "files_tree") {
|
|
4481
|
+
current = await filesTree(step.args?.path || ".", step.args || {});
|
|
4482
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4483
|
+
} else if (step.tool === "files_read") {
|
|
4484
|
+
const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
|
|
4485
|
+
current = [{ path: step.args?.path || step.args?.file || ".", text }];
|
|
4486
|
+
outputs.push({ tool: step.tool, bytes: text.length });
|
|
4487
|
+
} else if (step.tool === "files_search") {
|
|
4488
|
+
current = await filesSearch(step.args?.query || "", { path: step.args?.path || ".", limit: step.args?.limit || 50 });
|
|
4489
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4490
|
+
} else if (step.tool === "files_write") {
|
|
4491
|
+
await filesWrite(step.args?.path || step.args?.file, step.args?.text || "", { append: Boolean(step.args?.append) });
|
|
4492
|
+
current = [{ path: step.args?.path || step.args?.file, status: "written" }];
|
|
4493
|
+
outputs.push({ tool: step.tool, output: step.args?.path || step.args?.file, rows: 1 });
|
|
4494
|
+
} else if (step.tool === "files_patch") {
|
|
4495
|
+
const result = await filesPatch(step.args?.path || step.args?.file, step.args?.search || "", step.args?.replace || "");
|
|
4496
|
+
current = [result];
|
|
4497
|
+
outputs.push({ tool: step.tool, output: result.path, replacements: result.replacements });
|
|
4498
|
+
}
|
|
4499
|
+
summary = `rows=${current.length}`;
|
|
4500
|
+
} catch (error) {
|
|
4501
|
+
status = "error";
|
|
4502
|
+
summary = error instanceof Error ? error.message : String(error);
|
|
4503
|
+
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
4504
|
+
throw error;
|
|
4030
4505
|
}
|
|
4506
|
+
recordToolTrace(options.runId || "manual", step.tool, step.args || {}, status, summary);
|
|
4031
4507
|
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
4032
4508
|
}
|
|
4033
4509
|
return { rows: current, outputs };
|
|
@@ -4071,7 +4547,7 @@ async function runHooks(event, payload = {}) {
|
|
|
4071
4547
|
async function assertPermission(name) {
|
|
4072
4548
|
const config = await loadConfig();
|
|
4073
4549
|
const permissions = applyToolsetPermissions(config.permissions || DEFAULT_AI_CONFIG.permissions, config.toolsets?.enabled || []);
|
|
4074
|
-
if (
|
|
4550
|
+
if (ALL_LOCAL_TOOLS.includes(name)) {
|
|
4075
4551
|
if (permissions.localTools?.[name] === false) {
|
|
4076
4552
|
throw new Error(`Tool запрещен политикой permissions: ${name}`);
|
|
4077
4553
|
}
|
|
@@ -4584,12 +5060,12 @@ function parseOptions(args) {
|
|
|
4584
5060
|
|
|
4585
5061
|
for (let index = 0; index < args.length; index += 1) {
|
|
4586
5062
|
const arg = args[index];
|
|
4587
|
-
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" || arg === "--fix") {
|
|
5063
|
+
if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
4588
5064
|
result[arg.slice(2)] = true;
|
|
4589
5065
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
4590
5066
|
result.check = true;
|
|
4591
5067
|
result[arg.slice(2)] = true;
|
|
4592
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--query" || 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") {
|
|
5068
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || 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") {
|
|
4593
5069
|
result[arg.slice(2)] = args[index + 1];
|
|
4594
5070
|
index += 1;
|
|
4595
5071
|
} else {
|
|
@@ -4972,6 +5448,356 @@ function readRequestBody(req) {
|
|
|
4972
5448
|
});
|
|
4973
5449
|
}
|
|
4974
5450
|
|
|
5451
|
+
async function setFilesMode(mode, config = null) {
|
|
5452
|
+
const current = config || await loadConfig();
|
|
5453
|
+
const localTools = { ...(current.permissions?.localTools || {}) };
|
|
5454
|
+
for (const tool of FILE_TOOLS) localTools[tool] = false;
|
|
5455
|
+
const permissions = {
|
|
5456
|
+
...(current.permissions || DEFAULT_AI_CONFIG.permissions),
|
|
5457
|
+
localTools,
|
|
5458
|
+
readFiles: false,
|
|
5459
|
+
editFiles: false,
|
|
5460
|
+
deleteFiles: false,
|
|
5461
|
+
};
|
|
5462
|
+
const enabled = new Set(current.toolsets?.enabled || []);
|
|
5463
|
+
enabled.delete("local-files-read");
|
|
5464
|
+
enabled.delete("local-files-write");
|
|
5465
|
+
|
|
5466
|
+
if (mode === "read-only") {
|
|
5467
|
+
permissions.readFiles = true;
|
|
5468
|
+
for (const tool of ["files_tree", "files_read", "files_search"]) permissions.localTools[tool] = true;
|
|
5469
|
+
enabled.add("local-files-read");
|
|
5470
|
+
} else if (mode === "workspace-write") {
|
|
5471
|
+
permissions.readFiles = true;
|
|
5472
|
+
permissions.writeFiles = true;
|
|
5473
|
+
permissions.editFiles = true;
|
|
5474
|
+
for (const tool of FILE_TOOLS) permissions.localTools[tool] = true;
|
|
5475
|
+
enabled.add("local-files-read");
|
|
5476
|
+
enabled.add("local-files-write");
|
|
5477
|
+
} else if (mode === "full-access") {
|
|
5478
|
+
permissions.readFiles = true;
|
|
5479
|
+
permissions.writeFiles = true;
|
|
5480
|
+
permissions.editFiles = true;
|
|
5481
|
+
permissions.deleteFiles = false;
|
|
5482
|
+
for (const tool of FILE_TOOLS) permissions.localTools[tool] = true;
|
|
5483
|
+
enabled.add("local-files-read");
|
|
5484
|
+
enabled.add("local-files-write");
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
await saveConfig({
|
|
5488
|
+
permissions,
|
|
5489
|
+
toolsets: { ...(current.toolsets || {}), enabled: [...enabled] },
|
|
5490
|
+
files: { ...(current.files || {}), mode },
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5494
|
+
function resolveWorkspaceRoot(config) {
|
|
5495
|
+
return path.resolve(process.cwd(), config.files?.workspaceRoot || ".");
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
async function resolveFileTarget(target, operation) {
|
|
5499
|
+
if (!target) throw new Error("Путь к файлу обязателен.");
|
|
5500
|
+
const config = await loadConfig();
|
|
5501
|
+
const mode = config.files?.mode || "locked";
|
|
5502
|
+
if (mode === "locked") throw new Error("Файловые операции заблокированы. Включите: iola files mode read-only");
|
|
5503
|
+
const workspaceRoot = resolveWorkspaceRoot(config);
|
|
5504
|
+
const resolved = path.resolve(workspaceRoot, target);
|
|
5505
|
+
const relative = path.relative(workspaceRoot, resolved);
|
|
5506
|
+
const insideWorkspace = relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
5507
|
+
|
|
5508
|
+
if ((mode === "read-only" || mode === "workspace-write") && !insideWorkspace && resolved !== workspaceRoot) {
|
|
5509
|
+
throw new Error(`Путь вне workspace запрещен режимом ${mode}: ${resolved}`);
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
const blocked = config.files?.blockedGlobs || [];
|
|
5513
|
+
const normalized = resolved.toLocaleLowerCase("ru-RU");
|
|
5514
|
+
if (blocked.some((pattern) => filePatternMatches(normalized, pattern))) {
|
|
5515
|
+
throw new Error(`Путь заблокирован политикой безопасности: ${target}`);
|
|
5516
|
+
}
|
|
5517
|
+
|
|
5518
|
+
if (operation === "read") await assertPermission("readFiles");
|
|
5519
|
+
if (operation === "write") await assertPermission("writeFiles");
|
|
5520
|
+
if (operation === "edit") await assertPermission("editFiles");
|
|
5521
|
+
if (operation === "delete") await assertPermission("deleteFiles");
|
|
5522
|
+
|
|
5523
|
+
return { config, resolved, workspaceRoot, relative: resolved === workspaceRoot ? "." : relative, insideWorkspace };
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
function filePatternMatches(normalizedPath, pattern) {
|
|
5527
|
+
const normalizedPattern = String(pattern).toLocaleLowerCase("ru-RU").replace(/\*/g, "");
|
|
5528
|
+
if (!normalizedPattern) return false;
|
|
5529
|
+
return normalizedPath.split(/[\\/]/).includes(normalizedPattern) || normalizedPath.includes(normalizedPattern);
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
function isBlockedPathForConfig(fullPath, config) {
|
|
5533
|
+
const normalized = fullPath.toLocaleLowerCase("ru-RU");
|
|
5534
|
+
return (config.files?.blockedGlobs || []).some((pattern) => filePatternMatches(normalized, pattern));
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
async function filesTree(target = ".", options = {}) {
|
|
5538
|
+
await assertPermission("files_tree");
|
|
5539
|
+
const { resolved, workspaceRoot } = await resolveFileTarget(target, "read");
|
|
5540
|
+
const depth = Number(options.depth || 2);
|
|
5541
|
+
const limit = Number(options.limit || 100);
|
|
5542
|
+
const rows = [];
|
|
5543
|
+
await walkFiles(resolved, workspaceRoot, rows, depth, limit, (await loadConfig()));
|
|
5544
|
+
return rows;
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
async function walkFiles(directory, workspaceRoot, rows, depth, limit, config) {
|
|
5548
|
+
if (rows.length >= limit || depth < 0) return;
|
|
5549
|
+
let entries = [];
|
|
5550
|
+
try {
|
|
5551
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
5552
|
+
} catch {
|
|
5553
|
+
return;
|
|
5554
|
+
}
|
|
5555
|
+
for (const entry of entries) {
|
|
5556
|
+
if (rows.length >= limit) break;
|
|
5557
|
+
const full = path.join(directory, entry.name);
|
|
5558
|
+
if (isBlockedPathForConfig(full, config)) continue;
|
|
5559
|
+
const relative = path.relative(workspaceRoot, full) || ".";
|
|
5560
|
+
let size = "-";
|
|
5561
|
+
try {
|
|
5562
|
+
size = entry.isFile() ? (await stat(full)).size : "-";
|
|
5563
|
+
} catch {
|
|
5564
|
+
size = "-";
|
|
5565
|
+
}
|
|
5566
|
+
rows.push({ type: entry.isDirectory() ? "dir" : "file", path: relative, size });
|
|
5567
|
+
if (entry.isDirectory()) await walkFiles(full, workspaceRoot, rows, depth - 1, limit, config);
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5571
|
+
async function filesRead(target, options = {}) {
|
|
5572
|
+
await assertPermission("files_read");
|
|
5573
|
+
const { config, resolved } = await resolveFileTarget(target, "read");
|
|
5574
|
+
const info = await stat(resolved);
|
|
5575
|
+
if (!info.isFile()) throw new Error(`Это не файл: ${target}`);
|
|
5576
|
+
const maxBytes = Number(options.maxBytes || config.files?.maxReadBytes || 200000);
|
|
5577
|
+
if (info.size > maxBytes) throw new Error(`Файл слишком большой: ${info.size} байт. Лимит: ${maxBytes}`);
|
|
5578
|
+
return readFile(resolved, "utf8");
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
async function filesSearch(query, options = {}) {
|
|
5582
|
+
await assertPermission("files_search");
|
|
5583
|
+
if (!query) throw new Error("Строка поиска обязательна.");
|
|
5584
|
+
const rows = await filesTree(options.path || ".", { depth: Number(options.depth || 4), limit: Number(options.limit || 200) });
|
|
5585
|
+
const results = [];
|
|
5586
|
+
for (const row of rows.filter((item) => item.type === "file")) {
|
|
5587
|
+
if (results.length >= Number(options.limit || 50)) break;
|
|
5588
|
+
try {
|
|
5589
|
+
const text = await filesRead(row.path, { maxBytes: 500000 });
|
|
5590
|
+
const lines = text.split(/\r?\n/);
|
|
5591
|
+
lines.forEach((line, index) => {
|
|
5592
|
+
if (results.length < Number(options.limit || 50) && line.toLocaleLowerCase("ru-RU").includes(String(query).toLocaleLowerCase("ru-RU"))) {
|
|
5593
|
+
results.push({ file: row.path, line: index + 1, text: line.trim().slice(0, 240) });
|
|
5594
|
+
}
|
|
5595
|
+
});
|
|
5596
|
+
} catch {
|
|
5597
|
+
// Binary, blocked or oversized files are skipped.
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
return results;
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
async function filesWrite(target, text, options = {}) {
|
|
5604
|
+
await assertPermission("files_write");
|
|
5605
|
+
const { resolved, relative } = await resolveFileTarget(target, "write");
|
|
5606
|
+
await maybeConfirmFileOperation("write", relative, text);
|
|
5607
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
5608
|
+
if (options.append) {
|
|
5609
|
+
await appendFile(resolved, text, "utf8");
|
|
5610
|
+
} else {
|
|
5611
|
+
await writeFile(resolved, text, "utf8");
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
async function filesPatch(target, search, replace) {
|
|
5616
|
+
await assertPermission("files_patch");
|
|
5617
|
+
const { resolved, relative } = await resolveFileTarget(target, "edit");
|
|
5618
|
+
const current = await readFile(resolved, "utf8");
|
|
5619
|
+
if (!current.includes(search)) throw new Error("Искомый фрагмент не найден.");
|
|
5620
|
+
const next = current.split(search).join(replace);
|
|
5621
|
+
const replacements = current.split(search).length - 1;
|
|
5622
|
+
await maybeConfirmFileOperation("patch", relative, unifiedPreview(current, next));
|
|
5623
|
+
await writeFile(resolved, next, "utf8");
|
|
5624
|
+
return { path: relative, replacements };
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
async function maybeConfirmFileOperation(operation, target, preview) {
|
|
5628
|
+
const config = await loadConfig();
|
|
5629
|
+
const approvals = config.files?.approvals || "on-write";
|
|
5630
|
+
const needsApproval = approvals === "always" || approvals === "on-write" || (approvals === "on-danger" && operation !== "write");
|
|
5631
|
+
if (!needsApproval) return;
|
|
5632
|
+
console.log(`Файловая операция: ${operation} ${target}`);
|
|
5633
|
+
if (preview) console.log(String(preview).slice(0, 2000));
|
|
5634
|
+
const ok = await confirm("Продолжить? [y/N] ");
|
|
5635
|
+
if (!ok) throw new Error("Файловая операция отменена.");
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5638
|
+
function listTasks() {
|
|
5639
|
+
initDatabase();
|
|
5640
|
+
const db = openDatabase();
|
|
5641
|
+
try {
|
|
5642
|
+
return db.prepare("SELECT id, title, COALESCE(command, '-') AS command, status FROM tasks ORDER BY status, id DESC LIMIT 100").all();
|
|
5643
|
+
} finally {
|
|
5644
|
+
db.close();
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
function addTask(title, command = "") {
|
|
5649
|
+
initDatabase();
|
|
5650
|
+
const db = openDatabase();
|
|
5651
|
+
try {
|
|
5652
|
+
const result = db.prepare("INSERT INTO tasks(title, command) VALUES (?, ?)").run(title, command);
|
|
5653
|
+
return Number(result.lastInsertRowid);
|
|
5654
|
+
} finally {
|
|
5655
|
+
db.close();
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
function getTask(id) {
|
|
5660
|
+
initDatabase();
|
|
5661
|
+
const db = openDatabase();
|
|
5662
|
+
try {
|
|
5663
|
+
const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
5664
|
+
if (!row) throw new Error(`Задача не найдена: ${id}`);
|
|
5665
|
+
return row;
|
|
5666
|
+
} finally {
|
|
5667
|
+
db.close();
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5671
|
+
function updateTaskStatus(id, status) {
|
|
5672
|
+
initDatabase();
|
|
5673
|
+
const db = openDatabase();
|
|
5674
|
+
try {
|
|
5675
|
+
db.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run(status, id);
|
|
5676
|
+
} finally {
|
|
5677
|
+
db.close();
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
|
|
5681
|
+
function saveArtifact(kind, title, file = "", meta = {}) {
|
|
5682
|
+
initDatabase();
|
|
5683
|
+
const db = openDatabase();
|
|
5684
|
+
try {
|
|
5685
|
+
const result = db.prepare("INSERT INTO artifacts(kind, title, file, meta_json) VALUES (?, ?, ?, ?)").run(kind, title || kind, file || "", JSON.stringify(meta));
|
|
5686
|
+
return Number(result.lastInsertRowid);
|
|
5687
|
+
} finally {
|
|
5688
|
+
db.close();
|
|
5689
|
+
}
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5692
|
+
function listArtifacts() {
|
|
5693
|
+
initDatabase();
|
|
5694
|
+
const db = openDatabase();
|
|
5695
|
+
try {
|
|
5696
|
+
return db.prepare("SELECT id, kind, title, file, created_at FROM artifacts ORDER BY id DESC LIMIT 100").all();
|
|
5697
|
+
} finally {
|
|
5698
|
+
db.close();
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
function getArtifact(id) {
|
|
5703
|
+
initDatabase();
|
|
5704
|
+
const db = openDatabase();
|
|
5705
|
+
try {
|
|
5706
|
+
const row = db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id);
|
|
5707
|
+
if (!row) throw new Error(`Artifact не найден: ${id}`);
|
|
5708
|
+
return row;
|
|
5709
|
+
} finally {
|
|
5710
|
+
db.close();
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
function recordToolTrace(runId, tool, args, status, summary) {
|
|
5715
|
+
initDatabase();
|
|
5716
|
+
const db = openDatabase();
|
|
5717
|
+
try {
|
|
5718
|
+
db.prepare("INSERT INTO tool_traces(run_id, tool, args_json, status, summary) VALUES (?, ?, ?, ?, ?)").run(runId, tool, JSON.stringify(args), status, summary || "");
|
|
5719
|
+
} finally {
|
|
5720
|
+
db.close();
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
function listTrace(limit = 20) {
|
|
5725
|
+
initDatabase();
|
|
5726
|
+
const db = openDatabase();
|
|
5727
|
+
try {
|
|
5728
|
+
return db.prepare("SELECT id, run_id, tool, status, summary, created_at FROM tool_traces ORDER BY id DESC LIMIT ?").all(limit);
|
|
5729
|
+
} finally {
|
|
5730
|
+
db.close();
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
function getTraceRun(runId) {
|
|
5735
|
+
initDatabase();
|
|
5736
|
+
const db = openDatabase();
|
|
5737
|
+
try {
|
|
5738
|
+
return db.prepare("SELECT * FROM tool_traces WHERE run_id = ? ORDER BY id ASC").all(runId);
|
|
5739
|
+
} finally {
|
|
5740
|
+
db.close();
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
async function createSnapshot() {
|
|
5745
|
+
const config = await loadConfig();
|
|
5746
|
+
const workspace = resolveWorkspaceRoot(config);
|
|
5747
|
+
const snapshotsDir = path.join(CONFIG_DIR, "snapshots");
|
|
5748
|
+
await mkdir(snapshotsDir, { recursive: true });
|
|
5749
|
+
const target = path.join(snapshotsDir, `snapshot-${Date.now()}`);
|
|
5750
|
+
await cp(workspace, target, {
|
|
5751
|
+
recursive: true,
|
|
5752
|
+
filter: (source) => !isBlockedPathForConfig(source, config) && !source.includes(`${path.sep}node_modules${path.sep}`),
|
|
5753
|
+
});
|
|
5754
|
+
initDatabase();
|
|
5755
|
+
const db = openDatabase();
|
|
5756
|
+
try {
|
|
5757
|
+
const result = db.prepare("INSERT INTO snapshots(workspace, path) VALUES (?, ?)").run(workspace, target);
|
|
5758
|
+
return { id: Number(result.lastInsertRowid), workspace, path: target };
|
|
5759
|
+
} finally {
|
|
5760
|
+
db.close();
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
function listSnapshots() {
|
|
5765
|
+
initDatabase();
|
|
5766
|
+
const db = openDatabase();
|
|
5767
|
+
try {
|
|
5768
|
+
return db.prepare("SELECT id, workspace, path, created_at FROM snapshots ORDER BY id DESC LIMIT 50").all();
|
|
5769
|
+
} finally {
|
|
5770
|
+
db.close();
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
async function restoreSnapshot(id) {
|
|
5775
|
+
initDatabase();
|
|
5776
|
+
const db = openDatabase();
|
|
5777
|
+
let row;
|
|
5778
|
+
try {
|
|
5779
|
+
row = db.prepare("SELECT * FROM snapshots WHERE id = ?").get(id);
|
|
5780
|
+
} finally {
|
|
5781
|
+
db.close();
|
|
5782
|
+
}
|
|
5783
|
+
if (!row) throw new Error(`Snapshot не найден: ${id}`);
|
|
5784
|
+
await cp(row.path, row.workspace, { recursive: true, force: true });
|
|
5785
|
+
}
|
|
5786
|
+
|
|
5787
|
+
function unifiedPreview(before, after) {
|
|
5788
|
+
const beforeLines = before.split(/\r?\n/);
|
|
5789
|
+
const afterLines = after.split(/\r?\n/);
|
|
5790
|
+
const output = ["--- before", "+++ after"];
|
|
5791
|
+
const max = Math.max(beforeLines.length, afterLines.length);
|
|
5792
|
+
for (let index = 0; index < Math.min(max, 80); index += 1) {
|
|
5793
|
+
if (beforeLines[index] !== afterLines[index]) {
|
|
5794
|
+
if (beforeLines[index] !== undefined) output.push(`- ${beforeLines[index]}`);
|
|
5795
|
+
if (afterLines[index] !== undefined) output.push(`+ ${afterLines[index]}`);
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
return output.join("\n");
|
|
5799
|
+
}
|
|
5800
|
+
|
|
4975
5801
|
async function executeRpc(method, options = {}) {
|
|
4976
5802
|
if (method === "status") {
|
|
4977
5803
|
return { db: getDbStatus(), sync: getSyncStatus(), activeProfile: getActiveProfileName(await loadConfig()) };
|
|
@@ -4996,6 +5822,15 @@ async function executeRpc(method, options = {}) {
|
|
|
4996
5822
|
await assertPermission("sync");
|
|
4997
5823
|
return syncDataset(options.dataset || "schools");
|
|
4998
5824
|
}
|
|
5825
|
+
if (method === "files.tree") {
|
|
5826
|
+
return filesTree(options.path || ".", options);
|
|
5827
|
+
}
|
|
5828
|
+
if (method === "files.read") {
|
|
5829
|
+
return { path: options.path, text: await filesRead(options.path, options) };
|
|
5830
|
+
}
|
|
5831
|
+
if (method === "files.search") {
|
|
5832
|
+
return filesSearch(options.query || options.search || "", options);
|
|
5833
|
+
}
|
|
4999
5834
|
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
|
|
5000
5835
|
}
|
|
5001
5836
|
|
|
@@ -5155,6 +5990,10 @@ function mergeConfig(base, override) {
|
|
|
5155
5990
|
...(override.permissions?.localTools || {}),
|
|
5156
5991
|
},
|
|
5157
5992
|
},
|
|
5993
|
+
files: {
|
|
5994
|
+
...base.files,
|
|
5995
|
+
...(override.files || {}),
|
|
5996
|
+
},
|
|
5158
5997
|
memory: {
|
|
5159
5998
|
...base.memory,
|
|
5160
5999
|
...(override.memory || {}),
|
|
@@ -5190,7 +6029,7 @@ function validateConfig(config) {
|
|
|
5190
6029
|
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
5191
6030
|
}
|
|
5192
6031
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
5193
|
-
if (!
|
|
6032
|
+
if (!ALL_LOCAL_TOOLS.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
5194
6033
|
}
|
|
5195
6034
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
5196
6035
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
@@ -5205,8 +6044,9 @@ function configSchema() {
|
|
|
5205
6044
|
properties: {
|
|
5206
6045
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
5207
6046
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
5208
|
-
permissions: { localTools:
|
|
6047
|
+
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
5209
6048
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
6049
|
+
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
|
5210
6050
|
skills: { enabled: "array of skill names" },
|
|
5211
6051
|
daemon: { host: "127.0.0.1", port: DAEMON_PORT },
|
|
5212
6052
|
},
|