@iola_adm/iola-cli 0.1.50 → 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 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.51",
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
  },
@@ -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 export_data");
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 (ALL_LOCAL_TOOLS.includes(name)) {
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;
@@ -6062,9 +6067,10 @@ async function buildLocalToolPlan(question, providerConfig, options) {
6062
6067
  const prompt = [
6063
6068
  "Ты планировщик CLI iola. Верни только JSON.",
6064
6069
  `Доступные 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, если пользователь назвал файл.",
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, если пользователь назвал файл.",
6068
6074
  `Вопрос: ${question}`,
6069
6075
  ].join("\n");
6070
6076
 
@@ -6091,19 +6097,19 @@ function inferToolPlan(question, options = {}) {
6091
6097
  const dataset = normalized.includes("сад") ? "kindergartens" : normalized.includes("школ") || normalized.includes("лицей") ? "schools" : "all";
6092
6098
  const steps = [];
6093
6099
  if (normalized.includes("без телефона")) {
6094
- steps.push({ tool: "run_report", args: { name: "missing-phones" } });
6100
+ steps.push({ tool: "export_report", args: { name: "missing-phones" } });
6095
6101
  } else {
6096
6102
  const query = normalized.match(/петрова|школ[а-яё ]*\d+|сад[а-яё ]*\d+|лицей[а-яё ]*\d+/iu)?.[0] || question;
6097
- steps.push({ tool: "search_local", args: { dataset, query, limit: 20 } });
6103
+ steps.push({ tool: "search_data", args: { dataset, query, limit: 20 } });
6098
6104
  }
6099
6105
  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" } });
6106
+ steps.push({ tool: "export_report", args: { format: "csv", output: normalized.match(/([a-z0-9_-]+\.csv)/i)?.[1] || "iola-export.csv" } });
6101
6107
  }
6102
6108
  if (options.files || normalized.includes("файл") || normalized.includes("папк") || normalized.includes("readme")) {
6103
6109
  if (normalized.includes("найди") || normalized.includes("поиск")) {
6104
- steps.unshift({ tool: "files_search", args: { query: question, path: "." } });
6110
+ steps.unshift({ tool: "mcp:iola-local:index.search", args: { query: question, limit: 20 } });
6105
6111
  } else {
6106
- steps.unshift({ tool: "files_tree", args: { path: ".", depth: 2, limit: 80 } });
6112
+ steps.unshift({ tool: "file_read", args: { path: "." } });
6107
6113
  }
6108
6114
  }
6109
6115
  return { steps };
@@ -6124,13 +6130,15 @@ function validateToolPlan(plan, options = {}) {
6124
6130
  const allowed = new Set(availableToolNames(options));
6125
6131
  if (!plan || !Array.isArray(plan.steps)) throw new Error("Некорректный tool-plan.");
6126
6132
  for (const step of plan.steps) {
6127
- 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}`);
6128
6134
  }
6129
6135
  return plan;
6130
6136
  }
6131
6137
 
6132
6138
  function availableToolNames(options = {}) {
6133
- return options.files ? ALL_LOCAL_TOOLS : LOCAL_TOOLS;
6139
+ const names = new Set(LOCAL_TOOLS);
6140
+ for (const tool of getLocalMcpToolNames()) names.add(tool);
6141
+ return [...names];
6134
6142
  }
6135
6143
 
