@iola_adm/iola-cli 0.1.50 → 0.1.52
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 -2
- package/package.json +1 -1
- package/skills/browser-agent/SKILL.md +8 -0
- package/skills/local-files/SKILL.md +10 -0
- package/src/cli.js +182 -29
- package/wiki/Skills-/320/270-toolsets.md +10 -1
- package/wiki//320/233/320/276/320/272/320/260/320/273/321/214/320/275/321/213/320/271-/320/270/320/275/321/201/321/202/321/200/321/203/320/274/320/265/320/275/321/202/320/260/320/273/321/214/320/275/321/213/320/271-/320/260/320/263/320/265/320/275/321/202.md +37 -7
package/README.md
CHANGED
|
@@ -126,10 +126,12 @@ iola version --check
|
|
|
126
126
|
- поиск и выгрузка открытых данных;
|
|
127
127
|
- локальная SQLite-БД, история, сессии и FTS-поиск;
|
|
128
128
|
- AI-профили для Ollama, OpenAI, OpenRouter и Codex CLI;
|
|
129
|
-
- локальный tool-agent для слабых
|
|
130
|
-
- skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
129
|
+
- локальный tool-agent для слабых моделей с минимальными tools `search_data`, `get_card`, `export_report`, `file_read`, `browser_open`;
|
|
130
|
+
- ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
|
|
131
131
|
- subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
|
|
132
132
|
- полноценный локальный MCP server по stdio/http: tools, resources и prompts;
|
|
133
|
+
- MCP-мост для локальной модели: встроенный `iola-local` доступен как `mcp:iola-local:TOOL`;
|
|
134
|
+
- дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json` в раздел `mcp.servers`;
|
|
133
135
|
- браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
|
|
134
136
|
- личное локальное подключение Госуслуг с явным согласием пользователя и хранением доступа только на его ПК;
|
|
135
137
|
- управляемые локальные файловые операции с режимами `locked`, `read-only`, `workspace-write`, `full-access`;
|
package/package.json
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-agent
|
|
3
|
+
description: Работа с браузером и веб-страницами только по явному запросу пользователя.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Используй браузерные инструменты только когда пользователь просит открыть сайт, страницу, URL, сделать скриншот, получить текст страницы или проверить поведение веб-интерфейса.
|
|
7
|
+
|
|
8
|
+
Не открывай браузер для обычного диалога и запросов по локальным открытым данным, если URL или браузерный сценарий не нужен.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: local-files
|
|
3
|
+
description: Работа с локальными файлами только по явному запросу пользователя.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Используй файловые инструменты только когда пользователь просит прочитать, найти, проверить или подготовить локальный файл, папку, архив или документ.
|
|
7
|
+
|
|
8
|
+
Не обращайся к локальным файлам для обычного диалога.
|
|
9
|
+
|
|
10
|
+
Перед записью или изменением файлов учитывай текущий режим permissions и approvals.
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { createServer } from "node:http";
|
|
@@ -27,9 +27,11 @@ const LOCAL_CONFIG_FILE = path.join(PROJECT_IOLA_DIR, "local.json");
|
|
|
27
27
|
const BROWSER_RUNTIME_DIR = path.join(CONFIG_DIR, "browser-runtime");
|
|
28
28
|
const BROWSER_RUNTIME_PACKAGE = path.join(BROWSER_RUNTIME_DIR, "node_modules", "playwright", "package.json");
|
|
29
29
|
const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
|
|
30
|
-
const LOCAL_TOOLS = ["
|
|
30
|
+
const LOCAL_TOOLS = ["search_data", "get_card", "export_report", "file_read", "browser_open"];
|
|
31
|
+
const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
|
|
31
32
|
const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
|
|
32
33
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS];
|
|
34
|
+
const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
|
|
33
35
|
const HOOK_EVENTS = ["SessionStart", "BeforeTool", "AfterTool", "PreToolUse", "PostToolUse", "OnError", "AfterSync", "BeforeExport", "SessionEnd"];
|
|
34
36
|
const DAEMON_PORT = Number(process.env.IOLA_DAEMON_PORT || 18790);
|
|
35
37
|
const GOSUSLUGI_CONSENT_VERSION = "2026-05-26-personal-local-v1";
|
|
@@ -54,11 +56,11 @@ const PROJECT_CONTEXT_DIR_FILE = path.join(process.cwd(), ".iola", "context.md")
|
|
|
54
56
|
const TOOLSETS = {
|
|
55
57
|
"data-read": {
|
|
56
58
|
description: "Чтение открытых данных и локальный поиск.",
|
|
57
|
-
permissions: { externalApi: true, localTools: {
|
|
59
|
+
permissions: { externalApi: true, localTools: { search_data: true, get_card: true, export_report: true } },
|
|
58
60
|
},
|
|
59
61
|
reports: {
|
|
60
62
|
description: "Отчеты, выгрузки и сохранение view.",
|
|
61
|
-
permissions: { writeFiles: true, localTools: {
|
|
63
|
+
permissions: { writeFiles: true, localTools: { export_report: true } },
|
|
62
64
|
},
|
|
63
65
|
sync: {
|
|
64
66
|
description: "Обновление локальной копии данных из публичного API.",
|
|
@@ -171,11 +173,11 @@ const DEFAULT_AI_CONFIG = {
|
|
|
171
173
|
},
|
|
172
174
|
permissions: {
|
|
173
175
|
localTools: {
|
|
174
|
-
|
|
176
|
+
search_data: true,
|
|
175
177
|
get_card: true,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
export_report: true,
|
|
179
|
+
file_read: false,
|
|
180
|
+
browser_open: true,
|
|
179
181
|
files_tree: false,
|
|
180
182
|
files_read: false,
|
|
181
183
|
files_search: false,
|
|
@@ -206,12 +208,15 @@ const DEFAULT_AI_CONFIG = {
|
|
|
206
208
|
suggestions: true,
|
|
207
209
|
},
|
|
208
210
|
skills: {
|
|
209
|
-
enabled: ["open-data", "reports", "local-model"],
|
|
211
|
+
enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
|
|
210
212
|
},
|
|
211
213
|
daemon: {
|
|
212
214
|
host: "127.0.0.1",
|
|
213
215
|
port: DAEMON_PORT,
|
|
214
216
|
},
|
|
217
|
+
mcp: {
|
|
218
|
+
servers: {},
|
|
219
|
+
},
|
|
215
220
|
cron: {
|
|
216
221
|
enabled: true,
|
|
217
222
|
},
|
|
@@ -842,10 +847,13 @@ async function startAgentRawInput() {
|
|
|
842
847
|
continue;
|
|
843
848
|
}
|
|
844
849
|
output.write(`> ${line}\n`);
|
|
850
|
+
const stopActivity = startActivityIndicator("работаю");
|
|
845
851
|
try {
|
|
846
852
|
const shouldExit = await handleAgentLine(line, state);
|
|
853
|
+
stopActivity();
|
|
847
854
|
if (shouldExit) break;
|
|
848
855
|
} catch (error) {
|
|
856
|
+
stopActivity();
|
|
849
857
|
console.error(error instanceof Error ? error.message : String(error));
|
|
850
858
|
}
|
|
851
859
|
render();
|
|
@@ -1331,6 +1339,27 @@ function clearAgentInputArea(state = null) {
|
|
|
1331
1339
|
if (state) state.renderedInputLines = 0;
|
|
1332
1340
|
}
|
|
1333
1341
|
|
|
1342
|
+
function startActivityIndicator(label = "работаю") {
|
|
1343
|
+
if (!output.isTTY || process.env.NO_COLOR === "1") {
|
|
1344
|
+
output.write(`${label}...\n`);
|
|
1345
|
+
return () => {};
|
|
1346
|
+
}
|
|
1347
|
+
const frames = ["|", "/", "-", "\\"];
|
|
1348
|
+
const started = Date.now();
|
|
1349
|
+
let index = 0;
|
|
1350
|
+
const render = () => {
|
|
1351
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1);
|
|
1352
|
+
output.write(`\r\x1b[2K${colorMuted(`${frames[index % frames.length]} ${label} ${seconds}s`)}`);
|
|
1353
|
+
index += 1;
|
|
1354
|
+
};
|
|
1355
|
+
render();
|
|
1356
|
+
const timer = setInterval(render, 120);
|
|
1357
|
+
return () => {
|
|
1358
|
+
clearInterval(timer);
|
|
1359
|
+
output.write("\r\x1b[2K");
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1334
1363
|
function colorSlashSelection(row) {
|
|
1335
1364
|
if (!output.isTTY || process.env.NO_COLOR === "1") return row;
|
|
1336
1365
|
return `\x1b[38;5;213m${row}\x1b[0m`;
|
|
@@ -3440,12 +3469,12 @@ async function handlePermissions(args) {
|
|
|
3440
3469
|
|
|
3441
3470
|
if (action === "allow" || action === "deny") {
|
|
3442
3471
|
if (!name) {
|
|
3443
|
-
throw new Error("Пример: iola permissions deny
|
|
3472
|
+
throw new Error("Пример: iola permissions deny export_report");
|
|
3444
3473
|
}
|
|
3445
3474
|
const allow = action === "allow";
|
|
3446
3475
|
const next = { ...(config.permissions || DEFAULT_AI_CONFIG.permissions) };
|
|
3447
3476
|
next.localTools = { ...(next.localTools || {}) };
|
|
3448
|
-
if (
|
|
3477
|
+
if (ALL_TOOL_ALIASES.includes(name)) {
|
|
3449
3478
|
next.localTools[name] = allow;
|
|
3450
3479
|
} else if (name in DEFAULT_AI_CONFIG.permissions) {
|
|
3451
3480
|
next[name] = allow;
|
|
@@ -6062,9 +6091,10 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6062
6091
|
const prompt = [
|
|
6063
6092
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6064
6093
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
6065
|
-
"Схема: {\"steps\":[{\"tool\":\"
|
|
6066
|
-
|
|
6067
|
-
"
|
|
6094
|
+
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
6095
|
+
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
6096
|
+
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
6097
|
+
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
6068
6098
|
`Вопрос: ${question}`,
|
|
6069
6099
|
].join("\n");
|
|
6070
6100
|
|
|
@@ -6091,19 +6121,19 @@ function inferToolPlan(question, options = {}) {
|
|
|
6091
6121
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
6092
6122
|
const steps = [];
|
|
6093
6123
|
if (normalized.includes("без телефона")) {
|
|
6094
|
-
steps.push({ tool: "
|
|
6124
|
+
steps.push({ tool: "export_report", args: { name: "missing-phones" } });
|
|
6095
6125
|
} else {
|
|
6096
6126
|
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
6097
|
-
steps.push({ tool: "
|
|
6127
|
+
steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
|
|
6098
6128
|
}
|
|
6099
6129
|
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
6100
|
-
steps.push({ tool: "
|
|
6130
|
+
steps.push({ tool: "export_report", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
6101
6131
|
}
|
|
6102
6132
|
if (options.files || normalized.includes("файл") || normalized.includes("папк") || normalized.includes("readme")) {
|
|
6103
6133
|
if (normalized.includes("найди") || normalized.includes("поиск")) {
|
|
6104
|
-
steps.unshift({ tool: "
|
|
6134
|
+
steps.unshift({ tool: "mcp:iola-local:index.search", args: { query: question, limit: 20 } });
|
|
6105
6135
|
} else {
|
|
6106
|
-
steps.unshift({ tool: "
|
|
6136
|
+
steps.unshift({ tool: "file_read", args: { path: "." } });
|
|
6107
6137
|
}
|
|
6108
6138
|
}
|
|
6109
6139
|
return { steps };
|
|
@@ -6124,13 +6154,15 @@ function validateToolPlan(plan, options = {}) {
|
|
|
6124
6154
|
const allowed = new Set(availableToolNames(options));
|
|
6125
6155
|
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
6126
6156
|
for (const step of plan.steps) {
|
|
6127
|
-
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
6157
|
+
if (!allowed.has(step.tool) && !String(step.tool || "").startsWith("mcp:")) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
6128
6158
|
}
|
|
6129
6159
|
return plan;
|
|
6130
6160
|
}
|
|
6131
6161
|
|
|
6132
6162
|
function availableToolNames(options = {}) {
|
|
6133
|
-
|
|
6163
|
+
const names = new Set(LOCAL_TOOLS);
|
|
6164
|
+
for (const tool of getLocalMcpToolNames()) names.add(tool);
|
|
6165
|
+
return [...names];
|
|
6134
6166
|
}
|
|
6135
6167
|
|
|
6136
6168
|
async function executeToolPlan(plan, options = {}) {
|
|
@@ -6143,16 +6175,24 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6143
6175
|
await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
|
|
6144
6176
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
6145
6177
|
try {
|
|
6146
|
-
if (step.tool === "search_local") {
|
|
6178
|
+
if (step.tool === "search_data" || step.tool === "search_local") {
|
|
6147
6179
|
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
6148
6180
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6149
6181
|
} else if (step.tool === "get_card") {
|
|
6150
6182
|
const card = findCard(step.args?.query || "");
|
|
6151
6183
|
current = card ? [card] : [];
|
|
6152
6184
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6153
|
-
} else if (step.tool === "run_report") {
|
|
6185
|
+
} else if (step.tool === "export_report" || step.tool === "run_report") {
|
|
6154
6186
|
current = runQuality(step.args?.name || "all");
|
|
6155
6187
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6188
|
+
if (step.args?.output || step.args?.format) {
|
|
6189
|
+
await assertPermission("writeFiles");
|
|
6190
|
+
const output = step.args?.output || `${step.args?.name || "report"}.${step.args?.format || "csv"}`;
|
|
6191
|
+
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
6192
|
+
await writeFile(output, text, "utf8");
|
|
6193
|
+
saveArtifact("export", output, output, { rows: current.length });
|
|
6194
|
+
outputs.push({ tool: step.tool, output, rows: current.length });
|
|
6195
|
+
}
|
|
6156
6196
|
} else if (step.tool === "save_view") {
|
|
6157
6197
|
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
6158
6198
|
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
@@ -6163,6 +6203,18 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6163
6203
|
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
6164
6204
|
saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
|
|
6165
6205
|
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
6206
|
+
} else if (step.tool === "file_read") {
|
|
6207
|
+
const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
|
|
6208
|
+
current = [{ path: step.args?.path || step.args?.file || ".", text }];
|
|
6209
|
+
outputs.push({ tool: step.tool, bytes: text.length });
|
|
6210
|
+
} else if (step.tool === "browser_open") {
|
|
6211
|
+
const text = await runBrowserAutomation("text", { url: step.args?.url, waitMs: Number(step.args?.waitMs || 0), timeout: Number(step.args?.timeout || 30000), viewport: step.args?.viewport || "1366x768" });
|
|
6212
|
+
current = [{ url: step.args?.url, text }];
|
|
6213
|
+
outputs.push({ tool: step.tool, rows: 1 });
|
|
6214
|
+
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
6215
|
+
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
6216
|
+
current = Array.isArray(result) ? result : [result];
|
|
6217
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6166
6218
|
} else if (step.tool === "files_tree") {
|
|
6167
6219
|
current = await filesTree(step.args?.path || ".", step.args || {});
|
|
6168
6220
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
@@ -6197,6 +6249,78 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6197
6249
|
return { rows: current, outputs };
|
|
6198
6250
|
}
|
|
6199
6251
|
|
|
6252
|
+
function getLocalMcpToolNames() {
|
|
6253
|
+
return mcpTools().map((tool) => `mcp:iola-local:${tool.name}`);
|
|
6254
|
+
}
|
|
6255
|
+
|
|
6256
|
+
async function callConfiguredMcpTool(toolId, args = {}) {
|
|
6257
|
+
const [, serverName, ...toolParts] = String(toolId).split(":");
|
|
6258
|
+
const toolName = toolParts.join(":");
|
|
6259
|
+
if (!serverName || !toolName) throw new Error(`Некорректный MCP tool id: ${toolId}`);
|
|
6260
|
+
const server = getConfiguredMcpServers()[serverName];
|
|
6261
|
+
if (!server) throw new Error(`MCP server не настроен: ${serverName}`);
|
|
6262
|
+
return callStdioMcpTool(server, toolName, args);
|
|
6263
|
+
}
|
|
6264
|
+
|
|
6265
|
+
function getConfiguredMcpServers() {
|
|
6266
|
+
const userConfig = readConfigLayerSync(CONFIG_FILE);
|
|
6267
|
+
const configured = userConfig?.mcp?.servers && typeof userConfig.mcp.servers === "object" ? userConfig.mcp.servers : {};
|
|
6268
|
+
return {
|
|
6269
|
+
"iola-local": {
|
|
6270
|
+
command: process.execPath,
|
|
6271
|
+
args: [path.resolve(__dirname, "..", "bin", "iola.js"), "mcp", "serve", "--stdio"],
|
|
6272
|
+
},
|
|
6273
|
+
...configured,
|
|
6274
|
+
};
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6277
|
+
async function callStdioMcpTool(server, toolName, args = {}) {
|
|
6278
|
+
const child = spawn(server.command, server.args || [], {
|
|
6279
|
+
cwd: server.cwd || process.cwd(),
|
|
6280
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6281
|
+
windowsHide: true,
|
|
6282
|
+
});
|
|
6283
|
+
let stdout = "";
|
|
6284
|
+
let stderr = "";
|
|
6285
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
6286
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
6287
|
+
const request = { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: toolName, arguments: args } };
|
|
6288
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })}\n`);
|
|
6289
|
+
child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
6290
|
+
child.stdin.end();
|
|
6291
|
+
await waitForProcess(child, 15000);
|
|
6292
|
+
const responses = stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
6293
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
6294
|
+
}).filter(Boolean);
|
|
6295
|
+
const response = responses.find((item) => item.id === 2) || responses.at(-1);
|
|
6296
|
+
if (!response) throw new Error(`MCP server ${server.command} не вернул ответ. ${stderr}`.trim());
|
|
6297
|
+
if (response.error) throw new Error(response.error.message || JSON.stringify(response.error));
|
|
6298
|
+
const content = response.result?.content || [];
|
|
6299
|
+
const text = content.map((item) => item.text || "").join("\n").trim();
|
|
6300
|
+
try {
|
|
6301
|
+
return JSON.parse(text);
|
|
6302
|
+
} catch {
|
|
6303
|
+
return text || response.result;
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
function waitForProcess(child, timeoutMs) {
|
|
6308
|
+
return new Promise((resolve, reject) => {
|
|
6309
|
+
const timer = setTimeout(() => {
|
|
6310
|
+
child.kill();
|
|
6311
|
+
reject(new Error("MCP call timeout"));
|
|
6312
|
+
}, timeoutMs);
|
|
6313
|
+
child.once("error", (error) => {
|
|
6314
|
+
clearTimeout(timer);
|
|
6315
|
+
reject(error);
|
|
6316
|
+
});
|
|
6317
|
+
child.once("close", () => {
|
|
6318
|
+
clearTimeout(timer);
|
|
6319
|
+
resolve();
|
|
6320
|
+
});
|
|
6321
|
+
});
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6200
6324
|
function formatToolResult(result, options) {
|
|
6201
6325
|
if (options.schema === "json") return JSON.stringify(result, null, 2);
|
|
6202
6326
|
const exported = result.outputs.find((item) => item.output);
|
|
@@ -6221,7 +6345,7 @@ async function runHooks(event, payload = {}) {
|
|
|
6221
6345
|
const commands = config.hooks?.[event] || [];
|
|
6222
6346
|
for (const command of commands) {
|
|
6223
6347
|
const [maybeFilter, ...rest] = String(command).split(":");
|
|
6224
|
-
const commandText = payload.tool && rest.length > 0 &&
|
|
6348
|
+
const commandText = payload.tool && rest.length > 0 && ALL_TOOL_ALIASES.includes(maybeFilter.trim())
|
|
6225
6349
|
? (maybeFilter.trim() === payload.tool ? rest.join(":").trim() : "")
|
|
6226
6350
|
: command;
|
|
6227
6351
|
if (!commandText) continue;
|
|
@@ -6240,7 +6364,7 @@ async function runHooks(event, payload = {}) {
|
|
|
6240
6364
|
async function assertPermission(name) {
|
|
6241
6365
|
const config = await loadConfig();
|
|
6242
6366
|
const permissions = applyToolsetPermissions(config.permissions || DEFAULT_AI_CONFIG.permissions, config.toolsets?.enabled || []);
|
|
6243
|
-
if (
|
|
6367
|
+
if (ALL_TOOL_ALIASES.includes(name)) {
|
|
6244
6368
|
if (permissions.localTools?.[name] === false) {
|
|
6245
6369
|
throw new Error(`Tool запрещен политикой permissions: ${name}`);
|
|
6246
6370
|
}
|
|
@@ -6425,7 +6549,7 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
6425
6549
|
const sourceLines = buildSourceLines(dataContext);
|
|
6426
6550
|
const memoryText = options.bare ? "" : buildMemoryText();
|
|
6427
6551
|
const projectContext = options.bare ? "" : await buildProjectContextText();
|
|
6428
|
-
const skillsText = options.bare ? "" : await buildSkillsText(config);
|
|
6552
|
+
const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
|
|
6429
6553
|
const hasDataContext = dataContext.enabled !== false;
|
|
6430
6554
|
const system = [
|
|
6431
6555
|
"Ты терминальный AI-агент городского округа Йошкар-Ола.",
|
|
@@ -7113,16 +7237,29 @@ function isSkillEnabled(config, name) {
|
|
|
7113
7237
|
return (config.skills?.enabled || []).includes(name);
|
|
7114
7238
|
}
|
|
7115
7239
|
|
|
7116
|
-
async function buildSkillsText(config) {
|
|
7240
|
+
async function buildSkillsText(config, question = "", options = {}) {
|
|
7117
7241
|
const chunks = [];
|
|
7242
|
+
const selected = selectSkillsForPrompt(config, question, options);
|
|
7118
7243
|
for (const skill of listSkills(config)) {
|
|
7119
|
-
if (!skill.enabled) continue;
|
|
7244
|
+
if (!skill.enabled || !selected.has(skill.name)) continue;
|
|
7120
7245
|
const text = await readFile(skill.file, "utf8");
|
|
7121
7246
|
chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
|
|
7122
7247
|
}
|
|
7123
7248
|
return chunks.join("\n\n").slice(0, 12000);
|
|
7124
7249
|
}
|
|
7125
7250
|
|
|
7251
|
+
function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
7252
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
7253
|
+
const selected = new Set();
|
|
7254
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
7255
|
+
if (enabled.has("local-model")) selected.add("local-model");
|
|
7256
|
+
if (enabled.has("open-data") && shouldUseDataContext(question, options)) selected.add("open-data");
|
|
7257
|
+
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
7258
|
+
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
7259
|
+
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
7260
|
+
return selected;
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7126
7263
|
function stripFrontmatter(text) {
|
|
7127
7264
|
return String(text).replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
7128
7265
|
}
|
|
@@ -8641,6 +8778,14 @@ async function readConfigLayer(file) {
|
|
|
8641
8778
|
}
|
|
8642
8779
|
}
|
|
8643
8780
|
|
|
8781
|
+
function readConfigLayerSync(file) {
|
|
8782
|
+
try {
|
|
8783
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
8784
|
+
} catch {
|
|
8785
|
+
return null;
|
|
8786
|
+
}
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8644
8789
|
function mergeConfig(base, override) {
|
|
8645
8790
|
return {
|
|
8646
8791
|
...base,
|
|
@@ -8689,6 +8834,14 @@ function mergeConfig(base, override) {
|
|
|
8689
8834
|
...base.daemon,
|
|
8690
8835
|
...(override.daemon || {}),
|
|
8691
8836
|
},
|
|
8837
|
+
mcp: {
|
|
8838
|
+
...base.mcp,
|
|
8839
|
+
...(override.mcp || {}),
|
|
8840
|
+
servers: {
|
|
8841
|
+
...(base.mcp?.servers || {}),
|
|
8842
|
+
...(override.mcp?.servers || {}),
|
|
8843
|
+
},
|
|
8844
|
+
},
|
|
8692
8845
|
cron: {
|
|
8693
8846
|
...base.cron,
|
|
8694
8847
|
...(override.cron || {}),
|
|
@@ -8722,7 +8875,7 @@ function validateConfig(config) {
|
|
|
8722
8875
|
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
8723
8876
|
}
|
|
8724
8877
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
8725
|
-
if (!
|
|
8878
|
+
if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
8726
8879
|
}
|
|
8727
8880
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
8728
8881
|
if (!TOOLSETS[toolset]) errors.push(`toolsets.enabled содержит неизвестный toolset: ${toolset}`);
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
`iola-cli` использует skills как подключаемые инструкции для работы с данными городского округа "Город Йошкар-Ола".
|
|
4
4
|
|
|
5
|
+
Skills не подмешиваются в каждый запрос целиком. CLI выбирает их по смыслу:
|
|
6
|
+
|
|
7
|
+
- `open-data` - когда запрос про открытые данные, школы, детские сады, адреса, ИНН, слои;
|
|
8
|
+
- `reports` - когда нужен отчет, выгрузка, CSV/XLSX или проверка качества;
|
|
9
|
+
- `local-files` - когда пользователь просит работать с локальными файлами, папками, архивами или документами;
|
|
10
|
+
- `browser-agent` - когда запрос связан с сайтом, URL, страницей, скриншотом или браузером;
|
|
11
|
+
- `local-model` - инструкции для локальных компактных моделей и tool-планирования.
|
|
12
|
+
|
|
13
|
+
Обычный диалог вроде `привет` не получает инструкции про слои, отчеты, файлы и браузер.
|
|
14
|
+
|
|
5
15
|
```bash
|
|
6
16
|
iola skills list
|
|
7
17
|
iola skills show open-data
|
|
@@ -28,4 +38,3 @@ iola tools profile full
|
|
|
28
38
|
|
|
29
39
|
Режим `safe` подходит для чтения и анализа без записи файлов и без запуска sync.
|
|
30
40
|
Режим `full` предназначен для доверенного локального пользователя.
|
|
31
|
-
|
|
@@ -9,13 +9,43 @@ iola ask "выгрузи школы на Петрова в csv" --profile local
|
|
|
9
9
|
iola ask "найди детсады без телефона" --profile local --tools --reasoning verify
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Минимальные встроенные tools:
|
|
13
13
|
|
|
14
|
-
- `
|
|
14
|
+
- `search_data` - поиск по локальной SQLite-БД и открытым данным.
|
|
15
15
|
- `get_card` - карточка объекта.
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
16
|
+
- `export_report` - отчет или выгрузка результата в CSV/JSON.
|
|
17
|
+
- `file_read` - чтение локального файла в разрешенном workspace.
|
|
18
|
+
- `browser_open` - открыть страницу через browser runtime и вернуть текст.
|
|
19
|
+
|
|
20
|
+
Старые имена `search_local`, `run_report`, `export_data` сохранены как алиасы для совместимости, но новый планировщик их не предлагает.
|
|
21
|
+
|
|
22
|
+
MCP tools:
|
|
23
|
+
|
|
24
|
+
Локальная модель может вызывать подключенные MCP tools через CLI-мост. Встроенный локальный MCP-сервер доступен как `iola-local`, а tool указывается в виде:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
mcp:iola-local:search
|
|
28
|
+
mcp:iola-local:card
|
|
29
|
+
mcp:iola-local:report
|
|
30
|
+
mcp:iola-local:browser.text
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcp": {
|
|
38
|
+
"servers": {
|
|
39
|
+
"example": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "some-mcp-server"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
После этого локальный plan может вызвать tool как `mcp:example:tool_name`.
|
|
19
49
|
|
|
20
50
|
Toolsets:
|
|
21
51
|
|
|
@@ -31,8 +61,8 @@ iola tools profile full
|
|
|
31
61
|
|
|
32
62
|
```bash
|
|
33
63
|
iola permissions list
|
|
34
|
-
iola permissions deny
|
|
35
|
-
iola permissions allow
|
|
64
|
+
iola permissions deny export_report
|
|
65
|
+
iola permissions allow export_report
|
|
36
66
|
```
|
|
37
67
|
|
|
38
68
|
Режимы планирования:
|