@iola_adm/iola-cli 0.1.73 → 0.1.75

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.
Files changed (3) hide show
  1. package/README.md +35 -2
  2. package/package.json +1 -1
  3. package/src/cli.js +148 -30
package/README.md CHANGED
@@ -1,7 +1,37 @@
1
- ![CLI-проект Йошкар-Олы](https://cdn.jsdelivr.net/npm/@iola_adm/iola-cli@latest/docs/assets/readme-header.png)
2
-
3
1
  # iola-cli
4
2
 
3
+ <p align="center">
4
+ <img src="https://cdn.jsdelivr.net/npm/@iola_adm/iola-cli@latest/docs/assets/readme-header.png" alt="CLI-проект Йошкар-Олы" width="100%">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/adm-iola/iola-cli/wiki">Документация</a>
9
+ ·
10
+ <a href="https://github.com/adm-iola/iola-cli/wiki/Установка">Установка</a>
11
+ ·
12
+ <a href="https://github.com/adm-iola/iola-cli/wiki/Первый-запуск">Первый запуск</a>
13
+ ·
14
+ <a href="https://github.com/adm-iola/iola-cli/wiki/AI-профили">AI-профили</a>
15
+ ·
16
+ <a href="https://github.com/adm-iola/iola-cli/wiki/Команды">Команды</a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://www.npmjs.com/package/@iola_adm/iola-cli">
21
+ <img alt="npm version" src="https://img.shields.io/npm/v/@iola_adm/iola-cli?label=npm">
22
+ </a>
23
+ <a href="https://github.com/adm-iola/iola-cli/actions/workflows/ci.yml">
24
+ <img alt="CI" src="https://github.com/adm-iola/iola-cli/actions/workflows/ci.yml/badge.svg">
25
+ </a>
26
+ <a href="https://github.com/adm-iola/iola-cli/actions/workflows/npm-publish.yml">
27
+ <img alt="npm publish" src="https://github.com/adm-iola/iola-cli/actions/workflows/npm-publish.yml/badge.svg">
28
+ </a>
29
+ <a href="https://github.com/adm-iola/iola-cli/blob/main/LICENSE">
30
+ <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg">
31
+ </a>
32
+ <img alt="Node.js 22.5+" src="https://img.shields.io/badge/node-22.5%2B-339933">
33
+ </p>
34
+
5
35
  CLI и AI-агент городского округа "Город Йошкар-Ола".
6
36
 
7
37
  Подробная документация: [GitHub Wiki](https://github.com/adm-iola/iola-cli/wiki).
@@ -129,6 +159,9 @@ iola version --check
129
159
  - subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
130
160
  - полноценный локальный MCP server по stdio/http: tools, resources и prompts;
131
161
  - MCP-мост для локальной модели: встроенный `iola-local` доступен как `mcp:iola-local:TOOL`;
162
+ - вопросы по открытым данным сначала идут в публичный remote MCP `https://apiiola.yasg.ru/mcp`
163
+ через `layer_suggest`, `layer_query`, `layer_get` и `layer_answer_context`, а локальная БД/API
164
+ остаются fallback;
132
165
  - дополнительные stdio MCP-серверы можно добавить в `~/.iola/config.json` в раздел `mcp.servers`;
133
166
  - браузерный runtime через Playwright: чтение страниц, скриншоты, PDF, клики, ввод и eval;
134
167
  - управляемые локальные файловые операции с режимами `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.73",
3
+ "version": "0.1.75",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -6477,22 +6477,44 @@ async function buildDataContext(question) {
6477
6477
  await assertPermission("externalApi");
6478
6478
  const queryTerms = extractSearchTerms(question);
6479
6479
  const patterns = extractStructuredPatterns(question);
6480
- const layers = await callMcpTool("layer.list", { category: "Образование" });
6481
- const targetLayerIds = resolveTargetLayerIds(patterns);
6482
- const layerResults = await Promise.all(targetLayerIds.map((layer) =>
6483
- callMcpTool("layer.query", { layer, query: question, terms: queryTerms, patterns, limit: 8 })));
6484
- const layerMap = Object.fromEntries(layerResults.map((result) => [result.layer, result.items || []]));
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 || []]));
6485
6504
 
6486
- return {
6487
- layers,
6488
- query: {
6489
- text: question,
6490
- terms: queryTerms,
6491
- patterns,
6492
- },
6493
- schools: layerMap.schools || [],
6494
- kindergartens: layerMap.kindergartens || [],
6495
- };
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
+ }
6496
6518
  }
6497
6519
 
6498
6520
  function resolveTargetLayerIds(patterns = {}) {
@@ -6878,6 +6900,7 @@ async function showMcpInfo(args) {
6878
6900
  server_name: info.server_name,
6879
6901
  server_version: info.server_version,
6880
6902
  skill_version: info.skill_version,
6903
+ contract_version: info.contract_version || "-",
6881
6904
  npm_package: info.npm_package,
6882
6905
  mcp_endpoint: info.mcp_endpoint,
6883
6906
  layers: info.data_layers.map((layer) => layer.id).join(", "),
@@ -6928,9 +6951,9 @@ async function listDataset(dataset, args) {
6928
6951
 
6929
6952
  const data = options.local
6930
6953
  ? searchLocalRecords(options.search || options._.join(" ") || "", { dataset, limit: Number(options.limit || 20), fts: options.fts })
6931
- : normalizeItems(await fetchJsonMaybeCached(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?${params}`, options));
6954
+ : await listDatasetViaRemoteMcp(dataset, options, params);
6932
6955
  const items = data;