6136
6144
  async function executeToolPlan(plan, options = {}) {
@@ -6143,16 +6151,24 @@ async function executeToolPlan(plan, options = {}) {
6143
6151
  await runHooks("PreToolUse", { tool: step.tool, args: step.args || {} });
6144
6152
  await runHooks("BeforeTool", { tool: step.tool, args: step.args || {} });
6145
6153
  try {
6146
- if (step.tool === "search_local") {
6154
+ if (step.tool === "search_data" || step.tool === "search_local") {
6147
6155
  current = searchLocalRecords(step.args?.query || "", { dataset: step.args?.dataset || "all", limit: step.args?.limit || 20, fts: true });
6148
6156
  outputs.push({ tool: step.tool, rows: current.length });
6149
6157
  } else if (step.tool === "get_card") {
6150
6158
  const card = findCard(step.args?.query || "");
6151
6159
  current = card ? [card] : [];
6152
6160
  outputs.push({ tool: step.tool, rows: current.length });
6153
- } else if (step.tool === "run_report") {
6161
+ } else if (step.tool === "export_report" || step.tool === "run_report") {
6154
6162
  current = runQuality(step.args?.name || "all");
6155
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
+ }
6156
6172
  } else if (step.tool === "save_view") {
6157
6173
  saveView(step.args?.name, step.args?.dataset || "all", step.args?.args || []);
6158
6174
  outputs.push({ tool: step.tool, saved: step.args?.name });
@@ -6163,6 +6179,18 @@ async function executeToolPlan(plan, options = {}) {
6163
6179
  await writeFile(step.args?.output || "iola-export.csv", text, "utf8");
6164
6180
  saveArtifact("export", step.args?.output || "iola-export.csv", step.args?.output || "iola-export.csv", { rows: current.length });
6165
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 });
6166
6194
  } else if (step.tool === "files_tree") {
6167
6195
  current = await filesTree(step.args?.path || ".", step.args || {});
6168
6196
  outputs.push({ tool: step.tool, rows: current.length });
@@ -6197,6 +6225,78 @@ async function executeToolPlan(plan, options = {}) {
6197
6225
  return { rows: current, outputs };
6198
6226
  }
6199
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
+
6200
6300
  function formatToolResult(result, options) {
6201
6301
  if (options.schema === "json") return JSON.stringify(result, null, 2);
6202
6302
  const exported = result.outputs.find((item) => item.output);
@@ -6221,7 +6321,7 @@ async function runHooks(event, payload = {}) {
6221
6321
  const commands = config.hooks?.[event] || [];
6222
6322
  for (const command of commands) {
6223
6323
  const [maybeFilter, ...rest] = String(command).split(":");
6224
- const commandText = payload.tool && rest.length > 0 && ALL_LOCAL_TOOLS.includes(maybeFilter.trim())
6324
+ const commandText = payload.tool && rest.length > 0 && ALL_TOOL_ALIASES.includes(maybeFilter.trim())
6225
6325
  ? (maybeFilter.trim() === payload.tool ? rest.join(":").trim() : "")
6226
6326
  : command;
6227
6327
  if (!commandText) continue;
@@ -6240,7 +6340,7 @@ async function runHooks(event, payload = {}) {
6240
6340
  async function assertPermission(name) {
6241
6341
  const config = await loadConfig();
6242
6342
  const permissions = applyToolsetPermissions(config.permissions || DEFAULT_AI_CONFIG.permissions, config.toolsets?.enabled || []);
6243
- if (ALL_LOCAL_TOOLS.includes(name)) {
6343
+ if (ALL_TOOL_ALIASES.includes(name)) {
6244
6344
  if (permissions.localTools?.[name] === false) {
6245
6345
  throw new Error(`Tool запрещен политикой permissions: ${name}`);
6246
6346
  }
@@ -6425,7 +6525,7 @@ async function buildAiMessages(question, dataContext, history, options = {}, con
6425
6525
  const sourceLines = buildSourceLines(dataContext);
6426
6526
  const memoryText = options.bare ? "" : buildMemoryText();
6427
6527
  const projectContext = options.bare ? "" : await buildProjectContextText();
6428
- const skillsText = options.bare ? "" : await buildSkillsText(config);
6528
+ const skillsText = options.bare ? "" : await buildSkillsText(config, question, options);
6429
6529
  const hasDataContext = dataContext.enabled !== false;
6430
6530
  const system = [
6431
6531
  "Ты терминальный AI-агент городского округа Йошкар-Ола.",
@@ -7113,16 +7213,29 @@ function isSkillEnabled(config, name) {
7113
7213
  return (config.skills?.enabled || []).includes(name);
7114
7214
  }
7115
7215
 
7116
- async function buildSkillsText(config) {
7216
+ async function buildSkillsText(config, question = "", options = {}) {
7117
7217
  const chunks = [];
7218
+ const selected = selectSkillsForPrompt(config, question, options);
7118
7219
  for (const skill of listSkills(config)) {
7119
- if (!skill.enabled) continue;
7220
+ if (!skill.enabled || !selected.has(skill.name)) continue;
7120
7221
  const text = await readFile(skill.file, "utf8");
7121
7222
  chunks.push(`## Skill: ${skill.name}\n${stripFrontmatter(text).trim()}`);
7122
7223
  }
7123
7224
  return chunks.join("\n\n").slice(0, 12000);
7124
7225
  }
7125
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
+
7126
7239
  function stripFrontmatter(text) {
7127
7240
  return String(text).replace(/^---\n[\s\S]*?\n---\n?/, "");
7128
7241
  }
@@ -8641,6 +8754,14 @@ async function readConfigLayer(file) {
8641
8754
  }
8642
8755
  }
8643
8756
 
8757
+ function readConfigLayerSync(file) {
8758
+ try {
8759
+ return JSON.parse(readFileSync(file, "utf8"));
8760
+ } catch {
8761
+ return null;
8762
+ }
8763
+ }
8764
+
8644
8765
  function mergeConfig(base, override) {
8645
8766
  return {
8646
8767
  ...base,
@@ -8689,6 +8810,14 @@ function mergeConfig(base, override) {
8689
8810
  ...base.daemon,
8690
8811
  ...(override.daemon || {}),
8691
8812
  },
8813
+ mcp: {
8814
+ ...base.mcp,
8815
+ ...(override.mcp || {}),
8816
+ servers: {
8817
+ ...(base.mcp?.servers || {}),
8818
+ ...(override.mcp?.servers || {}),
8819
+ },
8820
+ },
8692
8821
  cron: {
8693
8822
  ...base.cron,
8694
8823
  ...(override.cron || {}),
@@ -8722,7 +8851,7 @@ function validateConfig(config) {
8722
8851
  if (profile.provider !== "codex" && !profile.baseUrl) errors.push(`ai.profiles.${name}.baseUrl обязателен`);
8723
8852
  }
8724
8853
  for (const tool of Object.keys(config.permissions?.localTools || {})) {
8725
- if (!ALL_LOCAL_TOOLS.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
8854
+ if (!ALL_TOOL_ALIASES.includes(tool)) errors.push(`permissions.localTools.${tool} неизвестен`);
8726
8855
  }
8727
8856
  for (const toolset of config.toolsets?.enabled || []) {
8728
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
- Доступные 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
  Режимы планирования: