@iola_adm/iola-cli 0.1.24 → 0.1.25
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 +4 -0
- package/package.json +1 -1
- package/src/cli.js +388 -19
- package/wiki/Home.md +1 -0
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +4 -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/README.md
CHANGED
|
@@ -52,6 +52,8 @@ iola agent
|
|
|
52
52
|
```bash
|
|
53
53
|
iola skills list
|
|
54
54
|
iola tools toolsets
|
|
55
|
+
iola files mode read-only
|
|
56
|
+
iola files tree .
|
|
55
57
|
iola context init
|
|
56
58
|
iola cron list
|
|
57
59
|
iola daemon status
|
|
@@ -79,6 +81,7 @@ iola version --check
|
|
|
79
81
|
- [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили)
|
|
80
82
|
- [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
|
|
81
83
|
- [Skills и toolsets](https://github.com/adm-iola/iola-cli/wiki/Skills-и-toolsets)
|
|
84
|
+
- [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
|
|
82
85
|
- [Daemon, RPC и cron](https://github.com/adm-iola/iola-cli/wiki/Daemon-RPC-и-cron)
|
|
83
86
|
- [Контекст и память](https://github.com/adm-iola/iola-cli/wiki/Контекст-и-память)
|
|
84
87
|
- [Команды](https://github.com/adm-iola/iola-cli/wiki/Команды)
|
|
@@ -91,6 +94,7 @@ iola version --check
|
|
|
91
94
|
- AI-профили для Ollama, OpenAI, OpenRouter и Codex CLI;
|
|
92
95
|
- локальный tool-agent для слабых моделей;
|
|
93
96
|
- skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
97
|
+
- управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
|
|
94
98
|
- cron-задачи, локальный daemon и RPC для автоматизаций;
|
|
95
99
|
- контекстные файлы `IOLA.md` и `.iola/context.md`;
|
|
96
100
|
- интеграция с публичным MCP-сервером Йошкар-Олы.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -19,6 +19,8 @@ const SECRETS_FILE = path.join(CONFIG_DIR, "secrets.json");
|
|
|
19
19
|
const DB_FILE = path.join(CONFIG_DIR, "iola.db");
|
|
20
20
|
const DB_SCHEMA_VERSION = 4;
|
|
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,7 @@ const COMMANDS = new Map([
|
|
|
213
241
|
["context", handleContext],
|
|
214
242
|
["skills", handleSkills],
|
|
215
243
|
["tools", handleTools],
|
|
244
|
+
["files", handleFiles],
|
|
216
245
|
["cron", handleCron],
|
|
217
246
|
["daemon", handleDaemon],
|
|
218
247
|
["rpc", handleRpc],
|
|
@@ -315,6 +344,7 @@ Usage:
|
|
|
315
344
|
iola context list|show|init
|
|
316
345
|
iola skills list|show|paths|enable|disable
|
|
317
346
|
iola tools list|toolsets|enable|disable|profile
|
|
347
|
+
iola files status|mode|approvals|tree|read|search|write|patch
|
|
318
348
|
iola cron list|add|delete|run|tick
|
|
319
349
|
iola daemon start|status
|
|
320
350
|
iola rpc call METHOD [ARGS] [--json]
|
|
@@ -344,7 +374,7 @@ Usage:
|
|
|
344
374
|
iola config set api.mcpBaseUrl URL
|
|
345
375
|
iola config reset
|
|
346
376
|
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]
|
|
377
|
+
iola ask TEXT [--profile NAME] [--model MODEL] [--tools] [--files] [--reasoning fast|verify|vote] [--output FILE] [--schema json|table] [--events] [--no-history] [--bare] [--quiet] [--no-color] [--fail-on-empty]
|
|
348
378
|
iola data LAYER [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
|
|
349
379
|
iola ai ask TEXT [--provider ollama|openai|openrouter] [--model MODEL]
|
|
350
380
|
iola ai context TEXT [--json]
|
|
@@ -555,6 +585,11 @@ async function handleAgentLine(line, state) {
|
|
|
555
585
|
return false;
|
|
556
586
|
}
|
|
557
587
|
|
|
588
|
+
if (command === "files") {
|
|
589
|
+
await handleFiles(args);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
558
593
|
if (command === "cron") {
|
|
559
594
|
await handleCron(args);
|
|
560
595
|
return false;
|
|
@@ -677,6 +712,7 @@ async function handleAgentLine(line, state) {
|
|
|
677
712
|
wiki: ["wiki", args],
|
|
678
713
|
context: ["context", args],
|
|
679
714
|
skills: ["skills", args],
|
|
715
|
+
files: ["files", args],
|
|
680
716
|
cron: ["cron", args],
|
|
681
717
|
daemon: ["daemon", args],
|
|
682
718
|
rpc: ["rpc", args],
|
|
@@ -724,6 +760,7 @@ function printAgentHelp() {
|
|
|
724
760
|
/skills list
|
|
725
761
|
/permissions
|
|
726
762
|
/tools
|
|
763
|
+
/files status
|
|
727
764
|
/cron list
|
|
728
765
|
/daemon status
|
|
729
766
|
/rpc call status
|
|
@@ -1453,6 +1490,7 @@ async function handleWiki(args) {
|
|
|
1453
1490
|
["AI-профили", `${base}/AI-профили`],
|
|
1454
1491
|
["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
|
|
1455
1492
|
["Skills и toolsets", `${base}/Skills-и-toolsets`],
|
|
1493
|
+
["Локальные файлы", `${base}/Локальные-файлы`],
|
|
1456
1494
|
["Daemon, RPC и cron", `${base}/Daemon-RPC-и-cron`],
|
|
1457
1495
|
["Контекст и память", `${base}/Контекст-и-память`],
|
|
1458
1496
|
["Команды", `${base}/Команды`],
|
|
@@ -1617,6 +1655,84 @@ async function handleTools(args) {
|
|
|
1617
1655
|
throw new Error("Команды tools: list, toolsets, enable NAME, disable NAME, profile NAME.");
|
|
1618
1656
|
}
|
|
1619
1657
|
|
|
1658
|
+
async function handleFiles(args) {
|
|
1659
|
+
const [action = "status", target, ...rest] = args;
|
|
1660
|
+
const options = parseOptions(rest);
|
|
1661
|
+
const config = await loadConfig();
|
|
1662
|
+
|
|
1663
|
+
if (action === "status") {
|
|
1664
|
+
printKeyValue({
|
|
1665
|
+
mode: config.files?.mode || "locked",
|
|
1666
|
+
approvals: config.files?.approvals || "on-write",
|
|
1667
|
+
workspaceRoot: resolveWorkspaceRoot(config),
|
|
1668
|
+
maxReadBytes: config.files?.maxReadBytes || 200000,
|
|
1669
|
+
readFiles: config.permissions?.readFiles ? "allow" : "deny",
|
|
1670
|
+
writeFiles: config.permissions?.writeFiles ? "allow" : "deny",
|
|
1671
|
+
editFiles: config.permissions?.editFiles ? "allow" : "deny",
|
|
1672
|
+
});
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (action === "mode") {
|
|
1677
|
+
if (!["locked", "read-only", "workspace-write", "full-access"].includes(target)) {
|
|
1678
|
+
throw new Error("Режимы файлов: locked, read-only, workspace-write, full-access.");
|
|
1679
|
+
}
|
|
1680
|
+
await setFilesMode(target, config);
|
|
1681
|
+
console.log(`Файловый режим: ${target}`);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (action === "approvals") {
|
|
1686
|
+
if (!["never", "on-write", "on-danger", "always"].includes(target)) {
|
|
1687
|
+
throw new Error("Политики approvals: never, on-write, on-danger, always.");
|
|
1688
|
+
}
|
|
1689
|
+
await saveConfig({ files: { ...(config.files || {}), approvals: target } });
|
|
1690
|
+
console.log(`Файловые подтверждения: ${target}`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (action === "tree") {
|
|
1695
|
+
const rows = await filesTree(target || ".", options);
|
|
1696
|
+
if (options.json) printJson(rows);
|
|
1697
|
+
else printTable(rows, [["type", "Тип"], ["path", "Путь"], ["size", "Размер"]]);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if (action === "read") {
|
|
1702
|
+
if (!target) throw new Error("Пример: iola files read README.md");
|
|
1703
|
+
console.log(await filesRead(target, options));
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (action === "search") {
|
|
1708
|
+
const query = target;
|
|
1709
|
+
if (!query) throw new Error('Пример: iola files search "Петрова" --path .');
|
|
1710
|
+
const rows = await filesSearch(query, options);
|
|
1711
|
+
if (options.json) printJson(rows);
|
|
1712
|
+
else printTable(rows, [["file", "Файл"], ["line", "Строка"], ["text", "Текст"]]);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (action === "write") {
|
|
1717
|
+
if (!target) throw new Error('Пример: iola files write report.md --text "..."');
|
|
1718
|
+
const text = options.text ?? rest.join(" ");
|
|
1719
|
+
if (!text) throw new Error('Для записи нужен --text "..." или текст после пути.');
|
|
1720
|
+
await filesWrite(target, text, { append: Boolean(options.append) });
|
|
1721
|
+
console.log(`Файл записан: ${target}`);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (action === "patch") {
|
|
1726
|
+
if (!target) throw new Error('Пример: iola files patch README.md --search old --replace new');
|
|
1727
|
+
if (!options.search || options.replace === undefined) throw new Error("Для patch нужны --search и --replace.");
|
|
1728
|
+
const result = await filesPatch(target, options.search, options.replace);
|
|
1729
|
+
printKeyValue(result);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
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.");
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1620
1736
|
async function handleCron(args) {
|
|
1621
1737
|
const [action = "list", ...rest] = args;
|
|
1622
1738
|
const options = parseOptions(rest);
|
|
@@ -1722,6 +1838,14 @@ async function handlePermissions(args) {
|
|
|
1722
1838
|
value: permissions.localTools?.[tool] === false ? "deny" : "allow",
|
|
1723
1839
|
scope: "local-tool",
|
|
1724
1840
|
})),
|
|
1841
|
+
...FILE_TOOLS.map((tool) => ({
|
|
1842
|
+
permission: `localTools.${tool}`,
|
|
1843
|
+
value: permissions.localTools?.[tool] === true ? "allow" : "deny",
|
|
1844
|
+
scope: "file-tool",
|
|
1845
|
+
})),
|
|
1846
|
+
{ permission: "readFiles", value: permissions.readFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
1847
|
+
{ permission: "editFiles", value: permissions.editFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
1848
|
+
{ permission: "deleteFiles", value: permissions.deleteFiles === true ? "allow" : "deny", scope: "filesystem" },
|
|
1725
1849
|
{ permission: "writeFiles", value: permissions.writeFiles === false ? "deny" : "allow", scope: "runtime" },
|
|
1726
1850
|
{ permission: "sync", value: permissions.sync === false ? "deny" : "allow", scope: "runtime" },
|
|
1727
1851
|
{ permission: "externalApi", value: permissions.externalApi === false ? "deny" : "allow", scope: "network" },
|
|
@@ -1743,12 +1867,12 @@ async function handlePermissions(args) {
|
|
|
1743
1867
|
const allow = action === "allow";
|
|
1744
1868
|
const next = { ...(config.permissions || DEFAULT_AI_CONFIG.permissions) };
|
|
1745
1869
|
next.localTools = { ...(next.localTools || {}) };
|
|
1746
|
-
if (
|
|
1870
|
+
if (ALL_LOCAL_TOOLS.includes(name)) {
|
|
1747
1871
|
next.localTools[name] = allow;
|
|
1748
1872
|
} else if (name in DEFAULT_AI_CONFIG.permissions) {
|
|
1749
1873
|
next[name] = allow;
|
|
1750
1874
|
} else {
|
|
1751
|
-
throw new Error(`Неизвестное разрешение: ${name}. Доступно: ${[...
|
|
1875
|
+
throw new Error(`Неизвестное разрешение: ${name}. Доступно: ${[...ALL_LOCAL_TOOLS, "readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"].join(", ")}`);
|
|
1752
1876
|
}
|
|
1753
1877
|
await saveConfig({ permissions: next });
|
|
1754
1878
|
console.log(`${name}: ${allow ? "allow" : "deny"}`);
|
|
@@ -3910,7 +4034,7 @@ function resolveAiProfile(config, options = {}) {
|
|
|
3910
4034
|
async function localToolAsk(question, providerConfig, options) {
|
|
3911
4035
|
await ensureLocalData();
|
|
3912
4036
|
const plan = await buildLocalToolPlan(question, providerConfig, options);
|
|
3913
|
-
const validated = validateToolPlan(plan);
|
|
4037
|
+
const validated = validateToolPlan(plan, options);
|
|
3914
4038
|
const result = await executeToolPlan(validated);
|
|
3915
4039
|
const answer = formatToolResult(result, options);
|
|
3916
4040
|
|
|
@@ -3942,8 +4066,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
3942
4066
|
const mode = options.reasoning || "verify";
|
|
3943
4067
|
const prompt = [
|
|
3944
4068
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
3945
|
-
|
|
4069
|
+
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
3946
4070
|
"Схема: {\"steps\":[{\"tool\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
4071
|
+
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
4072
|
"Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
|
|
3948
4073
|
`Вопрос: ${question}`,
|
|
3949
4074
|
].join("\n");
|
|
@@ -3952,11 +4077,11 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
3952
4077
|
const raw = await callOllama(providerConfig, [{ role: "user", content: prompt }]);
|
|
3953
4078
|
const parsed = parseJsonObject(raw);
|
|
3954
4079
|
if (mode === "vote") {
|
|
3955
|
-
return chooseBestPlan([parsed, inferToolPlan(question)]);
|
|
4080
|
+
return chooseBestPlan([parsed, inferToolPlan(question, options)], options);
|
|
3956
4081
|
}
|
|
3957
4082
|
return parsed;
|
|
3958
4083
|
} catch {
|
|
3959
|
-
return inferToolPlan(question);
|
|
4084
|
+
return inferToolPlan(question, options);
|
|
3960
4085
|
}
|
|
3961
4086
|
}
|
|
3962
4087
|
|
|
@@ -3966,7 +4091,7 @@ function parseJsonObject(text) {
|
|
|
3966
4091
|
return JSON.parse(match[0]);
|
|
3967
4092
|
}
|
|
3968
4093
|
|
|
3969
|
-
function inferToolPlan(question) {
|
|
4094
|
+
function inferToolPlan(question, options = {}) {
|
|
3970
4095
|
const normalized = question.toLocaleLowerCase("ru-RU");
|
|
3971
4096
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
3972
4097
|
const steps = [];
|
|
@@ -3979,13 +4104,20 @@ function inferToolPlan(question) {
|
|
|
3979
4104
|
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
3980
4105
|
steps.push({ tool: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
3981
4106
|
}
|
|
4107
|
+
if (options.files || normalized.includes("файл") || normalized.includes("папк") || normalized.includes("readme")) {
|
|
4108
|
+
if (normalized.includes("найди") || normalized.includes("поиск")) {
|
|
4109
|
+
steps.unshift({ tool: "files_search", args: { query: question, path: "." } });
|
|
4110
|
+
} else {
|
|
4111
|
+
steps.unshift({ tool: "files_tree", args: { path: ".", depth: 2, limit: 80 } });
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
3982
4114
|
return { steps };
|
|
3983
4115
|
}
|
|
3984
4116
|
|
|
3985
|
-
function chooseBestPlan(plans) {
|
|
4117
|
+
function chooseBestPlan(plans, options = {}) {
|
|
3986
4118
|
return plans.find((plan) => {
|
|
3987
4119
|
try {
|
|
3988
|
-
validateToolPlan(plan);
|
|
4120
|
+
validateToolPlan(plan, options);
|
|
3989
4121
|
return true;
|
|
3990
4122
|
} catch {
|
|
3991
4123
|
return false;
|
|
@@ -3993,8 +4125,8 @@ function chooseBestPlan(plans) {
|
|
|
3993
4125
|
}) || plans.at(-1);
|
|
3994
4126
|
}
|
|
3995
4127
|
|
|
3996
|
-
function validateToolPlan(plan) {
|
|
3997
|
-
const allowed = new Set(
|
|
4128
|
+
function validateToolPlan(plan, options = {}) {
|
|
4129
|
+
const allowed = new Set(availableToolNames(options));
|
|
3998
4130
|
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
3999
4131
|
for (const step of plan.steps) {
|
|
4000
4132
|
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
@@ -4002,6 +4134,10 @@ function validateToolPlan(plan) {
|
|
|
4002
4134
|
return plan;
|
|
4003
4135
|
}
|
|
4004
4136
|
|
|
4137
|
+
function availableToolNames(options = {}) {
|
|
4138
|
+
return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4005
4141
|
async function executeToolPlan(plan) {
|
|
4006
4142
|
let current = [];
|
|
4007
4143
|
const outputs = [];
|
|
@@ -4027,6 +4163,24 @@ async function executeToolPlan(plan) {
|
|
|
4027
4163
|
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
4028
4164
|
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
4029
4165
|
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
4166
|
+
} else if (step.tool === "files_tree") {
|
|
4167
|
+
current = await filesTree(step.args?.path || ".", step.args || {});
|
|
4168
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4169
|
+
} else if (step.tool === "files_read") {
|
|
4170
|
+
const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
|
|
4171
|
+
current = [{ path: step.args?.path || step.args?.file || ".", text }];
|
|
4172
|
+
outputs.push({ tool: step.tool, bytes: text.length });
|
|
4173
|
+
} else if (step.tool === "files_search") {
|
|
4174
|
+
current = await filesSearch(step.args?.query || "", { path: step.args?.path || ".", limit: step.args?.limit || 50 });
|
|
4175
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
4176
|
+
} else if (step.tool === "files_write") {
|
|
4177
|
+
await filesWrite(step.args?.path || step.args?.file, step.args?.text || "", { append: Boolean(step.args?.append) });
|
|
4178
|
+
current = [{ path: step.args?.path || step.args?.file, status: "written" }];
|
|
4179
|
+
outputs.push({ tool: step.tool, output: step.args?.path || step.args?.file, rows: 1 });
|
|
4180
|
+
} else if (step.tool === "files_patch") {
|
|
4181
|
+
const result = await filesPatch(step.args?.path || step.args?.file, step.args?.search || "", step.args?.replace || "");
|
|
4182
|
+
current = [result];
|
|
4183
|
+
outputs.push({ tool: step.tool, output: result.path, replacements: result.replacements });
|
|
4030
4184
|
}
|
|
4031
4185
|
await runHooks("AfterTool", { tool: step.tool, rows: current.length });
|
|
4032
4186
|
}
|
|
@@ -4071,7 +4225,7 @@ async function runHooks(event, payload = {}) {
|
|
|
4071
4225
|
async function assertPermission(name) {
|
|
4072
4226
|
const config = await loadConfig();
|
|
4073
4227
|
const permissions = applyToolsetPermissions(config.permissions || DEFAULT_AI_CONFIG.permissions, config.toolsets?.enabled || []);
|
|
4074
|
-
if (
|
|
4228
|
+
if (ALL_LOCAL_TOOLS.includes(name)) {
|
|
4075
4229
|
if (permissions.localTools?.[name] === false) {
|
|
4076
4230
|
throw new Error(`Tool запрещен политикой permissions: ${name}`);
|
|
4077
4231
|
}
|
|
@@ -4584,12 +4738,12 @@ function parseOptions(args) {
|
|
|
4584
4738
|
|
|
4585
4739
|
for (let index = 0; index < args.length; index += 1) {
|
|
4586
4740
|
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") {
|
|
4741
|
+
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 === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--append") {
|
|
4588
4742
|
result[arg.slice(2)] = true;
|
|
4589
4743
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
4590
4744
|
result.check = true;
|
|
4591
4745
|
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") {
|
|
4746
|
+
} 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
4747
|
result[arg.slice(2)] = args[index + 1];
|
|
4594
4748
|
index += 1;
|
|
4595
4749
|
} else {
|
|
@@ -4972,6 +5126,207 @@ function readRequestBody(req) {
|
|
|
4972
5126
|
});
|
|
4973
5127
|
}
|
|
4974
5128
|
|
|
5129
|
+
async function setFilesMode(mode, config = null) {
|
|
5130
|
+
const current = config || await loadConfig();
|
|
5131
|
+
const localTools = { ...(current.permissions?.localTools || {}) };
|
|
5132
|
+
for (const tool of FILE_TOOLS) localTools[tool] = false;
|
|
5133
|
+
const permissions = {
|
|
5134
|
+
...(current.permissions || DEFAULT_AI_CONFIG.permissions),
|
|
5135
|
+
localTools,
|
|
5136
|
+
readFiles: false,
|
|
5137
|
+
editFiles: false,
|
|
5138
|
+
deleteFiles: false,
|
|
5139
|
+
};
|
|
5140
|
+
const enabled = new Set(current.toolsets?.enabled || []);
|
|
5141
|
+
enabled.delete("local-files-read");
|
|
5142
|
+
enabled.delete("local-files-write");
|
|
5143
|
+
|
|
5144
|
+
if (mode === "read-only") {
|
|
5145
|
+
permissions.readFiles = true;
|
|
5146
|
+
for (const tool of ["files_tree", "files_read", "files_search"]) permissions.localTools[tool] = true;
|
|
5147
|
+
enabled.add("local-files-read");
|
|
5148
|
+
} else if (mode === "workspace-write") {
|
|
5149
|
+
permissions.readFiles = true;
|
|
5150
|
+
permissions.writeFiles = true;
|
|
5151
|
+
permissions.editFiles = true;
|
|
5152
|
+
for (const tool of FILE_TOOLS) permissions.localTools[tool] = true;
|
|
5153
|
+
enabled.add("local-files-read");
|
|
5154
|
+
enabled.add("local-files-write");
|
|
5155
|
+
} else if (mode === "full-access") {
|
|
5156
|
+
permissions.readFiles = true;
|
|
5157
|
+
permissions.writeFiles = true;
|
|
5158
|
+
permissions.editFiles = true;
|
|
5159
|
+
permissions.deleteFiles = false;
|
|
5160
|
+
for (const tool of FILE_TOOLS) permissions.localTools[tool] = true;
|
|
5161
|
+
enabled.add("local-files-read");
|
|
5162
|
+
enabled.add("local-files-write");
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
await saveConfig({
|
|
5166
|
+
permissions,
|
|
5167
|
+
toolsets: { ...(current.toolsets || {}), enabled: [...enabled] },
|
|
5168
|
+
files: { ...(current.files || {}), mode },
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
function resolveWorkspaceRoot(config) {
|
|
5173
|
+
return path.resolve(process.cwd(), config.files?.workspaceRoot || ".");
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
async function resolveFileTarget(target, operation) {
|
|
5177
|
+
if (!target) throw new Error("Путь к файлу обязателен.");
|
|
5178
|
+
const config = await loadConfig();
|
|
5179
|
+
const mode = config.files?.mode || "locked";
|
|
5180
|
+
if (mode === "locked") throw new Error("Файловые операции заблокированы. Включите: iola files mode read-only");
|
|
5181
|
+
const workspaceRoot = resolveWorkspaceRoot(config);
|
|
5182
|
+
const resolved = path.resolve(workspaceRoot, target);
|
|
5183
|
+
const relative = path.relative(workspaceRoot, resolved);
|
|
5184
|
+
const insideWorkspace = relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
5185
|
+
|
|
5186
|
+
if ((mode === "read-only" || mode === "workspace-write") && !insideWorkspace && resolved !== workspaceRoot) {
|
|
5187
|
+
throw new Error(`Путь вне workspace запрещен режимом ${mode}: ${resolved}`);
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
const blocked = config.files?.blockedGlobs || [];
|
|
5191
|
+
const normalized = resolved.toLocaleLowerCase("ru-RU");
|
|
5192
|
+
if (blocked.some((pattern) => filePatternMatches(normalized, pattern))) {
|
|
5193
|
+
throw new Error(`Путь заблокирован политикой безопасности: ${target}`);
|
|
5194
|
+
}
|
|
5195
|
+
|
|
5196
|
+
if (operation === "read") await assertPermission("readFiles");
|
|
5197
|
+
if (operation === "write") await assertPermission("writeFiles");
|
|
5198
|
+
if (operation === "edit") await assertPermission("editFiles");
|
|
5199
|
+
if (operation === "delete") await assertPermission("deleteFiles");
|
|
5200
|
+
|
|
5201
|
+
return { config, resolved, workspaceRoot, relative: resolved === workspaceRoot ? "." : relative, insideWorkspace };
|
|
5202
|
+
}
|
|
5203
|
+
|
|
5204
|
+
function filePatternMatches(normalizedPath, pattern) {
|
|
5205
|
+
const normalizedPattern = String(pattern).toLocaleLowerCase("ru-RU").replace(/\*/g, "");
|
|
5206
|
+
if (!normalizedPattern) return false;
|
|
5207
|
+
return normalizedPath.split(/[\\/]/).includes(normalizedPattern) || normalizedPath.includes(normalizedPattern);
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
function isBlockedPathForConfig(fullPath, config) {
|
|
5211
|
+
const normalized = fullPath.toLocaleLowerCase("ru-RU");
|
|
5212
|
+
return (config.files?.blockedGlobs || []).some((pattern) => filePatternMatches(normalized, pattern));
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
async function filesTree(target = ".", options = {}) {
|
|
5216
|
+
await assertPermission("files_tree");
|
|
5217
|
+
const { resolved, workspaceRoot } = await resolveFileTarget(target, "read");
|
|
5218
|
+
const depth = Number(options.depth || 2);
|
|
5219
|
+
const limit = Number(options.limit || 100);
|
|
5220
|
+
const rows = [];
|
|
5221
|
+
await walkFiles(resolved, workspaceRoot, rows, depth, limit, (await loadConfig()));
|
|
5222
|
+
return rows;
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
async function walkFiles(directory, workspaceRoot, rows, depth, limit, config) {
|
|
5226
|
+
if (rows.length >= limit || depth < 0) return;
|
|
5227
|
+
let entries = [];
|
|
5228
|
+
try {
|
|
5229
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
5230
|
+
} catch {
|
|
5231
|
+
return;
|
|
5232
|
+
}
|
|
5233
|
+
for (const entry of entries) {
|
|
5234
|
+
if (rows.length >= limit) break;
|
|
5235
|
+
const full = path.join(directory, entry.name);
|
|
5236
|
+
if (isBlockedPathForConfig(full, config)) continue;
|
|
5237
|
+
const relative = path.relative(workspaceRoot, full) || ".";
|
|
5238
|
+
let size = "-";
|
|
5239
|
+
try {
|
|
5240
|
+
size = entry.isFile() ? (await stat(full)).size : "-";
|
|
5241
|
+
} catch {
|
|
5242
|
+
size = "-";
|
|
5243
|
+
}
|
|
5244
|
+
rows.push({ type: entry.isDirectory() ? "dir" : "file", path: relative, size });
|
|
5245
|
+
if (entry.isDirectory()) await walkFiles(full, workspaceRoot, rows, depth - 1, limit, config);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5249
|
+
async function filesRead(target, options = {}) {
|
|
5250
|
+
await assertPermission("files_read");
|
|
5251
|
+
const { config, resolved } = await resolveFileTarget(target, "read");
|
|
5252
|
+
const info = await stat(resolved);
|
|
5253
|
+
if (!info.isFile()) throw new Error(`Это не файл: ${target}`);
|
|
5254
|
+
const maxBytes = Number(options.maxBytes || config.files?.maxReadBytes || 200000);
|
|
5255
|
+
if (info.size > maxBytes) throw new Error(`Файл слишком большой: ${info.size} байт. Лимит: ${maxBytes}`);
|
|
5256
|
+
return readFile(resolved, "utf8");
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
async function filesSearch(query, options = {}) {
|
|
5260
|
+
await assertPermission("files_search");
|
|
5261
|
+
if (!query) throw new Error("Строка поиска обязательна.");
|
|
5262
|
+
const rows = await filesTree(options.path || ".", { depth: Number(options.depth || 4), limit: Number(options.limit || 200) });
|
|
5263
|
+
const results = [];
|
|
5264
|
+
for (const row of rows.filter((item) => item.type === "file")) {
|
|
5265
|
+
if (results.length >= Number(options.limit || 50)) break;
|
|
5266
|
+
try {
|
|
5267
|
+
const text = await filesRead(row.path, { maxBytes: 500000 });
|
|
5268
|
+
const lines = text.split(/\r?\n/);
|
|
5269
|
+
lines.forEach((line, index) => {
|
|
5270
|
+
if (results.length < Number(options.limit || 50) && line.toLocaleLowerCase("ru-RU").includes(String(query).toLocaleLowerCase("ru-RU"))) {
|
|
5271
|
+
results.push({ file: row.path, line: index + 1, text: line.trim().slice(0, 240) });
|
|
5272
|
+
}
|
|
5273
|
+
});
|
|
5274
|
+
} catch {
|
|
5275
|
+
// Binary, blocked or oversized files are skipped.
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
return results;
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
async function filesWrite(target, text, options = {}) {
|
|
5282
|
+
await assertPermission("files_write");
|
|
5283
|
+
const { resolved, relative } = await resolveFileTarget(target, "write");
|
|
5284
|
+
await maybeConfirmFileOperation("write", relative, text);
|
|
5285
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
5286
|
+
if (options.append) {
|
|
5287
|
+
await appendFile(resolved, text, "utf8");
|
|
5288
|
+
} else {
|
|
5289
|
+
await writeFile(resolved, text, "utf8");
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
async function filesPatch(target, search, replace) {
|
|
5294
|
+
await assertPermission("files_patch");
|
|
5295
|
+
const { resolved, relative } = await resolveFileTarget(target, "edit");
|
|
5296
|
+
const current = await readFile(resolved, "utf8");
|
|
5297
|
+
if (!current.includes(search)) throw new Error("Искомый фрагмент не найден.");
|
|
5298
|
+
const next = current.split(search).join(replace);
|
|
5299
|
+
const replacements = current.split(search).length - 1;
|
|
5300
|
+
await maybeConfirmFileOperation("patch", relative, unifiedPreview(current, next));
|
|
5301
|
+
await writeFile(resolved, next, "utf8");
|
|
5302
|
+
return { path: relative, replacements };
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
async function maybeConfirmFileOperation(operation, target, preview) {
|
|
5306
|
+
const config = await loadConfig();
|
|
5307
|
+
const approvals = config.files?.approvals || "on-write";
|
|
5308
|
+
const needsApproval = approvals === "always" || approvals === "on-write" || (approvals === "on-danger" && operation !== "write");
|
|
5309
|
+
if (!needsApproval) return;
|
|
5310
|
+
console.log(`Файловая операция: ${operation} ${target}`);
|
|
5311
|
+
if (preview) console.log(String(preview).slice(0, 2000));
|
|
5312
|
+
const ok = await confirm("Продолжить? [y/N] ");
|
|
5313
|
+
if (!ok) throw new Error("Файловая операция отменена.");
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5316
|
+
function unifiedPreview(before, after) {
|
|
5317
|
+
const beforeLines = before.split(/\r?\n/);
|
|
5318
|
+
const afterLines = after.split(/\r?\n/);
|
|
5319
|
+
const output = ["--- before", "+++ after"];
|
|
5320
|
+
const max = Math.max(beforeLines.length, afterLines.length);
|
|
5321
|
+
for (let index = 0; index < Math.min(max, 80); index += 1) {
|
|
5322
|
+
if (beforeLines[index] !== afterLines[index]) {
|
|
5323
|
+
if (beforeLines[index] !== undefined) output.push(`- ${beforeLines[index]}`);
|
|
5324
|
+
if (afterLines[index] !== undefined) output.push(`+ ${afterLines[index]}`);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
return output.join("\n");
|
|
5328
|
+
}
|
|
5329
|
+
|
|
4975
5330
|
async function executeRpc(method, options = {}) {
|
|
4976
5331
|
if (method === "status") {
|
|
4977
5332
|
return { db: getDbStatus(), sync: getSyncStatus(), activeProfile: getActiveProfileName(await loadConfig()) };
|
|
@@ -4996,6 +5351,15 @@ async function executeRpc(method, options = {}) {
|
|
|
4996
5351
|
await assertPermission("sync");
|
|
4997
5352
|
return syncDataset(options.dataset || "schools");
|
|
4998
5353
|
}
|
|
5354
|
+
if (method === "files.tree") {
|
|
5355
|
+
return filesTree(options.path || ".", options);
|
|
5356
|
+
}
|
|
5357
|
+
if (method === "files.read") {
|
|
5358
|
+
return { path: options.path, text: await filesRead(options.path, options) };
|
|
5359
|
+
}
|
|
5360
|
+
if (method === "files.search") {
|
|
5361
|
+
return filesSearch(options.query || options.search || "", options);
|
|
5362
|
+
}
|
|
4999
5363
|
throw new Error(`RPC method неизвестен: ${method}. Доступно: status, search, card, quality, sync.`);
|
|
5000
5364
|
}
|
|
5001
5365
|
|
|
@@ -5155,6 +5519,10 @@ function mergeConfig(base, override) {
|
|
|
5155
5519
|
...(override.permissions?.localTools || {}),
|
|
5156
5520
|
},
|
|
5157
5521
|
},
|
|
5522
|
+
files: {
|
|
5523
|
+
...base.files,
|
|
5524
|
+
...(override.files || {}),
|
|
5525
|
+
},
|
|
5158
5526
|
memory: {
|
|
5159
5527
|
...base.memory,
|
|
5160
5528
|
...(override.memory || {}),
|
|
@@ -5190,7 +5558,7 @@ function validateConfig(config) {
|
|
|
5190
5558
|
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
5191
5559
|
}
|
|
5192
5560
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
5193
|
-
if (!
|
|
5561
|
+
if (!ALL_LOCAL_TOOLS.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
5194
5562
|
}
|
|
5195
5563
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
5196
5564
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
@@ -5205,8 +5573,9 @@ function configSchema() {
|
|
|
5205
5573
|
properties: {
|
|
5206
5574
|
api: { required: ["baseUrl", "mcpBaseUrl"] },
|
|
5207
5575
|
ai: { required: ["activeProfile", "profiles"], providers: ["ollama", "openai", "openrouter", "codex"] },
|
|
5208
|
-
permissions: { localTools:
|
|
5576
|
+
permissions: { localTools: ALL_LOCAL_TOOLS, runtime: ["readFiles", "writeFiles", "editFiles", "deleteFiles", "sync", "externalApi", "externalAi", "codex"] },
|
|
5209
5577
|
toolsets: { available: Object.keys(TOOLSETS) },
|
|
5578
|
+
files: { modes: ["locked", "read-only", "workspace-write", "full-access"], approvals: ["never", "on-write", "on-danger", "always"] },
|
|
5210
5579
|
skills: { enabled: "array of skill names" },
|
|
5211
5580
|
daemon: { host: "127.0.0.1", port: DAEMON_PORT },
|
|
5212
5581
|
},
|
package/wiki/Home.md
CHANGED
|
@@ -29,6 +29,7 @@ iola ask "найди школу 29"
|
|
|
29
29
|
- [AI-профили](AI-профили)
|
|
30
30
|
- [Локальный инструментальный агент](Локальный-инструментальный-агент)
|
|
31
31
|
- [Skills и toolsets](Skills-и-toolsets)
|
|
32
|
+
- [Локальные файлы](Локальные-файлы)
|
|
32
33
|
- [Daemon, RPC и cron](Daemon-RPC-и-cron)
|
|
33
34
|
- [Контекст и память](Контекст-и-память)
|
|
34
35
|
- [Команды](Команды)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Локальные файлы
|
|
2
|
+
|
|
3
|
+
Файловые операции в `iola-cli` управляются отдельным режимом доступа и политикой подтверждений.
|
|
4
|
+
|
|
5
|
+
Режимы:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
iola files mode locked
|
|
9
|
+
iola files mode read-only
|
|
10
|
+
iola files mode workspace-write
|
|
11
|
+
iola files mode full-access
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `locked` - файлы недоступны.
|
|
15
|
+
- `read-only` - дерево папок, чтение и поиск.
|
|
16
|
+
- `workspace-write` - чтение и запись только внутри workspace.
|
|
17
|
+
- `full-access` - расширенный режим, опасные пути все равно блокируются.
|
|
18
|
+
|
|
19
|
+
Подтверждения:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
iola files approvals never
|
|
23
|
+
iola files approvals on-write
|
|
24
|
+
iola files approvals on-danger
|
|
25
|
+
iola files approvals always
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Команды:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
iola files status
|
|
32
|
+
iola files tree .
|
|
33
|
+
iola files read README.md
|
|
34
|
+
iola files search "Петрова" --path .
|
|
35
|
+
iola files write report.md --text "Текст отчета"
|
|
36
|
+
iola files patch README.md --search old --replace new
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
AI/tool-agent:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
iola ask "найди в текущей папке упоминания школ" --profile local --tools --files
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
По умолчанию файловый режим `locked`. Запись требует включения `workspace-write` или `full-access`.
|
|
46
|
+
|