@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -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 = ["search_local", "get_card", "export_data", "run_report", "save_view"];
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: { search_local: true, get_card: true, run_report: true } },
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: { export_data: true, save_view: true, run_report: true } },
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
- search_local: true,
176
+ search_data: true,
175
177
  get_card: true,
176
- export_data: true,
177
- run_report: true,
178
- save_view: true,
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 export_data");
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 (ALL_LOCAL_TOOLS.includes(name)) {
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\":\"search_local\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
6066
- options.files ? "Файловые tools: files_tree {path,depth,limit}, files_read {path}, files_search {query,path}, files_write {path,text}, files_patch {path,search,replace}." : "",
6067
- "Для выгрузки CSV добавь export_data с format=csv и output, если пользователь назвал файл.",
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: "run_report", args: { name: "missing-phones" } });
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: "search_local", args: { dataset, query, limit: 20 } });
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: "export_data", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
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: "files_search", args: { query: question, path: "." } });
6134
+ steps.unshift({ tool: "mcp:iola-local:index.search", args: { query: question, limit: 20 } });
6105
6135
  } else {
6106
- steps.unshift({ tool: "files_tree", args: { path: ".", depth: 2, limit: 80 } });
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
- return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
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 && ALL_LOCAL_TOOLS.includes(maybeFilter.trim())
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 (ALL_LOCAL_TOOLS.includes(name)) {
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 (!ALL_LOCAL_TOOLS.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
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
- Доступные tools:
12
+ Минимальные встроенные tools:
13
13
 
14
- - `search_local` - поиск по локальной SQLite-БД.
14
+ - `search_data` - поиск по локальной SQLite-БД и открытым данным.
15
15
  - `get_card` - карточка объекта.
16
- - `run_report` - встроенная проверка/отчет.
17
- - `export_data` - выгрузка результата в CSV/JSON.
18
- - `save_view` - сохранение представления.
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 export_data
35
- iola permissions allow export_data
64
+ iola permissions deny export_report
65
+ iola permissions allow export_report
36
66
  ```
37
67
 
38
68
  Режимы планирования: