@iola_adm/iola-cli 0.1.81 → 0.1.83

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +94 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
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
@@ -774,9 +774,10 @@ async function startAgentReadline() {
774
774
  }
775
775
 
776
776
  async function startAgentRawInput() {
777
- const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, renderedLines: 0, rawMode: true, pendingOutput: "", aiStatus: null };
777
+ const state = { history: [], buffer: "", selected: 0, slashOffset: 0, slashOpen: false, running: false, renderedInputLines: 0, renderedLines: 0, rawMode: true, pendingOutput: "", aiStatus: null, statusBar: false, statusRows: 0 };
778
778
  const wasRaw = input.isRaw;
779
779
  activateRawInput(input);
780
+ setupAgentStatusBar(state);
780
781
 
781
782
  await refreshAgentAiStatus(state);
782
783
  const render = () => renderAgentInput(state);
@@ -857,6 +858,8 @@ async function startAgentRawInput() {
857
858
  }
858
859
  }
859
860
  } finally {
861
+ clearAgentInputArea(state);
862
+ clearAgentStatusBar(state);
860
863
  if (!wasRaw) input.setRawMode(false);
861
864
  input.pause();
862
865
  }
@@ -1305,8 +1308,6 @@ function renderAgentInput(state) {
1305
1308
  const prompt = "> ";
1306
1309
  const lines = state.buffer.split("\n");
1307
1310
  const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1)];
1308
- const statusLine = truncateTerminalLine(` ${buildAgentStatusLine(state)}`);
1309
- const cwdLine = colorMuted(statusLine);
1310
1311
  const menuLines = [];
1311
1312
  if (state.slashOpen) {
1312
1313
  const matches = currentSlashMatches(state);
@@ -1328,7 +1329,8 @@ function renderAgentInput(state) {
1328
1329
  }
1329
1330
  }
1330
1331
 
1331
- const renderedLines = [cwdLine, ...menuLines, ...inputLines];
1332
+ renderAgentStatusBar(state);
1333
+ const renderedLines = [...menuLines, ...inputLines];
1332
1334
  output.write(renderedLines.join("\n"));
1333
1335
  if (output.isTTY) {
1334
1336
  const cursorColumn = visibleLength(inputLines[inputLines.length - 1]);
@@ -1349,6 +1351,37 @@ function clearAgentInputArea(state = null) {
1349
1351
  }
1350
1352
  }
1351
1353
 
