@iola_adm/iola-cli 0.1.72 → 0.1.74

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
@@ -129,6 +129,9 @@ iola version --check
129
129
  - subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
130
130
  - полноценный локальный MCP server по stdio/http: tools, resources и prompts;
131
131
  - MCP-мост для локальной модели: встроенный `iola-local` доступен как `mcp:iola-local:TOOL`;
132
+ - вопросы по открытым данным сначала идут в публичный remote MCP `https://apiiola.yasg.ru/mcp`
133
+ через `layer_suggest`, `layer_query`, `layer_get` и `layer_answer_context`, а локальная БД/API
134
+ остаются fallback;
132
135
  - дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json` в раздел `mcp.servers`;
133
136
  - браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
134
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.72",
3
+ "version": "0.1.74",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: education
3
+ description: Вопросы по образовательным учреждениям Йошкар-Олы: школы, детские сады, руководители, контакты, адреса и лицензии.
4
+ ---
5
+
6
+ Используй MCP tools слоя данных, а не прямую догадку модели.
7
+
8
+ Основные слои:
9
+
10
+ - `schools` — муниципальные школы, лицеи и гимназии.
11
+ - `kindergartens` — муниципальные детские сады.
12
+
13
+ Для поиска используй:
14
+
15
+ - `mcp:iola-local:layer.list` — список доступных слоев.
16
+ - `mcp:iola-local:layer.schema` — схема слоя.
17
+ - `mcp:iola-local:layer.query` — поиск по слою.
18
+ - `mcp:iola-local:layer.get` — точная карточка записи.
19
+
20
+ Если вопрос можно ответить точным совпадением по руководителю, ИНН, названию или адресу, отвечай только по найденной записи и указывай источник: слой, название и ИНН.
21
+
22
+ Если совпадений нет или их несколько, прямо скажи, что данных недостаточно или нужно уточнение. Не выдумывай школы, детские сады, руководителей, адреса, телефоны, лицензии и сайты.
package/src/cli.js CHANGED
@@ -180,7 +180,7 @@ const DEFAULT_AI_CONFIG = {
180
180
  suggestions: true,
181
181
  },
182
182
  skills: {
183
- enabled: ["open-data", "reports", "local-model", "local-files", "browser-agent"],
183
+ enabled: ["education", "open-data", "reports", "local-model", "local-files", "browser-agent"],
184
184
  },
185
185
  daemon: {
186
186
  host: "127.0.0.1",
@@ -236,11 +236,19 @@ const AGENTS = {
236
236
  const DATASETS = {
237
237
  schools: {
238
238
  title: "Школы",
239
+ category: "Образование",
239
240
  endpoint: "schools",
241
+ aliases: ["школ", "лицей", "гимнази"],
242
+ searchFields: ["name", "address", "head", "inn"],
243
+ personFields: ["head"],
240
244
  },
241
245
  kindergartens: {
242
246
  title: "Детские сады",
247
+ category: "Образование",
243
248
  endpoint: "kindergartens",
249
+ aliases: ["сад", "детсад", "детский сад", "доу", "мбдоу"],
250
+ searchFields: ["name", "address", "head", "inn"],
251
+ personFields: ["head"],
244
252
  },
245
253
  };
246
254
  const SLASH_COMMANDS = [
@@ -6467,34 +6475,52 @@ function emitEvent(options, type, data) {
6467
6475
 
6468
6476
  async function buildDataContext(question) {
6469
6477
  await assertPermission("externalApi");
6470
- const apiBaseUrl = await getApiBaseUrl();
6471
- const mcpBaseUrl = await getMcpBaseUrl();
6472
- const [layers, schools, kindergartens] = await Promise.all([
6473
- fetchJson(`${mcpBaseUrl}/mcp-version`),
6474
- fetchAllApiItems(`${apiBaseUrl}/schools`),
6475
- fetchAllApiItems(`${apiBaseUrl}/kindergartens`),
6476
- ]);
6477
6478
  const queryTerms = extractSearchTerms(question);
6478
6479
  const patterns = extractStructuredPatterns(question);
6479
- const includeSchools = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("schools");
6480
- const includeKindergartens = patterns.targetLayers.length === 0 || patterns.targetLayers.includes("kindergartens");
6481
- const schoolItems = includeSchools
6482
- ? findRelevantItems(normalizeItems(schools), queryTerms, patterns, "schools").slice(0, 8)
6483
- : [];
6484
- const kindergartenItems = includeKindergartens
6485
- ? findRelevantItems(normalizeItems(kindergartens), queryTerms, patterns, "kindergartens").slice(0, 8)
6486
- : [];
6480
+ try {
6481
+ const context = await callPublicMcpTool("layer_answer_context", { question, limit: 8 });
6482
+ const layerMap = Object.fromEntries((context.results || []).map((result) => [result.layer?.id || result.layer, result.items || []]));
6483
+ return {
6484
+ source: "remote-mcp",
6485
+ contract_version: context.contract_version,
6486
+ layers: context.layers || [],
6487
+ facts: context.facts || [],
6488
+ sources: context.sources || [],
6489
+ answer_guidance: context.answer_guidance || "",
6490
+ query: {
6491
+ text: question,
6492
+ terms: queryTerms,
6493
+ patterns,
6494
+ },
6495
+ schools: layerMap.schools || [],
6496
+ kindergartens: layerMap.kindergartens || [],
6497
+ };
6498
+ } catch (error) {
6499
+ const layers = await callMcpTool("layer.list", { category: "Образование" });
6500
+ const targetLayerIds = resolveTargetLayerIds(patterns);
6501
+ const layerResults = await Promise.all(targetLayerIds.map((layer) =>
6502
+ callMcpTool("layer.query", { layer, query: question, terms: queryTerms, patterns, limit: 8 })));
6503
+ const layerMap = Object.fromEntries(layerResults.map((result) => [result.layer, result.items || []]));
6487
6504
 
6488
- return {
6489
- layers: layers.data_layers || [],
6490
- query: {
6491
- text: question,
6492
- terms: queryTerms,
6493
- patterns,
6494
- },
6495
- schools: schoolItems.map(selectPublicSummary),
6496
- kindergartens: kindergartenItems.map(selectPublicSummary),
6497
- };
6505
+ return {
6506
+ source: "local-fallback",
6507
+ fallback_error: error instanceof Error ? error.message : String(error),
6508
+ layers,
6509
+ query: {
6510
+ text: question,
6511
+ terms: queryTerms,
6512
+ patterns,
6513
+ },
6514
+ schools: layerMap.schools || [],
6515
+ kindergartens: layerMap.kindergartens || [],
6516
+ };
6517
+ }
6518
+ }
6519
+
6520
+ function resolveTargetLayerIds(patterns = {}) {
6521
+ const knownLayers = Object.keys(DATASETS);
6522
+ if (patterns.targetLayers?.length) return patterns.targetLayers.filter((layer) => DATASETS[layer]);
6523
+ return knownLayers;
6498
6524
  }
6499
6525
 
6500
6526
  async function fetchAllApiItems(endpoint, limit = 500, maxItems = 5000) {
@@ -6509,6 +6535,36 @@ async function fetchAllApiItems(endpoint, limit = 500, maxItems = 5000) {
6509
6535
  return all;
6510
6536
  }
6511
6537
 
6538
+ async function queryLayer(layer, args = {}) {
6539
+ const meta = DATASETS[layer];
6540
+ if (!meta) throw new Error(`Неизвестный слой: ${layer}`);
6541
+ const endpoint = `${await getApiBaseUrl()}/${meta.endpoint}`;
6542
+ const items = await fetchAllApiItems(endpoint);
6543
+ const terms = args.terms || extractSearchTerms(args.query || "");
6544
+ const patterns = args.patterns || extractStructuredPatterns(args.query || "");
6545
+ const limit = Number(args.limit || 20);
6546
+ return {
6547
+ layer,
6548
+ schema: layerSchema(layer),
6549
+ items: findRelevantItems(normalizeItems(items), terms, patterns, layer).slice(0, limit).map(selectPublicSummary),
6550
+ };
6551
+ }
6552
+
6553
+ function layerSchema(layer) {
6554
+ const meta = DATASETS[layer];
6555
+ if (!meta) throw new Error(`Неизвестный слой: ${layer}`);
6556
+ return {
6557
+ id: layer,
6558
+ title: meta.title,
6559
+ category: meta.category,
6560
+ endpoint: meta.endpoint,
6561
+ aliases: meta.aliases || [],
6562
+ searchFields: meta.searchFields || [],
6563
+ personFields: meta.personFields || [],
6564
+ sourceFields: ["layer", "name", "inn"],
6565
+ };
6566
+ }
6567
+
6512
6568
  function emptyDataContext(question) {
6513
6569
  return {
6514
6570
  enabled: false,
@@ -6844,6 +6900,7 @@ async function showMcpInfo(args) {
6844
6900
  server_name: info.server_name,
6845
6901
  server_version: info.server_version,
6846
6902
  skill_version: info.skill_version,
6903
+ contract_version: info.contract_version || "-",
6847
6904
  npm_package: info.npm_package,
6848
6905
  mcp_endpoint: info.mcp_endpoint,
6849
6906
  layers: info.data_layers.map((layer) => layer.id).join(", "),
@@ -6894,9 +6951,9 @@ async function listDataset(dataset, args) {
6894
6951
 
6895
6952
  const data = options.local
6896
6953
  ? searchLocalRecords(options.search || options._.join(" ") || "", { dataset, limit: Number(options.limit || 20), fts: options.fts })
6897
- : normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
6954
+ : await listDatasetViaRemoteMcp(dataset, options, params);
6898
6955
  const items = data;
6899
- const filtered = applyDatasetFilters(items, options);
6956
+ const filtered = applyDatasetFilters(items, options.local ? options : { ...options, search: "" });
6900
6957
  const limited = filtered.slice(0, Number(options.limit || 20));
6901
6958
  const summarized = limited.map(selectPublicSummary);
6902
6959
  const projected = projectColumns(summarized, options.columns);
@@ -6918,13 +6975,34 @@ async function listDataset(dataset, args) {
6918
6975
  printDatasetTable(projected, options.columns);
6919
6976
  }
6920
6977
 
6978
+ async function listDatasetViaRemoteMcp(dataset, options, params) {
6979
+ try {
6980
+ const limit = Number(options.limit || 20);
6981
+ const offset = Number(options.offset || 0);
6982
+ const query = options.search || options._.join(" ") || "";
6983
+ const result = await callPublicMcpTool("layer_query", {
6984
+ layer: dataset,
6985
+ query,
6986
+ limit: offset + limit,
6987
+ });
6988
+ return normalizeItems(result.items || []).slice(offset, offset + limit);
6989
+ } catch (error) {
6990
+ if (options.debug) {
6991
+ console.error(`remote MCP fallback to API: ${error instanceof Error ? error.message : String(error)}`);
6992
+ }
6993
+ return normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
6994
+ }
6995
+ }
6996
+
6921
6997
  async function getDatasetItem(dataset, options) {
6922
6998
  if (!options.inn) {
6923
6999
  throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
6924
7000
  }
6925
7001
 
6926
- const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
6927
- const item = normalizeItems(data).find((entry) => String(entry.inn) === String(options.inn));
7002
+ const result = options.local
7003
+ ? { found: true, item: searchLocalRecords(options.inn, { dataset, limit: 1, fts: false })[0] }
7004
+ : await callPublicMcpTool("layer_get", { layer: dataset, inn: options.inn });
7005
+ const item = result?.item;
6928
7006
 
6929
7007
  if (!item) {
6930
7008
  throw new Error(`Record was not found in ${dataset}: inn=${options.inn}`);
@@ -6953,13 +7031,13 @@ async function searchAll(args) {
6953
7031
  searchLocalRecords(query, { dataset: "kindergartens", limit, fts: options.fts }),
6954
7032
  ]
6955
7033
  : await Promise.all([
6956
- fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
6957
- fetchJsonMaybeCached(`${await getApiBaseUrl()}/kindergartens?limit=100&offset=0`, options),
7034
+ callPublicMcpTool("layer_query", { layer: "schools", query, limit }),
7035
+ callPublicMcpTool("layer_query", { layer: "kindergartens", query, limit }),
6958
7036
  ]);
6959
7037
 
6960
7038
  const result = {
6961
- schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
6962
- kindergartens: projectColumns(filterItems(normalizeItems(kindergartens), query).slice(0, limit).map(selectPublicSummary), options.columns),
7039
+ schools: projectColumns((options.local ? filterItems(normalizeItems(schools.items || schools), query) : normalizeItems(schools.items || schools)).slice(0, limit).map(selectPublicSummary), options.columns),
7040
+ kindergartens: projectColumns((options.local ? filterItems(normalizeItems(kindergartens.items || kindergartens), query) : normalizeItems(kindergartens.items || kindergartens)).slice(0, limit).map(selectPublicSummary), options.columns),
6963
7041
  };
6964
7042
 
6965
7043
  if (options.json || options.format === "json") {
@@ -7843,6 +7921,12 @@ function mcpTools() {
7843
7921
  const schema = (properties = {}) => ({ type: "object", properties, additionalProperties: false });
7844
7922
  return [
7845
7923
  { name: "status", description: "Статус локальной БД, sync и активного AI-профиля.", inputSchema: schema() },
7924
+ { name: "layer.list", description: "Список слоев данных и их схем.", inputSchema: schema({ category: { type: "string" } }) },
7925
+ { name: "layer.schema", description: "Схема слоя данных.", inputSchema: schema({ layer: { type: "string" } }) },
7926
+ { name: "layer.suggest", description: "Подобрать слой данных по вопросу пользователя через публичный MCP.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
7927
+ { name: "layer.query", description: "Поиск по слою данных через общий retrieval.", inputSchema: schema({ layer: { type: "string" }, query: { type: "string" }, terms: { type: "array" }, patterns: { type: "object" }, limit: { type: "number" } }) },
7928
+ { name: "layer.get", description: "Получить запись слоя по ИНН или названию.", inputSchema: schema({ layer: { type: "string" }, query: { type: "string" }, inn: { type: "string" } }) },
7929
+ { name: "layer.answer_context", description: "RAG-контекст с фактами и источниками через публичный MCP.", inputSchema: schema({ question: { type: "string" }, layer: { type: "string" }, limit: { type: "number" } }) },
7846
7930
  { name: "search", description: "Поиск по локальным открытым данным Йошкар-Олы.", inputSchema: schema({ query: { type: "string" }, dataset: { type: "string" }, limit: { type: "number" } }) },
7847
7931
  { name: "card", description: "Карточка объекта по названию или ИНН.", inputSchema: schema({ query: { type: "string" } }) },
7848
7932
  { name: "quality", description: "Проверки качества данных.", inputSchema: schema({ scope: { type: "string" } }) },
@@ -7860,6 +7944,7 @@ function mcpTools() {
7860
7944
  function mcpResources() {
7861
7945
  return [
7862
7946
  { uri: "iola://status", name: "Статус CLI", mimeType: "application/json" },
7947
+ { uri: "iola://layers", name: "Слои данных", mimeType: "application/json" },
7863
7948
  { uri: "iola://sync", name: "Статус синхронизации", mimeType: "application/json" },
7864
7949
  { uri: "iola://settings", name: "Эффективные настройки", mimeType: "application/json" },
7865
7950
  { uri: "iola://skills", name: "Skills", mimeType: "application/json" },
@@ -7877,6 +7962,41 @@ function mcpPrompts() {
7877
7962
  }
7878
7963
 
7879
7964
  async function callMcpTool(name, args = {}) {
7965
+ if (name === "layer.list") {
7966
+ try {
7967
+ const result = await callPublicMcpTool("layer_list", { category: args.category || undefined });
7968
+ return result.items || result;
7969
+ } catch {
7970
+ return Object.entries(DATASETS)
7971
+ .map(([id, meta]) => layerSchema(id))
7972
+ .filter((layer) => !args.category || layer.category === args.category);
7973
+ }
7974
+ }
7975
+ if (name === "layer.schema") {
7976
+ try {
7977
+ return await callPublicMcpTool("layer_schema", { layer: args.layer });
7978
+ } catch {
7979
+ return layerSchema(args.layer);
7980
+ }
7981
+ }
7982
+ if (name === "layer.suggest") return callPublicMcpTool("layer_suggest", { query: args.query || "", limit: Number(args.limit || 5) });
7983
+ if (name === "layer.query") {
7984
+ try {
7985
+ const result = await callPublicMcpTool("layer_query", { layer: args.layer, query: args.query || "", limit: Number(args.limit || 20) });
7986
+ return { layer: result.layer?.id || args.layer, schema: result.layer, items: result.items || [] };
7987
+ } catch {
7988
+ return queryLayer(args.layer, args);
7989
+ }
7990
+ }
7991
+ if (name === "layer.get") {
7992
+ try {
7993
+ return await callPublicMcpTool("layer_get", { layer: args.layer, query: args.query || "", inn: args.inn || "" });
7994
+ } catch {
7995
+ const result = await queryLayer(args.layer, { query: args.inn || args.query || "", terms: [args.inn || args.query || ""], limit: 1 });
7996
+ return result.items[0] || null;
7997
+ }
7998
+ }
7999
+ if (name === "layer.answer_context") return callPublicMcpTool("layer_answer_context", { question: args.question || "", layer: args.layer || "", limit: Number(args.limit || 5) });
7880
8000
  if (name === "index.search") return searchDocs(args.query || "", Number(args.limit || 20));
7881
8001
  if (name === "report") {
7882
8002
  const output = args.output || `${args.name || "education-contacts"}.${args.format || "xlsx"}`;
@@ -7896,6 +8016,7 @@ async function callMcpTool(name, args = {}) {
7896
8016
 
7897
8017
  async function readMcpResource(uri) {
7898
8018
  if (uri === "iola://status") return JSON.stringify({ db: getDbStatus(), sync: getSyncStatus() }, null, 2);
8019
+ if (uri === "iola://layers") return JSON.stringify(Object.fromEntries(Object.keys(DATASETS).map((id) => [id, layerSchema(id)])), null, 2);
7899
8020
  if (uri === "iola://sync") return JSON.stringify(getSyncStatus(), null, 2);
7900
8021
  if (uri === "iola://settings") return JSON.stringify(await loadConfig(), null, 2);
7901
8022
  if (uri === "iola://skills") return JSON.stringify(listSkills(await loadConfig()), null, 2);
@@ -9044,6 +9165,9 @@ function sanitizeConfig(config) {
9044
9165
  }
9045
9166
  }
9046
9167
  }
9168
+ if (Array.isArray(next.skills?.enabled) && next.skills.enabled.includes("open-data") && !next.skills.enabled.includes("education")) {
9169
+ next.skills.enabled = ["education", ...next.skills.enabled];
9170
+ }
9047
9171
  return next;
9048
9172
  }
9049
9173
 
@@ -9275,6 +9399,54 @@ async function fetchJson(url) {
9275
9399
  return response.json();
9276
9400
  }
9277
9401
 
9402
+ function parseJsonOrSse(text) {
9403
+ const trimmed = String(text || "").trim();
9404
+ if (!trimmed) return null;
9405
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return JSON.parse(trimmed);
9406
+ const dataLines = [];
9407
+ for (const line of trimmed.split(/\r?\n/)) {
9408
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
9409
+ }
9410
+ if (!dataLines.length) throw new Error(`Unexpected MCP response: ${trimmed.slice(0, 300)}`);
9411
+ return JSON.parse(dataLines.join("\n"));
9412
+ }
9413
+
9414
+ async function publicMcpRequest(method, params = undefined) {
9415
+ const baseUrl = await getMcpBaseUrl();
9416
+ const body = { jsonrpc: "2.0", id: 1, method };
9417
+ if (params !== undefined) body.params = params;
9418
+ const response = await fetch(`${baseUrl}/mcp`, {
9419
+ method: "POST",
9420
+ headers: {
9421
+ accept: "application/json, text/event-stream",
9422
+ "content-type": "application/json",
9423
+ },
9424
+ body: JSON.stringify(body),
9425
+ });
9426
+ const text = await response.text();
9427
+ if (!response.ok) {
9428
+ throw new Error(`MCP ${method} failed: ${response.status} ${response.statusText}: ${text.slice(0, 300)}`);
9429
+ }
9430
+ const payload = parseJsonOrSse(text);
9431
+ if (payload?.error) throw new Error(payload.error.message || JSON.stringify(payload.error));
9432
+ return payload?.result;
9433
+ }
9434
+
9435
+ async function callPublicMcpTool(name, args = {}) {
9436
+ const result = await publicMcpRequest("tools/call", { name, arguments: args });
9437
+ if (result?.structuredContent) return result.structuredContent;
9438
+ if (result?.structured_content) return result.structured_content;
9439
+ const text = result?.content?.find?.((item) => item.type === "text")?.text;
9440
+ if (text) {
9441
+ try {
9442
+ return JSON.parse(text);
9443
+ } catch {
9444
+ return text;
9445
+ }
9446
+ }
9447
+ return result;
9448
+ }
9449
+
9278
9450
  function printJson(value) {
9279
9451
  console.log(JSON.stringify(value, null, 2));
9280
9452
  }