@iola_adm/iola-cli 0.1.49 → 0.1.51
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 +194 -40
- 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
|
},
|
|
@@ -3440,12 +3445,12 @@ async function handlePermissions(args) {
|
|
|
3440
3445
|
|
|
3441
3446
|
if (action === "allow" || action === "deny") {
|
|
3442
3447
|
if (!name) {
|
|
3443
|
-
throw new Error("Пример: iola permissions deny
|
|
3448
|
+
throw new Error("Пример: iola permissions deny export_report");
|
|
3444
3449
|
}
|
|
3445
3450
|
const allow = action === "allow";
|
|
3446
3451
|
const next = { ...(config.permissions || DEFAULT_AI_CONFIG.permissions) };
|
|
3447
3452
|
next.localTools = { ...(next.localTools || {}) };
|
|
3448
|
-
if (
|
|
3453
|
+
if (ALL_TOOL_ALIASES.includes(name)) {
|
|
3449
3454
|
next.localTools[name] = allow;
|
|
3450
3455
|
} else if (name in DEFAULT_AI_CONFIG.permissions) {
|
|
3451
3456
|
next[name] = allow;
|
|
@@ -5904,7 +5909,8 @@ async function aiAsk(args, context = {}) {
|
|
|
5904
5909
|
return localToolAsk(question, providerConfig, options);
|
|
5905
5910
|
}
|
|
5906
5911
|
applyRuntimeConfig(providerConfig, options.config);
|
|
5907
|
-
const
|
|
5912
|
+
const useDataContext = !options.bare && shouldUseDataContext(question, options);
|
|
5913
|
+
const dataContext = useDataContext ? await buildDataContext(question) : emptyDataContext(question);
|
|
5908
5914
|
emitEvent(options, "context_loaded", { schools: dataContext.schools.length, kindergartens: dataContext.kindergartens.length });
|
|
5909
5915
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
5910
5916
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
@@ -6061,9 +6067,10 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
6061
6067
|
const prompt = [
|
|
6062
6068
|
"Ты планировщик CLI iola. Верни только JSON.",
|
|
6063
6069
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
6064
|
-
"Схема: {\"steps\":[{\"tool\":\"
|
|
6065
|
-
|
|
6066
|
-
"
|
|
6070
|
+
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
6071
|
+
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
6072
|
+
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
6073
|
+
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
6067
6074
|
`Вопрос: ${question}`,
|
|
6068
6075
|
].join("\n");
|
|
6069
6076
|
|
|
@@ -6090,19 +6097,19 @@ function inferToolPlan(question, options = {}) {
|
|
|
6090
6097
|
const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
|
|
6091
6098
|
const steps = [];
|
|
6092
6099
|
if (normalized.includes("без телефона")) {
|
|
6093
|
-
steps.push({ tool: "
|
|
6100
|
+
steps.push({ tool: "export_report", args: { name: "missing-phones" } });
|
|
6094
6101
|
} else {
|
|
6095
6102
|
const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
|
|
6096
|
-
steps.push({ tool: "
|
|
6103
|
+
steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
|
|
6097
6104
|
}
|
|
6098
6105
|
if (normalized.includes("csv") || normalized.includes("выгруз")) {
|
|
6099
|
-
steps.push({ tool: "
|
|
6106
|
+
steps.push({ tool: "export_report", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
|
|
6100
6107
|
}
|
|
6101
6108
|
if (options.files || normalized.includes("файл") || normalized.includes("папк") || normalized.includes("readme")) {
|
|
6102
6109
|
if (normalized.includes("найди") || normalized.includes("поиск")) {
|
|
6103
|
-
steps.unshift({ tool: "
|
|
6110
|
+
steps.unshift({ tool: "mcp:iola-local:index.search", args: { query: question, limit: 20 } });
|
|
6104
6111
|
} else {
|
|
6105
|
-
steps.unshift({ tool: "
|
|
6112
|
+
steps.unshift({ tool: "file_read", args: { path: "." } });
|
|
6106
6113
|
}
|
|
6107
6114
|
}
|
|
6108
6115
|
return { steps };
|
|
@@ -6123,13 +6130,15 @@ function validateToolPlan(plan, options = {}) {
|
|
|
6123
6130
|
const allowed = new Set(availableToolNames(options));
|
|
6124
6131
|
if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
|
|
6125
6132
|
for (const step of plan.steps) {
|
|
6126
|
-
if (!allowed.has(step.tool)) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
6133
|
+
if (!allowed.has(step.tool) && !String(step.tool || "").startsWith("mcp:")) throw new Error(`Недопустимый tool: ${step.tool}`);
|
|
6127
6134
|
}
|
|
6128
6135
|
return plan;
|
|
6129
6136
|
}
|
|
6130
6137
|
|
|
6131
6138
|
function availableToolNames(options = {}) {
|
|
6132
|
-
|
|
6139
|
+
const names = new Set(LOCAL_TOOLS);
|
|
6140
|
+
for (const tool of getLocalMcpToolNames()) names.add(tool);
|
|
6141
|
+
return [...names];
|
|
6133
6142
|
}
|
|
6134
6143
|
|
|
6135
6144
|
async function executeToolPlan(plan, options = {}) {
|
|
@@ -6142,16 +6151,24 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6142
6151
|
await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
|
|
6143
6152
|
await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
|
|
6144
6153
|
try {
|
|
6145
|
-
if (step.tool === "search_local") {
|
|
6154
|
+
if (step.tool === "search_data" || step.tool === "search_local") {
|
|
6146
6155
|
current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
|
|
6147
6156
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6148
6157
|
} else if (step.tool === "get_card") {
|
|
6149
6158
|
const card = findCard(step.args?.query || "");
|
|
6150
6159
|
current = card ? [card] : [];
|
|
6151
6160
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6152
|
-
} else if (step.tool === "run_report") {
|
|
6161
|
+
} else if (step.tool === "export_report" || step.tool === "run_report") {
|
|
6153
6162
|
current = runQuality(step.args?.name || "all");
|
|
6154
6163
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
6164
|
+
if (step.args?.output || step.args?.format) {
|
|
6165
|
+
await assertPermission("writeFiles");
|
|
6166
|
+
const output = step.args?.output || `${step.args?.name || "report"}.${step.args?.format || "csv"}`;
|
|
6167
|
+
const text = step.args?.format === "json" ? JSON.stringify(current, null, 2) : toCsv(current);
|
|
6168
|
+
await writeFile(output, text, "utf8");
|
|
6169
|
+
saveArtifact("export", output, output, { rows: current.length });
|
|
6170
|
+
outputs.push({ tool: step.tool, output, rows: current.length });
|
|
6171
|
+
}
|
|
6155
6172
|
} else if (step.tool === "save_view") {
|
|
6156
6173
|
saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
|
|
6157
6174
|
outputs.push({ tool: step.tool, saved: step.args?.name });
|
|
@@ -6162,6 +6179,18 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6162
6179
|
await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
|
|
6163
6180
|
saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
|
|
6164
6181
|
outputs.push({ tool: step.tool, output: step.args?.output || "iola-export.csv", rows: current.length });
|
|
6182
|
+
} else if (step.tool === "file_read") {
|
|
6183
|
+
const text = await filesRead(step.args?.path || step.args?.file || ".", step.args || {});
|
|
6184
|
+
current = [{ path: step.args?.path || step.args?.file || ".", text }];
|
|
6185
|
+
outputs.push({ tool: step.tool, bytes: text.length });
|
|
6186
|
+
} else if (step.tool === "browser_open") {
|
|
6187
|
+
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" });
|
|
6188
|
+
current = [{ url: step.args?.url, text }];
|
|
6189
|
+
outputs.push({ tool: step.tool, rows: 1 });
|
|
6190
|
+
} else if (String(step.tool || "").startsWith("mcp:")) {
|
|
6191
|
+
const result = await callConfiguredMcpTool(step.tool, step.args || {});
|
|
6192
|
+
current = Array.isArray(result) ? result : [result];
|
|
6193
|
+
outputs.push({ tool: step.tool, rows: current.length });
|
|
6165
6194
|
} else if (step.tool === "files_tree") {
|
|
6166
6195
|
current = await filesTree(step.args?.path || ".", step.args || {});
|
|
6167
6196
|
outputs.push({ tool: step.tool, rows: current.length });
|
|
@@ -6196,6 +6225,78 @@ async function executeToolPlan(plan, options = {}) {
|
|
|
6196
6225
|
return { rows: current, outputs };
|
|
6197
6226
|
}
|
|
6198
6227
|
|
|
6228
|
+
function getLocalMcpToolNames() {
|
|
6229
|
+
return mcpTools().map((tool) => `mcp:iola-local:${tool.name}`);
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
async function callConfiguredMcpTool(toolId, args = {}) {
|
|
6233
|
+
const [, serverName, ...toolParts] = String(toolId).split(":");
|
|
6234
|
+
const toolName = toolParts.join(":");
|
|
6235
|
+
if (!serverName || !toolName) throw new Error(`Некорректный MCP tool id: ${toolId}`);
|
|
6236
|
+
const server = getConfiguredMcpServers()[serverName];
|
|
6237
|
+
if (!server) throw new Error(`MCP server не настроен: ${serverName}`);
|
|
6238
|
+
return callStdioMcpTool(server, toolName, args);
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
function getConfiguredMcpServers() {
|
|
6242
|
+
const userConfig = readConfigLayerSync(CONFIG_FILE);
|
|
6243
|
+
const configured = userConfig?.mcp?.servers && typeof userConfig.mcp.servers === "object" ? userConfig.mcp.servers : {};
|
|
6244
|
+
return {
|
|
6245
|
+
"iola-local": {
|
|
6246
|
+
command: process.execPath,
|
|
6247
|
+
args: [path.resolve(__dirname, "..", "bin", "iola.js"), "mcp", "serve", "--stdio"],
|
|
6248
|
+
},
|
|
6249
|
+
...configured,
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
async function callStdioMcpTool(server, toolName, args = {}) {
|
|
6254
|
+
const child = spawn(server.command, server.args || [], {
|
|
6255
|
+
cwd: server.cwd || process.cwd(),
|
|
6256
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6257
|
+
windowsHide: true,
|
|
6258
|
+
});
|
|
6259
|
+
let stdout = "";
|
|
6260
|
+
let stderr = "";
|
|
6261
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
6262
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
6263
|
+
const request = { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: toolName, arguments: args } };
|
|
6264
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })}\n`);
|
|
6265
|
+
child.stdin.write(`${JSON.stringify(request)}\n`);
|
|
6266
|
+
child.stdin.end();
|
|
6267
|
+
await waitForProcess(child, 15000);
|
|
6268
|
+
const responses = stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
6269
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
6270
|
+
}).filter(Boolean);
|
|
6271
|
+
const response = responses.find((item) => item.id === 2) || responses.at(-1);
|
|
6272
|
+
if (!response) throw new Error(`MCP server ${server.command} не вернул ответ. ${stderr}`.trim());
|
|
6273
|
+
if (response.error) throw new Error(response.error.message || JSON.stringify(response.error));
|
|
6274
|
+
const content = response.result?.content || [];
|
|
6275
|
+
const text = content.map((item) => item.text || "").join("\n").trim();
|
|
6276
|
+
try {
|
|
6277
|
+
return JSON.parse(text);
|
|
6278
|
+
} catch {
|
|
6279
|
+
return text || response.result;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
|
|
6283
|
+
function waitForProcess(child, timeoutMs) {
|
|
6284
|
+
return new Promise((resolve, reject) => {
|
|
6285
|
+
const timer = setTimeout(() => {
|
|
6286
|
+
child.kill();
|
|
6287
|
+
reject(new Error("MCP call timeout"));
|
|
6288
|
+
}, timeoutMs);
|
|
6289
|
+
child.once("error", (error) => {
|
|
6290
|
+
clearTimeout(timer);
|
|
6291
|
+
reject(error);
|
|
6292
|
+
});
|
|
6293
|
+
child.once("close", () => {
|
|
6294
|
+
clearTimeout(timer);
|
|
6295
|
+
resolve();
|
|
6296
|
+
});
|
|
6297
|
+
});
|
|
6298
|
+
}
|
|
6299
|
+
|
|
6199
6300
|
function formatToolResult(result, options) {
|
|
6200
6301
|
if (options.schema === "json") return JSON.stringify(result, null, 2);
|
|
6201
6302
|
const exported = result.outputs.find((item) => item.output);
|
|
@@ -6220,7 +6321,7 @@ async function runHooks(event, payload = {}) {
|
|
|
6220
6321
|
const commands = config.hooks?.[event] || [];
|
|
6221
6322
|
for (const command of commands) {
|
|
6222
6323
|
const [maybeFilter, ...rest] = String(command).split(":");
|
|
6223
|
-
const commandText = payload.tool && rest.length > 0 &&
|
|
6324
|
+
const commandText = payload.tool && rest.length > 0 && ALL_TOOL_ALIASES.includes(maybeFilter.trim())
|
|
6224
6325
|
? (maybeFilter.trim() === payload.tool ? rest.join(":").trim() : "")
|
|
6225
6326
|
: command;
|
|
6226
6327
|
if (!commandText) continue;
|
|
@@ -6239,7 +6340,7 @@ async function runHooks(event, payload = {}) {
|
|
|
6239
6340
|
async function assertPermission(name) {
|
|
6240
6341
|
const config = await loadConfig();
|
|
6241
6342
|
const permissions = applyToolsetPermissions(config.permissions || DEFAULT_AI_CONFIG.permissions, config.toolsets?.enabled || []);
|
|
6242
|
-
if (
|
|
6343
|
+
if (ALL_TOOL_ALIASES.includes(name)) {
|
|
6243
6344
|
if (permissions.localTools?.[name] === false) {
|
|
6244
6345
|
throw new Error(`Tool запрещен политикой permissions: ${name}`);
|
|
6245
6346
|
}
|
|
@@ -6306,6 +6407,29 @@ async function buildDataContext(question) {
|
|
|
6306
6407
|
};
|
|
6307
6408
|
}
|
|
6308
6409
|
|
|
6410
|
+
function emptyDataContext(question) {
|
|
6411
|
+
return {
|
|
6412
|
+
enabled: false,
|
|
6413
|
+
layers: [],
|
|
6414
|
+
query: {
|
|
6415
|
+
text: question,
|
|
6416
|
+
terms: [],
|
|
6417
|
+
patterns: { numbers: [], inns: [], streets: [], targetLayers: [] },
|
|
6418
|
+
},
|
|
6419
|
+
schools: [],
|
|
6420
|
+
kindergartens: [],
|
|
6421
|
+
};
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
function shouldUseDataContext(question, options = {}) {
|
|
6425
|
+
if (options.tools || options.files || options.schema || options.output) return true;
|
|
6426
|
+
const normalized = question.toLocaleLowerCase("ru-RU").trim();
|
|
6427
|
+
if (/^(привет|здравствуй|здравствуйте|добрый день|доброе утро|добрый вечер|hi|hello|hey)[!.?\s]*$/iu.test(normalized)) return false;
|
|
6428
|
+
if (/^(спасибо|благодарю|ок|окей|понял|поняла|ясно|хорошо|да|нет)[!.?\s]*$/iu.test(normalized)) return false;
|
|
6429
|
+
if (normalized.length <= 24 && /^(как дела|что нового|ты тут|ты здесь|кто ты)[?.!\s]*$/iu.test(normalized)) return false;
|
|
6430
|
+
return /\b(школ|сад|детсад|детский сад|лицей|гимнази|инн|адрес|телефон|почт|email|сайт|лиценз|руководител|директор|слой|слои|данн|отчет|отчёт|выгруз|csv|json|найди|покажи|список|карточк|организац|учрежден|йошкар|ола|петрова|строител|советск|первомайск)\b/iu.test(normalized);
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6309
6433
|
function extractSearchTerms(question) {
|
|
6310
6434
|
const normalized = question
|
|
6311
6435
|
.toLocaleLowerCase("ru-RU")
|
|
@@ -6401,14 +6525,15 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
6401
6525
|
const sourceLines = buildSourceLines(dataContext);
|
|
6402
6526
|
const memoryText = options.bare ? "" : buildMemoryText();
|
|
6403
6527
|
const projectContext = options.bare ? "" : await buildProjectContextText();
|
|
6404
|
-
const skillsText = options.bare ? "" : await buildSkillsText(config);
|
|
6528
|
+
const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
|
|
6529
|
+
const hasDataContext = dataContext.enabled !== false;
|
|
6405
6530
|
const system = [
|
|
6406
|
-
"Ты терминальный AI
|
|
6407
|
-
"Отвечай на русском
|
|
6408
|
-
"Используй только данные из переданного
|
|
6409
|
-
"Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно.",
|
|
6410
|
-
"Не выдумывай адреса, телефоны, лицензии и руководителей.",
|
|
6411
|
-
"Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН.",
|
|
6531
|
+
"Ты терминальный AI-агент городского округа Йошкар-Ола.",
|
|
6532
|
+
"Отвечай на русском языке естественно и по смыслу запроса пользователя.",
|
|
6533
|
+
hasDataContext ? "Используй только данные из переданного контекста открытых данных." : "Для обычного диалога отвечай как полноценный AI-ассистент, не перечисляй слои и возможности без запроса пользователя.",
|
|
6534
|
+
hasDataContext ? "Если в контексте нет нужных сведений, прямо напиши, что данных недостаточно." : "",
|
|
6535
|
+
hasDataContext ? "Не выдумывай адреса, телефоны, лицензии и руководителей." : "",
|
|
6536
|
+
hasDataContext ? "Если отвечаешь по конкретным организациям, укажи источник в конце: слой, название и ИНН." : "",
|
|
6412
6537
|
options.schema === "json" ? "Верни валидный JSON без markdown-обертки." : "",
|
|
6413
6538
|
options.schema === "table" ? "Если уместно, верни ответ в виде markdown-таблицы." : "",
|
|
6414
6539
|
memoryText ? `Учитывай пользовательскую память:\n${memoryText}` : "",
|
|
@@ -6418,14 +6543,14 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
|
|
|
6418
6543
|
].filter(Boolean).join(" ");
|
|
6419
6544
|
const contextText = JSON.stringify(dataContext, null, 2);
|
|
6420
6545
|
const recentHistory = history.slice(-6);
|
|
6546
|
+
const userContent = hasDataContext
|
|
6547
|
+
? `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nКраткие источники контекста:\n${sourceLines}\n\nВопрос пользователя: ${question}`
|
|
6548
|
+
: question;
|
|
6421
6549
|
|
|
6422
6550
|
return [
|
|
6423
6551
|
{ role: "system", content: system },
|
|
6424
6552
|
...recentHistory,
|
|
6425
|
-
{
|
|
6426
|
-
role: "user",
|
|
6427
|
-
content: `Контекст открытых данных городского округа "Город Йошкар-Ола":\n${contextText}\n\nКраткие источники контекста:\n${sourceLines}\n\nВопрос пользователя: ${question}`,
|
|
6428
|
-
},
|
|
6553
|
+
{ role: "user", content: userContent },
|
|
6429
6554
|
];
|
|
6430
6555
|
}
|
|
6431
6556
|
|
|
@@ -7088,16 +7213,29 @@ function isSkillEnabled(config, name) {
|
|
|
7088
7213
|
return (config.skills?.enabled || []).includes(name);
|
|
7089
7214
|
}
|
|
7090
7215
|
|
|
7091
|
-
async function buildSkillsText(config) {
|
|
7216
|
+
async function buildSkillsText(config, question = "", options = {}) {
|
|
7092
7217
|
const chunks = [];
|
|
7218
|
+
const selected = selectSkillsForPrompt(config, question, options);
|
|
7093
7219
|
for (const skill of listSkills(config)) {
|
|
7094
|
-
if (!skill.enabled) continue;
|
|
7220
|
+
if (!skill.enabled || !selected.has(skill.name)) continue;
|
|
7095
7221
|
const text = await readFile(skill.file, "utf8");
|
|
7096
7222
|
chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
|
|
7097
7223
|
}
|
|
7098
7224
|
return chunks.join("\n\n").slice(0, 12000);
|
|
7099
7225
|
}
|
|
7100
7226
|
|
|
7227
|
+
function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
7228
|
+
const enabled = new Set(config.skills?.enabled || []);
|
|
7229
|
+
const selected = new Set();
|
|
7230
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
7231
|
+
if (enabled.has("local-model")) selected.add("local-model");
|
|
7232
|
+
if (enabled.has("open-data") && shouldUseDataContext(question, options)) selected.add("open-data");
|
|
7233
|
+
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
7234
|
+
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
7235
|
+
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
7236
|
+
return selected;
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7101
7239
|
function stripFrontmatter(text) {
|
|
7102
7240
|
return String(text).replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
7103
7241
|
}
|
|
@@ -8616,6 +8754,14 @@ async function readConfigLayer(file) {
|
|
|
8616
8754
|
}
|
|
8617
8755
|
}
|
|
8618
8756
|
|
|
8757
|
+
function readConfigLayerSync(file) {
|
|
8758
|
+
try {
|
|
8759
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
8760
|
+
} catch {
|
|
8761
|
+
return null;
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
|
|
8619
8765
|
function mergeConfig(base, override) {
|
|
8620
8766
|
return {
|
|
8621
8767
|
...base,
|
|
@@ -8664,6 +8810,14 @@ function mergeConfig(base, override) {
|
|
|
8664
8810
|
...base.daemon,
|
|
8665
8811
|
...(override.daemon || {}),
|
|
8666
8812
|
},
|
|
8813
|
+
mcp: {
|
|
8814
|
+
...base.mcp,
|
|
8815
|
+
...(override.mcp || {}),
|
|
8816
|
+
servers: {
|
|
8817
|
+
...(base.mcp?.servers || {}),
|
|
8818
|
+
...(override.mcp?.servers || {}),
|
|
8819
|
+
},
|
|
8820
|
+
},
|
|
8667
8821
|
cron: {
|
|
8668
8822
|
...base.cron,
|
|
8669
8823
|
...(override.cron || {}),
|
|
@@ -8697,7 +8851,7 @@ function validateConfig(config) {
|
|
|
8697
8851
|
if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
|
|
8698
8852
|
}
|
|
8699
8853
|
for (const tool of Object.keys(config.permissions?.localTools || {})) {
|
|
8700
|
-
if (!
|
|
8854
|
+
if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
|
|
8701
8855
|
}
|
|
8702
8856
|
for (const toolset of config.toolsets?.enabled || []) {
|
|
8703
8857
|
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
|
Режимы планирования:
|