1354
+ function setupAgentStatusBar(state) {
1355
+ if (!output.isTTY) return;
1356
+ const rows = Number(output.rows || 0);
1357
+ if (rows < 4) return;
1358
+ state.statusBar = true;
1359
+ state.statusRows = rows;
1360
+ output.write(`\x1b[1;${rows - 1}r`);
1361
+ output.write(`\x1b[${rows - 1};1H`);
1362
+ }
1363
+
1364
+ function renderAgentStatusBar(state) {
1365
+ if (!output.isTTY || !state.statusBar) return;
1366
+ const rows = Number(output.rows || state.statusRows || 0);
1367
+ if (rows < 4) return;
1368
+ if (rows !== state.statusRows) {
1369
+ state.statusRows = rows;
1370
+ output.write(`\x1b[1;${rows - 1}r`);
1371
+ }
1372
+ const statusLine = colorMuted(truncateTerminalLine(` ${buildAgentStatusLine(state)} `));
1373
+ output.write(`\x1b7\x1b[${rows};1H\x1b[2K${statusLine}\x1b8`);
1374
+ }
1375
+
1376
+ function clearAgentStatusBar(state) {
1377
+ if (!output.isTTY || !state?.statusBar) return;
1378
+ const rows = Number(output.rows || state.statusRows || 0);
1379
+ output.write("\x1b[r");
1380
+ if (rows >= 1) output.write(`\x1b7\x1b[${rows};1H\x1b[2K\x1b8`);
1381
+ state.statusBar = false;
1382
+ state.statusRows = 0;
1383
+ }
1384
+
1352
1385
  function startActivityIndicator(label = "работаю") {
1353
1386
  const doneLabel = "готово";
1354
1387
  if (!output.isTTY || process.env.NO_COLOR === "1") {
@@ -6107,7 +6140,7 @@ function pickDirectDataItem(question, dataContext, rows) {
6107
6140
  function itemNameHasNumber(item, number) {
6108
6141
  const name = String(item.name || item.title || item.fns_full_name || item.fns_short_name || "").toLocaleLowerCase("ru-RU");
6109
6142
  const escaped = String(number).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6110
- return new RegExp(`(?:№\\s*${escaped}|\\b(?:школа|сош|лицей|гимназия|сад|детский сад)\\s*№?\\s*${escaped}\\b)`, "iu").test(name);
6143
+ return new RegExp(`(?:№\\s*${escaped}(?!\\d)|\\b(?:школа|сош|лицей|гимназия|сад|детский сад)\\s*№?\\s*${escaped}\\b)`, "iu").test(name);
6111
6144
  }
6112
6145
 
6113
6146
  function formatDirectDataField(field, item) {
@@ -6115,7 +6148,7 @@ function formatDirectDataField(field, item) {
6115
6148
  if (field === "head") {
6116
6149
  const head = item.head || item.fns_head_name;
6117
6150
  if (!head) return "";
6118
- const position = item.fns_head_position || (item.layer === "kindergartens" ? "заведующий" : "директор");
6151
+ const position = capitalizeFirst(item.fns_head_position || (item.layer === "kindergartens" ? "заведующий" : "директор"));
6119
6152
  return `${position}: ${head} (${name}).`;
6120
6153
  }
6121
6154
  if (field === "website") return item.website ? `Сайт: ${item.website}` : `Сайт для ${name} в открытых данных не указан.`;
@@ -6138,6 +6171,11 @@ function getDirectDataItemName(item) {
6138
6171
  return item.name || item.title || item.fns_short_name || item.fns_full_name || "организация";
6139
6172
  }
6140
6173
 
6174
+ function capitalizeFirst(value) {
6175
+ const text = String(value || "");
6176
+ return text ? `${text[0].toLocaleUpperCase("ru-RU")}${text.slice(1)}` : text;
6177
+ }
6178
+
6141
6179
  async function resolveUsableAiProfile(config, options = {}) {
6142
6180
  const explicit = Boolean(options.profile || options.provider);
6143
6181
  const providerConfig = resolveAiProfile(config, options);
@@ -6556,6 +6594,7 @@ async function buildDataContext(question) {
6556
6594
  try {
6557
6595
  const context = await callPublicMcpTool("layer_answer_context", { question, limit: 8 });
6558
6596
  const layerMap = Object.fromEntries((context.results || []).map((result) => [result.layer?.id || result.layer, result.items || []]));
6597
+ await enrichLayerMapWithExactMatches(layerMap, question, queryTerms, patterns);
6559
6598
  return {
6560
6599
  source: "remote-mcp",
6561
6600
  contract_version: context.contract_version,
@@ -6593,6 +6632,31 @@ async function buildDataContext(question) {
6593
6632
  }
6594
6633
  }
6595
6634
 
6635
+ async function enrichLayerMapWithExactMatches(layerMap, question, queryTerms, patterns) {
6636
+ if (!patterns.numbers?.length) return;
6637
+ const targetLayerIds = resolveTargetLayerIds(patterns);
6638
+ await Promise.all(targetLayerIds.map(async (layer) => {
6639
+ try {
6640
+ const result = await queryLayer(layer, { query: question, terms: queryTerms, patterns, limit: 8 });
6641
+ const existing = layerMap[layer] || [];
6642
+ const existingKeys = new Set(existing.map((item) => item.inn || item.name || item.fns_short_name).filter(Boolean));
6643
+ const exact = (result.items || []).filter((item) =>
6644
+ patterns.numbers.some((number) => itemNameHasNumber(item, number)));
6645
+ layerMap[layer] = [
6646
+ ...exact.filter((item) => {
6647
+ const key = item.inn || item.name || item.fns_short_name;
6648
+ if (!key || existingKeys.has(key)) return false;
6649
+ existingKeys.add(key);
6650
+ return true;
6651
+ }),
6652
+ ...existing,
6653
+ ];
6654
+ } catch {
6655
+ // Remote MCP remains the primary source; exact local/API enrichment is best effort.
6656
+ }
6657
+ }));
6658
+ }
6659
+
6596
6660
  function resolveTargetLayerIds(patterns = {}) {
6597
6661
  const knownLayers = Object.keys(DATASETS);
6598
6662
  if (patterns.targetLayers?.length) return patterns.targetLayers.filter((layer) => DATASETS[layer]);
@@ -6690,13 +6754,16 @@ function extractSearchTerms(question) {
6690
6754
 
6691
6755
  function extractStructuredPatterns(question) {
6692
6756
  const normalized = question.toLocaleLowerCase("ru-RU");
6693
- const numbers = [...new Set([...normalized.matchAll(/\b\d{1,3}\b/g)].map((match) => match[0]))];
6757
+ const numbers = [...new Set([
6758
+ ...[...normalized.matchAll(/\b\d{1,3}\b/g)].map((match) => match[0]),
6759
+ ...extractOrdinalNumbers(normalized),
6760
+ ])];
6694
6761
  const inns = [...new Set([...normalized.matchAll(/\b\d{10,12}\b/g)].map((match) => match[0]))];
6695
6762
  const targetLayers = [];
6696
- if (/(^|[^а-яёa-z])(школа|школы|лицей|лицея|гимназия|гимназии)(?=$|[^а-яёa-z])/iu.test(normalized)) {
6763
+ if (/(школ|сош|лице|гимнази)/iu.test(normalized)) {
6697
6764
  targetLayers.push("schools");
6698
6765
  }
6699
- if (/(^|[^а-яёa-z])(сад|сады|детсад|детский|детские|доу|мбдоу)(?=$|[^а-яёa-z])/iu.test(normalized)) {
6766
+ if (/(детсад|детск|сад|сады|доу|мбдоу)/iu.test(normalized)) {
6700
6767
  targetLayers.push("kindergartens");
6701
6768
  }
6702
6769
  const streetMatches = [
@@ -6708,6 +6775,24 @@ function extractStructuredPatterns(question) {
6708
6775
  return { numbers, inns, streets, targetLayers: [...new Set(targetLayers)] };
6709
6776
  }
6710
6777
 
6778
+ function extractOrdinalNumbers(normalizedQuestion) {
6779
+ const ordinals = [
6780
+ ["1", "(?:перв(?:ая|ой|ую|ое|ого|ом|ым|ых)?|первую)"],
6781
+ ["2", "(?:втор(?:ая|ой|ую|ое|ого|ом|ым|ых)?|вторую)"],
6782
+ ["3", "(?:трет(?:ья|ий|ью|ье|ьего|ьем|ьим|ьих)?|третью)"],
6783
+ ["4", "четверт(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6784
+ ["5", "пят(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6785
+ ["6", "шест(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6786
+ ["7", "седьм(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6787
+ ["8", "восьм(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6788
+ ["9", "девят(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6789
+ ["10", "десят(?:ая|ой|ую|ое|ого|ом|ым|ых)?"],
6790
+ ];
6791
+ return ordinals
6792
+ .filter(([, pattern]) => new RegExp(`(^|[^а-яёa-z])${pattern}(?=$|[^а-яёa-z])`, "iu").test(normalizedQuestion))
6793
+ .map(([number]) => number);
6794
+ }
6795
+
6711
6796
  function cleanupPattern(value) {
6712
6797
  return value
6713
6798
  .replace(/\b(школа|школы|сад|детский|детские|сады|лицей|гимназия|контакты|телефон|адрес|найди|покажи)\b/giu, " ")