@iola_adm/iola-cli 0.2.3 → 0.2.5

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
@@ -156,6 +156,16 @@ iola ai setup gigachat --model GigaChat-2
156
156
 
157
157
  У GigaChat для физических лиц есть Freemium-лимит на токены; для больших объемов используются платные пакеты. У YandexGPT тарификация идет через Yandex Cloud по токенам и квотам аккаунта, актуальные бесплатные гранты или лимиты нужно проверять в консоли Yandex Cloud.
158
158
 
159
+ Геокодер для пользовательских geo-skills:
160
+
161
+ ```bash
162
+ iola geo key set yandex
163
+ iola geo key doctor
164
+ iola geo geocode "Йошкар-Ола, улица Петрова, 15"
165
+ ```
166
+
167
+ Инструкция по получению ключа: [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key).
168
+
159
169
  Зарубежные API-ключи:
160
170
 
161
171
  - OpenAI Platform: регистрация `https://platform.openai.com/`, ключи `https://platform.openai.com/api-keys`;
@@ -182,6 +192,8 @@ iola version --check
182
192
  - [Первый запуск](https://github.com/adm-iola/iola-cli/wiki/Первый-запуск)
183
193
  - [Мастер настройки](https://github.com/adm-iola/iola-cli/wiki/Мастер-настройки)
184
194
  - [AI-профили](https://github.com/adm-iola/iola-cli/wiki/AI-профили)
195
+ - [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key)
196
+ - [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей)
185
197
  - [Локальный инструментальный агент](https://github.com/adm-iola/iola-cli/wiki/Локальный-инструментальный-агент)
186
198
  - [Skills и toolsets](https://github.com/adm-iola/iola-cli/wiki/Skills-и-toolsets)
187
199
  - [Локальные файлы](https://github.com/adm-iola/iola-cli/wiki/Локальные-файлы)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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
@@ -440,6 +440,7 @@ const COMMANDS = new Map([
440
440
  ["health", checkHealth],
441
441
  ["layers", listLayers],
442
442
  ["data", handleData],
443
+ ["geo", handleGeo],
443
444
  ["schools", listSchools],
444
445
  ["kindergartens", listKindergartens],
445
446
  ["search", searchAll],
@@ -653,6 +654,9 @@ Usage:
653
654
  iola kindergartens [--limit 10] [--search TEXT] [--where FIELD=VALUE] [--columns a,b,c] [--format table|json|csv]
654
655
  iola kindergartens get --inn INN [--json]
655
656
  iola search TEXT [--limit 5] [--format table|json|csv]
657
+ iola geo key set yandex
658
+ iola geo key doctor
659
+ iola geo geocode "Йошкар-Ола, ул. Петрова, 15"
656
660
  iola mcp-info [--json]
657
661
  iola setup codex
658
662
  iola onboard
@@ -701,6 +705,7 @@ async function startAgent() {
701
705
  setTerminalTitle(`iola - ${path.basename(process.cwd()) || process.cwd()}`);
702
706
  await showBanner();
703
707
  await ensureAgentAiReady();
708
+ await printActiveAiModelLine();
704
709
  console.log("Интерактивный режим. Введите /help для списка команд, /master чтобы запустить мастер настройки, /exit для выхода.");
705
710
  await runHooks("SessionStart", { mode: "agent" });
706
711
 
@@ -714,6 +719,16 @@ async function startAgent() {
714
719
  await runHooks("SessionEnd", { mode: "agent" });
715
720
  }
716
721
 
722
+ async function printActiveAiModelLine() {
723
+ const config = await loadConfig();
724
+ const name = getActiveProfileName(config);
725
+ const profile = config.ai.profiles?.[name] || {
726
+ provider: config.ai.provider,
727
+ model: config.ai.model,
728
+ };
729
+ console.log(`Активная модель: ${name} (${profile.provider || "-"}, ${profile.model || "-"})`);
730
+ }
731
+
717
732
  async function ensureAgentAiReady() {
718
733
  const readiness = await getAiReadiness();
719
734
  if (readiness.ready) return readiness;
@@ -1870,6 +1885,7 @@ async function doctor(args = []) {
1870
1885
  modelAvailable: await checkConfiguredModel({ ai: activeAiProfile }),
1871
1886
  openaiKey: process.env.OPENAI_API_KEY ? "env" : secrets.openai?.apiKey ? "local" : "missing",
1872
1887
  openrouterKey: process.env.OPENROUTER_API_KEY ? "env" : secrets.openrouter?.apiKey ? "local" : "missing",
1888
+ yandexGeocoderKey: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "env" : secrets.yandexGeocoder?.apiKey ? "local" : "missing",
1873
1889
  ollama: diagnostics.ollama.installed ? diagnostics.ollama.version : "not-installed",
1874
1890
  },
1875
1891
  skills: {
@@ -2554,6 +2570,8 @@ async function handleWiki(args) {
2554
2570
  ["Первый запуск", `${base}/Первый-запуск`],
2555
2571
  ["Мастер настройки", `${base}/Мастер-настройки`],
2556
2572
  ["AI-профили", `${base}/AI-профили`],
2573
+ ["Yandex Geocoder API key", `${base}/Yandex-Geocoder-API-key`],
2574
+ ["Скиллы для жителей", `${base}/Скиллы-для-жителей`],
2557
2575
  ["Локальный инструментальный агент", `${base}/Локальный-инструментальный-агент`],
2558
2576
  ["Skills и toolsets", `${base}/Skills-и-toolsets`],
2559
2577
  ["Локальные файлы", `${base}/Локальные-файлы`],
@@ -4852,17 +4870,38 @@ function normalizeModelMenuTarget(value = "") {
4852
4870
  }
4853
4871
 
4854
4872
  async function chooseModelTarget() {
4873
+ const active = await getActiveAiSummary();
4855
4874
  console.log("Выберите AI-подключение:");
4856
- console.log(" 1. Локальные модели");
4857
- console.log(" 2. Российские AI (YandexGPT/GigaChat)");
4858
- console.log(" 3. API (OpenAI/OpenRouter)");
4859
- console.log(" 4. Codex CLI");
4875
+ console.log(` 1. Локальные модели${active.group === "local" ? " [выбрано]" : ""}`);
4876
+ console.log(` 2. Российские AI (YandexGPT/GigaChat)${active.group === "russian" ? " [выбрано]" : ""}`);
4877
+ console.log(` 3. API (OpenAI/OpenRouter)${active.group === "api" ? " [выбрано]" : ""}`);
4878
+ console.log(` 4. Codex CLI${active.group === "codex" ? " [выбрано]" : ""}`);
4860
4879
  console.log(" 0. Отмена");
4861
4880
 
4862
4881
  const answer = await askText("Номер: ");
4863
4882
  return { 1: "local", 2: "russian", 3: "api", 4: "codex" }[answer.trim()] || "";
4864
4883
  }
4865
4884
 
4885
+ async function getActiveAiSummary() {
4886
+ const config = await loadConfig();
4887
+ const name = getActiveProfileName(config);
4888
+ const profile = config.ai.profiles?.[name] || {
4889
+ provider: config.ai.provider,
4890
+ model: config.ai.model,
4891
+ };
4892
+ const provider = profile.provider || "";
4893
+ const group = provider === "iola" || provider === "ollama"
4894
+ ? "local"
4895
+ : provider === "yandexgpt" || provider === "gigachat"
4896
+ ? "russian"
4897
+ : provider === "openai" || provider === "openrouter"
4898
+ ? "api"
4899
+ : provider === "codex"
4900
+ ? "codex"
4901
+ : "";
4902
+ return { name, provider, model: profile.model || "", group };
4903
+ }
4904
+
4866
4905
  async function openModelTargetMenu(target) {
4867
4906
  if (target === "local") {
4868
4907
  const selection = await chooseLocalModel();
@@ -4904,9 +4943,10 @@ async function openModelTargetMenu(target) {
4904
4943
 
4905
4944
  async function chooseApiProvider() {
4906
4945
  const config = await loadConfig();
4946
+ const active = await getActiveAiSummary();
4907
4947
  const apiProfiles = Object.entries(config.ai.profiles || {})
4908
4948
  .filter(([, profile]) => profile.provider === "openai" || profile.provider === "openrouter")
4909
- .map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
4949
+ .map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})${active.provider === profile.provider ? " [выбрано]" : ""}` }));
4910
4950
  const choices = [
4911
4951
  ...apiProfiles,
4912
4952
  { id: "openai", label: "OpenAI API" },
@@ -4923,9 +4963,10 @@ async function chooseApiProvider() {
4923
4963
 
4924
4964
  async function chooseRussianProvider() {
4925
4965
  const config = await loadConfig();
4966
+ const active = await getActiveAiSummary();
4926
4967
  const russianProfiles = Object.entries(config.ai.profiles || {})
4927
4968
  .filter(([, profile]) => profile.provider === "yandexgpt" || profile.provider === "gigachat")
4928
- .map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})` }));
4969
+ .map(([name, profile]) => ({ id: profile.provider, label: `${name}: ${profile.provider} (${profile.model || "-"})${active.provider === profile.provider ? " [выбрано]" : ""}` }));
4929
4970
  const choices = [
4930
4971
  ...russianProfiles,
4931
4972
  { id: "yandexgpt", label: "YandexGPT API" },
@@ -4949,15 +4990,16 @@ async function getDefaultApiProviderForModelSwitch() {
4949
4990
  }
4950
4991
 
4951
4992
  async function chooseLocalModel() {
4993
+ const active = await getActiveAiSummary();
4952
4994
  const models = await listAiModels("ollama");
4953
4995
  const choices = [
4954
- { id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router` },
4996
+ { id: IOLA_LOCAL_MODEL, provider: "iola", label: `${IOLA_LOCAL_MODEL} - IOLA local router${active.provider === "iola" && active.model === IOLA_LOCAL_MODEL ? " [выбрано]" : ""}` },
4955
4997
  ...models
4956
4998
  .filter((model) => model.id !== IOLA_LOCAL_MODEL)
4957
4999
  .map((model) => ({
4958
5000
  id: model.id,
4959
5001
  provider: "ollama",
4960
- label: `${model.id}${model.note ? ` - ${model.note}` : ""}`,
5002
+ label: `${model.id}${model.note ? ` - ${model.note}` : ""}${active.provider === "ollama" && active.model === model.id ? " [выбрано]" : ""}`,
4961
5003
  })),
4962
5004
  { id: "__manual__", provider: "ollama", label: "Другая Ollama-модель: ввести имя вручную" },
4963
5005
  ].filter((item, index, array) => array.findIndex((candidate) => candidate.id === item.id) === index);
@@ -4980,6 +5022,7 @@ async function chooseLocalModel() {
4980
5022
  }
4981
5023
 
4982
5024
  async function chooseAiModel(provider) {
5025
+ const active = await getActiveAiSummary();
4983
5026
  if (provider === "openrouter") {
4984
5027
  return chooseOpenRouterModel();
4985
5028
  }
@@ -5025,7 +5068,8 @@ async function chooseAiModel(provider) {
5025
5068
  console.log("Выберите модель:");
5026
5069
  filtered.forEach((model, index) => {
5027
5070
  const date = model.releaseDate ? ` (${model.releaseDate})` : "";
5028
- console.log(` ${index + 1}. ${model.id}${date}${model.note ? ` - ${model.note}` : ""}`);
5071
+ const selected = active.provider === provider && active.model === model.id ? " [выбрано]" : "";
5072
+ console.log(` ${index + 1}. ${model.id}${date}${model.note ? ` - ${model.note}` : ""}${selected}`);
5029
5073
  });
5030
5074
  console.log(" 0. Отмена");
5031
5075
 
@@ -5034,6 +5078,7 @@ async function chooseAiModel(provider) {
5034
5078
  }
5035
5079
 
5036
5080
  async function chooseOpenRouterModel() {
5081
+ const active = await getActiveAiSummary();
5037
5082
  const ready = await ensureApiKeyForModelSelection("openrouter");
5038
5083
  if (!ready) return "";
5039
5084
 
@@ -5078,7 +5123,8 @@ async function chooseOpenRouterModel() {
5078
5123
  filtered.forEach((model, index) => {
5079
5124
  const date = model.releaseDate || "дата неизвестна";
5080
5125
  const context = model.contextLength ? `, ctx ${formatCompactNumber(model.contextLength)}` : "";
5081
- console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}`);
5126
+ const selected = active.provider === "openrouter" && active.model === model.id ? " [выбрано]" : "";
5127
+ console.log(` ${index + 1}. ${model.id} (${date}${context}) - ${model.note || model.id}${selected}`);
5082
5128
  });
5083
5129
  console.log(" 0. Назад");
5084
5130
 
@@ -5284,6 +5330,176 @@ async function deleteAiKey(provider) {
5284
5330
  console.log(`Локальный ключ ${provider} удален.`);
5285
5331
  }
5286
5332
 
5333
+ async function handleGeo(args) {
5334
+ const [command, subcommand, ...rest] = args;
5335
+
5336
+ if (command === "key") {
5337
+ await handleGeoKey([subcommand, ...rest]);
5338
+ return;
5339
+ }
5340
+
5341
+ if (command === "geocode") {
5342
+ await geoGeocode([subcommand, ...rest].filter(Boolean));
5343
+ return;
5344
+ }
5345
+
5346
+ if (command === "doctor" || command === "status") {
5347
+ await printGeoKeyStatus({ check: command === "doctor" });
5348
+ return;
5349
+ }
5350
+
5351
+ throw new Error(`Команды geo:
5352
+ iola geo key set yandex
5353
+ iola geo key status
5354
+ iola geo key doctor
5355
+ iola geo key delete yandex
5356
+ iola geo geocode "Йошкар-Ола, ул. Петрова, 15"`);
5357
+ }
5358
+
5359
+ async function handleGeoKey(args) {
5360
+ const [action, provider = "yandex"] = args;
5361
+ if (provider !== "yandex") {
5362
+ throw new Error("Сейчас поддерживается только провайдер: yandex");
5363
+ }
5364
+
5365
+ if (action === "set") {
5366
+ await setYandexGeocoderKey();
5367
+ return;
5368
+ }
5369
+
5370
+ if (action === "status") {
5371
+ await printGeoKeyStatus();
5372
+ return;
5373
+ }
5374
+
5375
+ if (action === "doctor" || action === "check") {
5376
+ await printGeoKeyStatus({ check: true });
5377
+ return;
5378
+ }
5379
+
5380
+ if (action === "delete") {
5381
+ const secrets = await loadSecrets();
5382
+ delete secrets.yandexGeocoder;
5383
+ await saveSecrets(secrets);
5384
+ console.log("Локальный ключ yandex geocoder удален.");
5385
+ return;
5386
+ }
5387
+
5388
+ throw new Error("Команды geo key: set yandex, status, doctor, delete yandex.");
5389
+ }
5390
+
5391
+ async function setYandexGeocoderKey() {
5392
+ if (!process.stdin.isTTY) {
5393
+ throw new Error("Для сохранения ключа запустите команду в интерактивном терминале.");
5394
+ }
5395
+
5396
+ const key = (await askText("Введите YANDEX_GEOCODER_API_KEY: ")).trim();
5397
+ if (!key) throw new Error("Ключ пустой, сохранение отменено.");
5398
+
5399
+ const secrets = await loadSecrets();
5400
+ secrets.yandexGeocoder = { apiKey: key };
5401
+ await saveSecrets(secrets);
5402
+ console.log(`Ключ yandex geocoder сохранен локально: ${SECRETS_FILE}`);
5403
+
5404
+ const shouldCheck = await confirm("Проверить ключ запросом к Yandex Geocoder? [Y/n] ");
5405
+ if (shouldCheck) await checkYandexGeocoderKey({ print: true });
5406
+ }
5407
+
5408
+ async function printGeoKeyStatus(options = {}) {
5409
+ const key = await getYandexGeocoderKey();
5410
+ const rows = [{
5411
+ provider: "yandex-geocoder",
5412
+ env: (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) ? "yes" : "no",
5413
+ local: (await loadSecrets()).yandexGeocoder?.apiKey ? "yes" : "no",
5414
+ status: key ? "configured" : "missing",
5415
+ }];
5416
+ printTable(rows, [
5417
+ ["provider", "Провайдер"],
5418
+ ["env", "Env"],
5419
+ ["local", "Локально"],
5420
+ ["status", "Статус"],
5421
+ ]);
5422
+ if (options.check) await checkYandexGeocoderKey({ print: true });
5423
+ }
5424
+
5425
+ async function geoGeocode(args) {
5426
+ const query = args.join(" ").trim();
5427
+ if (!query) throw new Error('Адрес обязателен. Пример: iola geo geocode "Йошкар-Ола, ул. Петрова, 15"');
5428
+ const result = await callYandexGeocoder(query);
5429
+ if (!result) {
5430
+ console.log("Yandex Geocoder не вернул результат.");
5431
+ return;
5432
+ }
5433
+ printKeyValue(result);
5434
+ }
5435
+
5436
+ async function checkYandexGeocoderKey(options = {}) {
5437
+ try {
5438
+ const result = await callYandexGeocoder("Йошкар-Ола");
5439
+ if (!result?.coordinates) throw new Error("Yandex Geocoder вернул пустой результат.");
5440
+ if (options.print) {
5441
+ console.log(`Yandex Geocoder: ok (${result.name || result.address || result.coordinates})`);
5442
+ }
5443
+ return true;
5444
+ } catch (error) {
5445
+ if (options.print) {
5446
+ console.log(`Yandex Geocoder: error - ${error instanceof Error ? error.message : String(error)}`);
5447
+ }
5448
+ return false;
5449
+ }
5450
+ }
5451
+
5452
+ async function callYandexGeocoder(query) {
5453
+ const apiKey = await getYandexGeocoderKey();
5454
+ if (!apiKey) {
5455
+ throw new Error("Yandex Geocoder API key не найден. Выполните iola geo key set yandex или задайте YANDEX_GEOCODER_API_KEY.");
5456
+ }
5457
+
5458
+ const url = new URL("https://geocode-maps.yandex.ru/v1/");
5459
+ url.searchParams.set("apikey", apiKey);
5460
+ url.searchParams.set("geocode", query);
5461
+ url.searchParams.set("format", "json");
5462
+ url.searchParams.set("lang", "ru_RU");
5463
+ url.searchParams.set("results", "1");
5464
+
5465
+ let response;
5466
+ try {
5467
+ response = await fetch(url, { signal: AbortSignal.timeout(15000) });
5468
+ } catch (error) {
5469
+ throw new Error(formatProviderFetchError("Yandex Geocoder", error));
5470
+ }
5471
+
5472
+ if (!response.ok) {
5473
+ const text = await response.text();
5474
+ if (response.status === 403 && /Invalid api key/i.test(text)) {
5475
+ throw new Error(`Yandex Geocoder request failed: ${response.status} ${response.statusText}\nInvalid api key. Если ключ только что создан, подождите до 15 минут: Yandex указывает, что активация ключа может занять до 15 минут.`);
5476
+ }
5477
+ throw new Error(`Yandex Geocoder request failed: ${response.status} ${response.statusText}\n${sanitizeSecretFromText(text, apiKey)}`);
5478
+ }
5479
+
5480
+ const payload = await response.json();
5481
+ const member = payload?.response?.GeoObjectCollection?.featureMember?.[0];
5482
+ const object = member?.GeoObject;
5483
+ if (!object) return null;
5484
+ const point = object.Point?.pos || "";
5485
+ const [lon, lat] = point.split(/\s+/);
5486
+ return {
5487
+ name: object.name || "",
5488
+ address: object.metaDataProperty?.GeocoderMetaData?.text || object.description || "",
5489
+ precision: object.metaDataProperty?.GeocoderMetaData?.precision || "",
5490
+ coordinates: lat && lon ? `${lat}, ${lon}` : point,
5491
+ map: lat && lon ? `https://yandex.ru/maps/?pt=${lon},${lat}&z=16&l=map` : "",
5492
+ };
5493
+ }
5494
+
5495
+ async function getYandexGeocoderKey() {
5496
+ if (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) {
5497
+ return process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY;
5498
+ }
5499
+ const secrets = await loadSecrets();
5500
+ return secrets.yandexGeocoder?.apiKey || "";
5501
+ }
5502
+
5287
5503
  function openDatabase() {
5288
5504
  const db = new DatabaseSync(DB_FILE);
5289
5505
  db.exec("PRAGMA busy_timeout = 5000;");
@@ -9212,6 +9428,11 @@ async function onboard(args = []) {
9212
9428
  await chooseAndSaveApiModel("gigachat");
9213
9429
  }
9214
9430
  }
9431
+ if (components.includes("yandex-geocoder")) {
9432
+ if (process.stdin.isTTY) {
9433
+ await setYandexGeocoderKey();
9434
+ }
9435
+ }
9215
9436
  if (components.includes("codex")) {
9216
9437
  await installCodexIfMissing();
9217
9438
  await aiSetup(["codex"]);
@@ -9260,6 +9481,7 @@ async function chooseOnboardComponents(status = null) {
9260
9481
  11: "index",
9261
9482
  12: "browser",
9262
9483
  13: "ollama",
9484
+ 14: "yandex-geocoder",
9263
9485
  };
9264
9486
  return [...selected].map((item) => map[item] || item).filter(Boolean);
9265
9487
  } finally {
@@ -9272,13 +9494,14 @@ function isOnboardExitAnswer(answer) {
9272
9494
  }
9273
9495
 
9274
9496
  async function getOnboardComponentStatus() {
9275
- const [config, readiness, browser, archive, codexVersion, ollamaVersion] = await Promise.all([
9497
+ const [config, readiness, browser, archive, codexVersion, ollamaVersion, yandexGeocoderKey] = await Promise.all([
9276
9498
  loadConfig(),
9277
9499
  getAiReadiness(),
9278
9500
  getBrowserStatus(),
9279
9501
  findCommand(["7z", "7zz", "7za"], ["--help"]).catch(() => null),
9280
9502
  getCommandVersion("codex", ["--version"]),
9281
9503
  getOllamaVersion(),
9504
+ getYandexGeocoderKey(),
9282
9505
  ]);
9283
9506
  const workspaceReady = existsSync(PROJECT_CONTEXT_FILE) || existsSync(PROJECT_CONTEXT_DIR_FILE) || existsSync(PROJECT_IOLA_DIR);
9284
9507
  const policyReady = (config.toolsets?.enabled || []).includes("analyst");
@@ -9296,6 +9519,7 @@ async function getOnboardComponentStatus() {
9296
9519
  archive: Boolean(archive),
9297
9520
  index: false,
9298
9521
  browser: browser.installed === "yes",
9522
+ "yandex-geocoder": Boolean(yandexGeocoderKey),
9299
9523
  };
9300
9524
  }
9301
9525
 
@@ -9314,6 +9538,7 @@ function onboardComponentRows(status) {
9314
9538
  ["11", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
9315
9539
  ["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
9316
9540
  ["13", "ollama", "Ollama", "опциональный локальный runtime"],
9541
+ ["14", "yandex-geocoder", "Yandex Geocoder API", "ключ геокодера сохранен или есть в env"],
9317
9542
  ];
9318
9543
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
9319
9544
  }
@@ -9328,7 +9553,7 @@ function defaultOnboardSelection(status) {
9328
9553
  }
9329
9554
 
9330
9555
  function defaultOnboardComponents(status) {
9331
- const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandexgpt", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama" };
9556
+ const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandexgpt", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama", 14: "yandex-geocoder" };
9332
9557
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
9333
9558
  }
9334
9559
 
package/wiki/Home.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - выгрузка CSV/JSON;
12
12
  - работа с локальной моделью Ollama;
13
13
  - работа с YandexGPT, GigaChat, OpenAI, OpenRouter и Codex CLI;
14
+ - geo-сценарии для жителей через Yandex Geocoder API;
14
15
  - подключение публичного MCP-сервера.
15
16
 
16
17
  Быстрый старт:
@@ -28,6 +29,8 @@ iola ask "найди школу 29"
28
29
  - [Первый запуск](Первый-запуск)
29
30
  - [Мастер настройки](Мастер-настройки)
30
31
  - [AI-профили](AI-профили)
32
+ - [Yandex Geocoder API key](Yandex-Geocoder-API-key)
33
+ - [Скиллы для жителей](Скиллы-для-жителей)
31
34
  - [Локальный инструментальный агент](Локальный-инструментальный-агент)
32
35
  - [Skills и toolsets](Skills-и-toolsets)
33
36
  - [Локальные файлы](Локальные-файлы)
@@ -0,0 +1,72 @@
1
+ # Yandex Geocoder API key
2
+
3
+ Эта инструкция нужна для подключения геокодера Яндекса к `iola-cli`.
4
+
5
+ Геокодер нужен жителям для вопросов вида:
6
+
7
+ - что находится рядом с моим адресом;
8
+ - где находится школа или детский сад;
9
+ - какой объект ближе;
10
+ - открыть объект на карте;
11
+ - не перепутать Йошкар-Олу, Семеновку и другие населенные пункты.
12
+
13
+ ## Получение ключа
14
+
15
+ 1. Откройте Кабинет разработчика Яндекса: `https://developer.tech.yandex.ru/services/`.
16
+ 2. Войдите в аккаунт Яндекса.
17
+ 3. В списке `API интерфейсы` найдите `API Геокодера`.
18
+ 4. Откройте `API Геокодера`.
19
+ 5. Проверьте блок тарифа. На момент проверки 02.06.2026 в кабинете был показан тариф `Бесплатный с ограничениями` и счетчик `1000` запросов в сутки.
20
+ 6. В блоке `Ключи API` нажмите `Новый ключ`.
21
+ 7. В поле `Название ключа` введите понятное имя, например `iola-cli geocoder`.
22
+ 8. Нажмите `Добавить ключ`.
23
+ 9. После создания ключ появится в списке ключей API Геокодера.
24
+ 10. Скопируйте значение ключа. Это значение будет `YANDEX_GEOCODER_API_KEY`.
25
+
26
+ Важно: новый ключ может начать работать не сразу. В документации Яндекса указано, что активация ключа может занять до 15 минут. Если CLI пишет `Invalid api key` сразу после создания ключа, подождите и повторите проверку.
27
+
28
+ ## Сохранение в CLI
29
+
30
+ Запустите:
31
+
32
+ ```bash
33
+ iola geo key set yandex
34
+ ```
35
+
36
+ CLI попросит ввести:
37
+
38
+ ```text
39
+ YANDEX_GEOCODER_API_KEY
40
+ ```
41
+
42
+ Ключ сохраняется локально на устройстве пользователя в `~/.iola/secrets.json`.
43
+
44
+ Проверить сохранение:
45
+
46
+ ```bash
47
+ iola geo key status
48
+ ```
49
+
50
+ Проверить рабочий запрос к API:
51
+
52
+ ```bash
53
+ iola geo key doctor
54
+ ```
55
+
56
+ Проверить геокодирование адреса:
57
+
58
+ ```bash
59
+ iola geo geocode "Йошкар-Ола, улица Петрова, 15"
60
+ ```
61
+
62
+ CLI также понимает переменные окружения:
63
+
64
+ - `YANDEX_GEOCODER_API_KEY`;
65
+ - `YANDEX_MAPS_API_KEY`.
66
+
67
+ ## Официальные ссылки
68
+
69
+ - Кабинет разработчика: `https://developer.tech.yandex.ru/services/`
70
+ - API Геокодера: `https://developer.tech.yandex.ru/services/2`
71
+ - Документация Geocoder API: `https://yandex.com/maps-api/docs/geocoder-api/`
72
+ - Формат запроса: `https://yandex.com/maps-api/docs/geocoder-api/request.html`
@@ -26,6 +26,15 @@ iola card "школа 29"
26
26
  iola data schools --format csv --output schools.csv
27
27
  ```
28
28
 
29
+ Геокодер:
30
+
31
+ ```bash
32
+ iola geo key set yandex
33
+ iola geo key status
34
+ iola geo key doctor
35
+ iola geo geocode "Йошкар-Ола, улица Петрова, 15"
36
+ ```
37
+
29
38
  Локальная БД:
30
39
 
31
40
  ```bash
@@ -86,6 +86,14 @@ iola index folder ./docs
86
86
 
87
87
  Если runtime уже установлен, пункт показывается как `готово`.
88
88
 
89
+ ### 14. Yandex Geocoder API
90
+
91
+ Сохраняет ключ Yandex Geocoder API локально у пользователя и проверяет его тестовым запросом.
92
+
93
+ Нужен для будущих geo-skills: поиск объектов рядом с адресом, ссылки на карту, определение расстояний и уточнение населенного пункта.
94
+
95
+ Инструкция по получению ключа: [Yandex Geocoder API key](Yandex-Geocoder-API-key).
96
+
89
97
  ## Повторный запуск
90
98
 
91
99
  Если компонент уже настроен, его можно не выбирать. Если нужно переустановить или обновить компонент, выберите его номер вручную.
@@ -0,0 +1,97 @@
1
+ # Скиллы для жителей
2
+
3
+ Эта страница фиксирует пользовательские skills, которые стоит развивать в `iola-cli`.
4
+
5
+ Цель этих skills - помогать жителю города в бытовых вопросах: где находится объект, что рядом, какой объект ближе, как открыть место на карте. Это не административный аудит данных.
6
+
7
+ ## Geo skills
8
+
9
+ Для geo skills нужен ключ `Yandex Geocoder API`.
10
+
11
+ ### nearby
12
+
13
+ Находит городские объекты рядом с адресом пользователя.
14
+
15
+ Примеры:
16
+
17
+ - `какие детские сады рядом с Петрова 15`;
18
+ - `какие школы ближе к улице Машиностроителей`;
19
+ - `что из городских учреждений рядом с домом`.
20
+
21
+ На первом этапе работает по слоям `schools` и `kindergartens`. Позже можно подключить МФЦ, поликлиники, остановки, спортобъекты и учреждения культуры.
22
+
23
+ ### route-context
24
+
25
+ Объясняет, где находится объект, простым языком: адрес, ориентиры, что рядом, ссылка на карту.
26
+
27
+ Это не полноценный навигатор и не замена маршрутизатору. Skill дает понятный контекст места для жителя.
28
+
29
+ ### address-to-services
30
+
31
+ По адресу жителя подбирает релевантные городские объекты.
32
+
33
+ Примеры:
34
+
35
+ - ближайшие школы;
36
+ - ближайшие детские сады;
37
+ - будущие слои муниципальных услуг и учреждений.
38
+
39
+ ### map-link
40
+
41
+ Добавляет в ответы ссылку на карту.
42
+
43
+ Примеры:
44
+
45
+ - открыть школу на Яндекс.Картах;
46
+ - открыть детский сад на карте;
47
+ - показать точку по координатам.
48
+
49
+ ### place-resolver
50
+
51
+ Помогает понять криво написанные адреса, названия и ориентиры.
52
+
53
+ Примеры:
54
+
55
+ - `семеновка школа`;
56
+ - `первомаиская 89`;
57
+ - `садик золотой петушок`;
58
+ - `рядом с вокзалом`.
59
+
60
+ Главная задача - не подставлять похожий, но неправильный объект.
61
+
62
+ ### distance
63
+
64
+ Считает расстояние от адреса пользователя до объекта или между объектами.
65
+
66
+ Примеры:
67
+
68
+ - `какой садик ближе к дому`;
69
+ - `как далеко школа 7 от Первомайской 89`;
70
+ - `что ближе: Пчелка или Улыбка`.
71
+
72
+ ### area-filter
73
+
74
+ Фильтрует ответы по населенному пункту или району.
75
+
76
+ Примеры:
77
+
78
+ - Йошкар-Ола;
79
+ - Семеновка;
80
+ - другие населенные пункты городского округа, когда они появятся в слоях.
81
+
82
+ Это важно, чтобы не путать объекты с одинаковыми номерами или похожими названиями.
83
+
84
+ ## Приоритет реализации
85
+
86
+ Первый этап:
87
+
88
+ 1. `nearby`
89
+ 2. `place-resolver`
90
+ 3. `map-link`
91
+
92
+ Второй этап:
93
+
94
+ 1. `distance`
95
+ 2. `area-filter`
96
+ 3. `route-context`
97
+ 4. `address-to-services`