6933
- const filtered = applyDatasetFilters(items, options);
6956
+ const filtered = applyDatasetFilters(items, options.local ? options : { ...options, search: "" });
6934
6957
  const limited = filtered.slice(0, Number(options.limit || 20));
6935
6958
  const summarized = limited.map(selectPublicSummary);
6936
6959
  const projected = projectColumns(summarized, options.columns);
@@ -6952,13 +6975,34 @@ async function listDataset(dataset, args) {
6952
6975
  printDatasetTable(projected, options.columns);
6953
6976
  }
6954
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
+
6955
6997
  async function getDatasetItem(dataset, options) {
6956
6998
  if (!options.inn) {
6957
6999
  throw new Error(`INN is required. Example: iola ${dataset} get --inn 1215067180`);
6958
7000
  }
6959
7001
 
6960
- const data = await fetchJson(`${await getApiBaseUrl()}/${DATASETS[dataset].endpoint}?limit=500&offset=0`);
6961
- 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;
6962
7006
 
6963
7007
  if (!item) {
6964
7008
  throw new Error(`Record was not found in ${dataset}: inn=${options.inn}`);
@@ -6987,13 +7031,13 @@ async function searchAll(args) {
6987
7031
  searchLocalRecords(query, { dataset: "kindergartens", limit, fts: options.fts }),
6988
7032
  ]
6989
7033
  : await Promise.all([
6990
- fetchJsonMaybeCached(`${await getApiBaseUrl()}/schools?limit=100&offset=0`, options),
6991
- 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 }),
6992
7036
  ]);
6993
7037
 
6994
7038
  const result = {
6995
- schools: projectColumns(filterItems(normalizeItems(schools), query).slice(0, limit).map(selectPublicSummary), options.columns),
6996
- 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),
6997
7041
  };
6998
7042
 
6999
7043
  if (options.json || options.format === "json") {
@@ -7879,8 +7923,10 @@ function mcpTools() {
7879
7923
  { name: "status", description: "Статус локальной БД, sync и активного AI-профиля.", inputSchema: schema() },
7880
7924
  { name: "layer.list", description: "Список слоев данных и их схем.", inputSchema: schema({ category: { type: "string" } }) },
7881
7925
  { name: "layer.schema", description: "Схема слоя данных.", inputSchema: schema({ layer: { type: "string" } }) },
7926
+ { name: "layer.suggest", description: "Подобрать слой данных по вопросу пользователя через публичный MCP.", inputSchema: schema({ query: { type: "string" }, limit: { type: "number" } }) },
7882
7927
  { name: "layer.query", description: "Поиск по слою данных через общий retrieval.", inputSchema: schema({ layer: { type: "string" }, query: { type: "string" }, terms: { type: "array" }, patterns: { type: "object" }, limit: { type: "number" } }) },
7883
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" } }) },
7884
7930
  { name: "search", description: "Поиск по локальным открытым данным Йошкар-Олы.", inputSchema: schema({ query: { type: "string" }, dataset: { type: "string" }, limit: { type: "number" } }) },
7885
7931
  { name: "card", description: "Карточка объекта по названию или ИНН.", inputSchema: schema({ query: { type: "string" } }) },
7886
7932
  { name: "quality", description: "Проверки качества данных.", inputSchema: schema({ scope: { type: "string" } }) },
@@ -7917,16 +7963,40 @@ function mcpPrompts() {
7917
7963
 
7918
7964
  async function callMcpTool(name, args = {}) {
7919
7965
  if (name === "layer.list") {
7920
- return Object.entries(DATASETS)
7921
- .map(([id, meta]) => layerSchema(id))
7922
- .filter((layer) => !args.category || layer.category === args.category);
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
+ }
7923
7990
  }
7924
- if (name === "layer.schema") return layerSchema(args.layer);
7925
- if (name === "layer.query") return queryLayer(args.layer, args);
7926
7991
  if (name === "layer.get") {
7927
- const result = await queryLayer(args.layer, { query: args.inn || args.query || "", terms: [args.inn || args.query || ""], limit: 1 });
7928
- return result.items[0] || null;
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
+ }
7929
7998
  }
7999
+ if (name === "layer.answer_context") return callPublicMcpTool("layer_answer_context", { question: args.question || "", layer: args.layer || "", limit: Number(args.limit || 5) });
7930
8000
  if (name === "index.search") return searchDocs(args.query || "", Number(args.limit || 20));
7931
8001
  if (name === "report") {
7932
8002
  const output = args.output || `${args.name || "education-contacts"}.${args.format || "xlsx"}`;
@@ -9329,6 +9399,54 @@ async function fetchJson(url) {
9329
9399
  return response.json();
9330
9400
  }
9331
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
+
9332
9450
  function printJson(value) {
9333
9451
  console.log(JSON.stringify(value, null, 2));
9334
9452
  }