@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.
- package/README.md +35 -2
- package/package.json +1 -1
- package/src/cli.js +148 -30
package/README.md
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-

|
|
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
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
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
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
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
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
|
-
:
|
|
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
|
|
6961
|
-
|
|
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
|
-
|
|
6991
|
-
|
|
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
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
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
|
-
|
|
7928
|
-
|
|
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
|
}
|