@iola_adm/iola-cli 0.2.35 → 0.2.37

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/src/cli.js CHANGED
@@ -98,25 +98,25 @@ const YANDEX_CONNECTOR_SERVICES = {
98
98
  hint: "встречи через календарное событие, если поддерживается",
99
99
  },
100
100
  cloud: {
101
- title: "Yandex Cloud",
101
+ title: "Yandex Cloud Connector",
102
102
  category: "cloud-platform",
103
103
  scope: "",
104
- status: "separate",
105
- hint: "YandexGPT, Geocoder, SpeechKit, Vision, IAM и folder ID",
104
+ status: "ready",
105
+ hint: "геокодинг и YandexGPT через ключи Yandex Cloud",
106
106
  },
107
107
  maps: {
108
- title: "Яндекс Карты",
108
+ title: "Яндекс Геокодер",
109
109
  category: "maps",
110
110
  scope: "",
111
- status: "separate",
112
- hint: "геокодер, маршруты и ссылки на карты через отдельный API key",
111
+ status: "ready",
112
+ hint: "адреса, координаты, маршруты и ссылки на карты",
113
113
  },
114
114
  taxi: {
115
115
  title: "Яндекс Go / Такси",
116
116
  category: "mobility",
117
117
  scope: "",
118
- status: "backlog",
119
- hint: "только подготовка маршрута/deep link, без заказа и оплаты",
118
+ status: "ready",
119
+ hint: "deeplink и маршрут; заказ через API ожидает clid/apikey",
120
120
  },
121
121
  market: {
122
122
  title: "Яндекс Маркет",
@@ -151,7 +151,7 @@ const INDEXABLE_EXTENSIONS = /\.(md|txt|csv|json|html|docx|xlsx|pptx|pdf)$/i;
151
151
  const LOCAL_TOOLS = ["search_data", "search_entities", "resolve_entity_field", "get_card", "export_report", "file_read", "browser_open", "get_current_date"];
152
152
  const LEGACY_LOCAL_TOOLS = ["search_local", "export_data", "run_report", "save_view"];
153
153
  const FILE_TOOLS = ["files_tree", "files_read", "files_search", "files_write", "files_patch"];
154
- const USER_SKILL_TOOLS = ["user_skill_create", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list"];
154
+ const USER_SKILL_TOOLS = ["user_skill_create", "user_skill_update", "user_skill_enable", "user_skill_disable", "user_skill_delete", "user_skill_list", "user_skill_templates", "user_skill_validate", "user_skill_preview"];
155
155
  const YANDEX_TOOLS = [
156
156
  "yandex_identity_me",
157
157
  "yandex_disk_info",
@@ -192,6 +192,7 @@ const YANDEX_TOOLS = [
192
192
  "yandex_mail_city_context",
193
193
  "yandex_mail_map_addresses",
194
194
  "yandex_mail_create_task",
195
+ "yandex_mail_meeting_pack",
195
196
  "yandex_calendar_status",
196
197
  "yandex_calendar_calendars",
197
198
  "yandex_calendar_create_event",
@@ -243,6 +244,12 @@ const YANDEX_TOOLS = [
243
244
  "yandex_contact_from_public_entity",
244
245
  "yandex_telemost_status",
245
246
  "yandex_telemost_create_event",
247
+ "yandex_contact_full_pack",
248
+ "yandex_daily_digest",
249
+ "yandex_calendar_reminders_tick",
250
+ "yandex_disk_maintenance_tick",
251
+ "yandex_cloud_status",
252
+ "yandex_go_deeplink",
246
253
  ];
247
254
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
248
255
  const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
@@ -3065,6 +3072,29 @@ async function handleSkills(args) {
3065
3072
  return;
3066
3073
  }
3067
3074
 
3075
+ if (action === "templates") {
3076
+ printTable(userSkillTemplates().map((row) => ({ name: row.name, description: row.description, tools: row.tools.join(", ") })), [["name", "Шаблон"], ["description", "Описание"], ["tools", "Tools"]]);
3077
+ return;
3078
+ }
3079
+
3080
+ if (action === "preview") {
3081
+ const options = parseOptions(args.slice(2));
3082
+ console.log(buildUserSkillPreview({
3083
+ name,
3084
+ description: options.description || "",
3085
+ instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3086
+ tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3087
+ template: options.template,
3088
+ }));
3089
+ return;
3090
+ }
3091
+
3092
+ if (action === "validate") {
3093
+ const result = await userSkillValidate(name);
3094
+ printTable(result.checks, [["check", "Проверка"], ["status", "Статус"], ["message", "Сообщение"]]);
3095
+ return;
3096
+ }
3097
+
3068
3098
  if (action === "create" || action === "new") {
3069
3099
  const options = parseOptions(args.slice(2));
3070
3100
  const result = await userSkillCreate({
@@ -3072,6 +3102,7 @@ async function handleSkills(args) {
3072
3102
  description: options.description || "",
3073
3103
  instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3074
3104
  tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3105
+ template: options.template,
3075
3106
  enable: Boolean(options.enable),
3076
3107
  overwrite: Boolean(options.force),
3077
3108
  confirm: true,
@@ -3082,6 +3113,20 @@ async function handleSkills(args) {
3082
3113
  return;
3083
3114
  }
3084
3115
 
3116
+ if (action === "update" || action === "edit") {
3117
+ const options = parseOptions(args.slice(2));
3118
+ const result = await userSkillUpdate(name, {
3119
+ description: options.description,
3120
+ instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3121
+ tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3122
+ enable: options.enable,
3123
+ confirm: true,
3124
+ });
3125
+ console.log(`Skill обновлен: ${result.name}`);
3126
+ console.log(`Файл: ${result.file}`);
3127
+ return;
3128
+ }
3129
+
3085
3130
  if (action === "delete" || action === "remove" || action === "rm") {
3086
3131
  const options = parseOptions(args.slice(2));
3087
3132
  const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
@@ -3099,7 +3144,7 @@ async function handleSkills(args) {
3099
3144
  return;
3100
3145
  }
3101
3146
 
3102
- throw new Error("Команды skills: list, paths, show NAME, create NAME --description TEXT --instructions TEXT [--enable], enable NAME, disable NAME, delete NAME --yes, bundles, bundle enable NAME, doctor.");
3147
+ throw new Error("Команды skills: list, paths, show NAME, templates, preview NAME --template T, create NAME --description TEXT --instructions TEXT [--enable], update NAME --instructions TEXT, validate NAME, enable NAME, disable NAME, delete NAME --yes, bundles, bundle enable NAME, doctor.");
3103
3148
  }
3104
3149
 
3105
3150
  async function handleTools(args) {
@@ -3390,11 +3435,36 @@ async function handleYandex(args) {
3390
3435
  return;
3391
3436
  }
3392
3437
 
3438
+ if (action === "cloud" || action === "cloud-connector" || action === "yc") {
3439
+ await handleYandexCloudConnector([target, ...rest].filter(Boolean));
3440
+ return;
3441
+ }
3442
+
3443
+ if (action === "go" || action === "taxi" || action === "такси") {
3444
+ await handleYandexGo([target, ...rest].filter(Boolean));
3445
+ return;
3446
+ }
3447
+
3393
3448
  if (action === "mail-watch" || action === "mailwatch" || action === "watch-mail") {
3394
3449
  await handleYandexMailWatch([target, ...rest].filter(Boolean));
3395
3450
  return;
3396
3451
  }
3397
3452
 
3453
+ if (action === "daily-digest" || action === "digest") {
3454
+ await handleYandexDailyDigest([target, ...rest].filter(Boolean));
3455
+ return;
3456
+ }
3457
+
3458
+ if (action === "calendar-reminders" || action === "calendar-watch" || action === "reminders") {
3459
+ await handleYandexCalendarReminders([target, ...rest].filter(Boolean));
3460
+ return;
3461
+ }
3462
+
3463
+ if (action === "disk-maintenance" || action === "disk-watch" || action === "disk-doctor") {
3464
+ await handleYandexDiskMaintenance([target, ...rest].filter(Boolean));
3465
+ return;
3466
+ }
3467
+
3398
3468
  if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
3399
3469
  await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
3400
3470
  return;
@@ -3435,7 +3505,15 @@ async function handleYandex(args) {
3435
3505
  iola yandex menu
3436
3506
  iola yandex status|doctor
3437
3507
  iola yandex services
3508
+ iola yandex cloud setup|status|doctor|delete
3509
+ iola yandex cloud enable geocoder yandexgpt
3510
+ iola yandex cloud disable yandexgpt
3511
+ iola yandex go link --from "Адрес" --to "Адрес" [--tariff econom]
3512
+ iola yandex go open --from "Адрес" --to "Адрес" [--tariff econom]
3438
3513
  iola yandex mail-watch on|off|status|tick [--minutes 5]
3514
+ iola yandex daily-digest on|off|status|tick [--time 09:00] [--email]
3515
+ iola yandex calendar-reminders on|off|status|tick [--minutes 15]
3516
+ iola yandex disk-maintenance on|off|status|tick [--days 7]
3439
3517
  iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
3440
3518
  iola yandex enable disk mail calendar
3441
3519
  iola yandex disable mail
@@ -3466,6 +3544,332 @@ function printYandexServices(options = {}) {
3466
3544
  ]);
3467
3545
  }
3468
3546
 
3547
+ async function handleYandexCloudConnector(args = []) {
3548
+ const [action = "status", ...rest] = args;
3549
+ const options = parseOptions(rest);
3550
+
3551
+ if (action === "setup" || action === "connect" || action === "onboard") {
3552
+ await setupYandexCloudConnector(options);
3553
+ return;
3554
+ }
3555
+
3556
+ if (action === "status" || action === "doctor" || action === "check") {
3557
+ await printYandexCloudConnectorStatus({ check: action !== "status" || options.check });
3558
+ return;
3559
+ }
3560
+
3561
+ if (action === "enable" || action === "disable") {
3562
+ const services = options._.length ? options._ : rest.filter((item) => item && !String(item).startsWith("--"));
3563
+ await updateYandexCloudEnabledServices(services, action === "enable");
3564
+ return;
3565
+ }
3566
+
3567
+ if (action === "delete" || action === "disconnect" || action === "remove") {
3568
+ const ok = !process.stdin.isTTY || await askYesNo("Удалить локальные ключи и настройки Yandex Cloud Connector? [y/N] ", false);
3569
+ if (!ok) {
3570
+ console.log("Удаление отменено.");
3571
+ return;
3572
+ }
3573
+ await deleteYandexCloudConnector();
3574
+ return;
3575
+ }
3576
+
3577
+ if (action === "open") {
3578
+ await openUrl("https://console.yandex.cloud/");
3579
+ return;
3580
+ }
3581
+
3582
+ throw new Error("Команды: iola yandex cloud setup | status | doctor | enable geocoder yandexgpt | disable yandexgpt | delete");
3583
+ }
3584
+
3585
+ async function setupYandexCloudConnector(options = {}) {
3586
+ console.log("Yandex Cloud Connector: геокодинг и YandexGPT.");
3587
+ console.log("Геокодер будет включен по умолчанию. YandexGPT можно выбрать в /model после сохранения ключей.");
3588
+ if (process.stdin.isTTY) {
3589
+ const openConsole = await askYesNo("Открыть Yandex Cloud Console для получения ключей? [Y/n] ", true);
3590
+ if (openConsole) await openUrl("https://console.yandex.cloud/");
3591
+ }
3592
+
3593
+ const secrets = await loadSecrets();
3594
+ const currentGeocoder = secrets.yandexGeocoder?.apiKey || secrets.yandexCloud?.geocoderApiKey || "";
3595
+ const currentGptKey = secrets.yandexgpt?.apiKey || secrets.yandexCloud?.yandexgptApiKey || "";
3596
+ const currentFolderId = secrets.yandexgpt?.folderId || secrets.yandexCloud?.folderId || "";
3597
+
3598
+ if (!process.stdin.isTTY) {
3599
+ await saveYandexCloudEnabledServices(["geocoder"]);
3600
+ console.log("Интерактивный ввод ключей недоступен. Запустите: iola yandex cloud setup");
3601
+ return;
3602
+ }
3603
+
3604
+ const geocoderKey = (await askText(`YANDEX_GEOCODER_API_KEY${currentGeocoder ? " [уже сохранен, Enter - оставить]" : ""}: `)).trim() || currentGeocoder;
3605
+ if (!geocoderKey) throw new Error("Для Cloud Connector нужен хотя бы Geocoder API key.");
3606
+
3607
+ const setupGpt = await askYesNo(`Настроить YandexGPT сейчас${currentGptKey && currentFolderId ? " (уже сохранен)" : ""}? [y/N] `, Boolean(currentGptKey && currentFolderId));
3608
+ let yandexgptApiKey = currentGptKey;
3609
+ let folderId = currentFolderId;
3610
+ if (setupGpt) {
3611
+ yandexgptApiKey = (await askText(`YANDEXGPT_API_KEY${currentGptKey ? " [Enter - оставить]" : ""}: `)).trim() || currentGptKey;
3612
+ folderId = (await askText(`YANDEXGPT_FOLDER_ID${currentFolderId ? " [Enter - оставить]" : ""}: `)).trim() || currentFolderId;
3613
+ if (!yandexgptApiKey || !folderId) throw new Error("Для YandexGPT нужны API key и folder ID.");
3614
+ }
3615
+
3616
+ await saveYandexCloudConnectorSecrets({ geocoderApiKey: geocoderKey, yandexgptApiKey, folderId });
3617
+ await saveYandexCloudEnabledServices(setupGpt ? ["geocoder", "yandexgpt"] : ["geocoder"]);
3618
+ console.log(`Yandex Cloud Connector сохранен локально: ${SECRETS_FILE}`);
3619
+ await printYandexCloudConnectorStatus({ check: true });
3620
+ }
3621
+
3622
+ async function saveYandexCloudConnectorSecrets({ geocoderApiKey, yandexgptApiKey, folderId }) {
3623
+ const secrets = await loadSecrets();
3624
+ secrets.yandexCloud = {
3625
+ ...(secrets.yandexCloud || {}),
3626
+ geocoderApiKey: geocoderApiKey || secrets.yandexCloud?.geocoderApiKey || "",
3627
+ yandexgptApiKey: yandexgptApiKey || secrets.yandexCloud?.yandexgptApiKey || "",
3628
+ folderId: folderId || secrets.yandexCloud?.folderId || "",
3629
+ updatedAt: new Date().toISOString(),
3630
+ };
3631
+ if (geocoderApiKey) secrets.yandexGeocoder = { ...(secrets.yandexGeocoder || {}), apiKey: geocoderApiKey };
3632
+ if (yandexgptApiKey || folderId) {
3633
+ secrets.yandexgpt = {
3634
+ ...(secrets.yandexgpt || {}),
3635
+ apiKey: yandexgptApiKey || secrets.yandexgpt?.apiKey || "",
3636
+ folderId: folderId || secrets.yandexgpt?.folderId || "",
3637
+ };
3638
+ }
3639
+ await saveSecrets(secrets);
3640
+ }
3641
+
3642
+ async function saveYandexCloudEnabledServices(services) {
3643
+ const config = await loadConfig();
3644
+ const normalized = normalizeYandexCloudServiceList(services);
3645
+ await saveConfig({
3646
+ yandex: {
3647
+ ...(config.yandex || {}),
3648
+ cloudConnector: {
3649
+ ...(config.yandex?.cloudConnector || {}),
3650
+ enabledServices: normalized,
3651
+ updatedAt: new Date().toISOString(),
3652
+ },
3653
+ },
3654
+ });
3655
+ }
3656
+
3657
+ async function updateYandexCloudEnabledServices(rawServices, enabled) {
3658
+ const config = await loadConfig();
3659
+ const current = new Set(config.yandex?.cloudConnector?.enabledServices || ["geocoder"]);
3660
+ for (const service of normalizeYandexCloudServiceList(rawServices)) {
3661
+ if (enabled) current.add(service);
3662
+ else current.delete(service);
3663
+ }
3664
+ if (current.size > 0 && !current.has("geocoder")) current.add("geocoder");
3665
+ await saveYandexCloudEnabledServices([...current]);
3666
+ console.log(`Yandex Cloud services: ${[...current].join(", ") || "-"}`);
3667
+ }
3668
+
3669
+ function normalizeYandexCloudServiceList(services = []) {
3670
+ const aliases = {
3671
+ geo: "geocoder",
3672
+ geocoder: "geocoder",
3673
+ maps: "geocoder",
3674
+ map: "geocoder",
3675
+ "yandex-geocoder": "geocoder",
3676
+ gpt: "yandexgpt",
3677
+ yandexgpt: "yandexgpt",
3678
+ "yandex-gpt": "yandexgpt",
3679
+ model: "yandexgpt",
3680
+ models: "yandexgpt",
3681
+ };
3682
+ return [...new Set([].concat(services || []).map((item) => aliases[String(item || "").toLocaleLowerCase("ru-RU")] || "").filter(Boolean))];
3683
+ }
3684
+
3685
+ async function printYandexCloudConnectorStatus(options = {}) {
3686
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
3687
+ const enabled = new Set(config.yandex?.cloudConnector?.enabledServices || []);
3688
+ const geocoderKey = process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey || "";
3689
+ const gptKey = process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey || "";
3690
+ const folderId = process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId || "";
3691
+ const rows = [
3692
+ { service: "geocoder", enabled: enabled.has("geocoder") ? "yes" : "no", configured: geocoderKey ? "yes" : "no", source: geocoderKey ? "local/env" : "-", hint: "адреса и координаты" },
3693
+ { service: "yandexgpt", enabled: enabled.has("yandexgpt") ? "yes" : "no", configured: gptKey && folderId ? "yes" : "no", source: gptKey && folderId ? "local/env" : "-", hint: "модели YandexGPT" },
3694
+ ];
3695
+ printTable(rows, [
3696
+ ["service", "Сервис"],
3697
+ ["enabled", "Вкл"],
3698
+ ["configured", "Настроен"],
3699
+ ["source", "Ключ"],
3700
+ ["hint", "Суть"],
3701
+ ]);
3702
+ if (options.check) {
3703
+ await checkYandexGeocoderKey({ print: true });
3704
+ if (gptKey && folderId) console.log("YandexGPT: ключ и folder ID найдены.");
3705
+ else console.log("YandexGPT: не настроен.");
3706
+ }
3707
+ }
3708
+
3709
+ async function deleteYandexCloudConnector() {
3710
+ const secrets = await loadSecrets();
3711
+ delete secrets.yandexCloud;
3712
+ delete secrets.yandexGeocoder;
3713
+ delete secrets.yandexgpt;
3714
+ await saveSecrets(secrets);
3715
+ const config = await loadConfig();
3716
+ await saveConfig({
3717
+ yandex: {
3718
+ ...(config.yandex || {}),
3719
+ cloudConnector: { enabledServices: [], updatedAt: new Date().toISOString() },
3720
+ },
3721
+ });
3722
+ console.log("Yandex Cloud Connector удален локально.");
3723
+ }
3724
+
3725
+ async function handleYandexGo(args = []) {
3726
+ const [action = "link", ...rest] = args;
3727
+ const options = parseOptions(rest);
3728
+ if (action === "link" || action === "deeplink" || action === "url") {
3729
+ await ensureYandexGoGeocoderReady();
3730
+ const result = await buildYandexGoDeeplinkFromOptions(options);
3731
+ printYandexGoDeeplinkResult(result);
3732
+ return;
3733
+ }
3734
+ if (action === "open" || action === "prepare" || action === "route") {
3735
+ await ensureYandexGoGeocoderReady();
3736
+ const result = await buildYandexGoDeeplinkFromOptions(options);
3737
+ printYandexGoDeeplinkResult(result);
3738
+ await openUrl(result.url);
3739
+ return;
3740
+ }
3741
+ if (action === "status") {
3742
+ printKeyValue({
3743
+ deeplink: "ready",
3744
+ priceApi: "ожидает clid/apikey от Яндекса",
3745
+ orderApi: "не подключен",
3746
+ });
3747
+ return;
3748
+ }
3749
+ throw new Error('Команды: iola yandex go link --from "Адрес" --to "Адрес" [--tariff econom] | open --from "Адрес" --to "Адрес"');
3750
+ }
3751
+
3752
+ async function ensureYandexGoGeocoderReady() {
3753
+ if (await getYandexGeocoderKey()) return true;
3754
+ throw new Error([
3755
+ "Для Yandex Go deeplink нужен ключ Yandex Geocoder API: адреса нужно превратить в координаты.",
3756
+ "Откройте мастер настройки и запустите Yandex Cloud Connector (геокодинг и YandexGPT):",
3757
+ " iola master",
3758
+ "или напрямую:",
3759
+ " iola yandex cloud setup",
3760
+ "После подключения повторите команду такси.",
3761
+ ].join("\n"));
3762
+ }
3763
+
3764
+ async function buildYandexGoDeeplinkFromOptions(options = {}) {
3765
+ const from = options.from || options._?.[0] || "";
3766
+ const to = options.to || options._?.[1] || "";
3767
+ if (!from || !to) throw new Error('Укажите маршрут: iola yandex go link --from "Медведево, Школьная 15" --to "Медведево, Советская 20"');
3768
+ const fromPoint = await resolveYandexGoPoint(from);
3769
+ const toPoint = await resolveYandexGoPoint(to);
3770
+ const tariff = normalizeYandexGoTariff(options.tariff || options.class || options.level || "econom");
3771
+ return {
3772
+ from,
3773
+ to,
3774
+ fromPoint,
3775
+ toPoint,
3776
+ tariff,
3777
+ url: buildYandexGoDeeplink({
3778
+ fromPoint,
3779
+ toPoint,
3780
+ tariff,
3781
+ ref: options.ref || "iola-cli",
3782
+ lang: options.lang || "ru",
3783
+ }),
3784
+ };
3785
+ }
3786
+
3787
+ async function resolveYandexGoPoint(query) {
3788
+ const parsed = parseLonLat(query);
3789
+ if (parsed) return { lon: parsed.lon, lat: parsed.lat, label: `${parsed.lat}, ${parsed.lon}`, address: "" };
3790
+ const point = await callYandexGeocoder(query);
3791
+ const coords = parseCoordinates(point?.coordinates);
3792
+ if (!Number.isFinite(coords.lat) || !Number.isFinite(coords.lon)) throw new Error(`Не смог получить координаты: ${query}`);
3793
+ return { lon: coords.lon, lat: coords.lat, label: point.name || query, address: point.address || "" };
3794
+ }
3795
+
3796
+ function parseLonLat(value) {
3797
+ const text = String(value || "").trim();
3798
+ const match = text.match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/u);
3799
+ if (!match) return null;
3800
+ const first = Number(match[1]);
3801
+ const second = Number(match[2]);
3802
+ if (!Number.isFinite(first) || !Number.isFinite(second)) return null;
3803
+ if (Math.abs(first) <= 90 && Math.abs(second) > 90) return { lat: first, lon: second };
3804
+ return { lon: first, lat: second };
3805
+ }
3806
+
3807
+ function normalizeYandexGoTariff(value) {
3808
+ const text = String(value || "").toLocaleLowerCase("ru-RU").replace(/\s+/g, "");
3809
+ const aliases = {
3810
+ economy: "econom",
3811
+ econom: "econom",
3812
+ эконом: "econom",
3813
+ comfort: "business",
3814
+ комфорт: "business",
3815
+ business: "business",
3816
+ comfortplus: "comfortplus",
3817
+ "комфорт+": "comfortplus",
3818
+ komfortplus: "comfortplus",
3819
+ minivan: "minivan",
3820
+ минивен: "minivan",
3821
+ vip: "vip",
3822
+ бизнес: "vip",
3823
+ детский: "econom",
3824
+ child: "econom",
3825
+ children: "econom",
3826
+ };
3827
+ return aliases[text] || "econom";
3828
+ }
3829
+
3830
+ function buildYandexGoDeeplink({ fromPoint, toPoint, tariff = "econom", ref = "iola-cli", lang = "ru" }) {
3831
+ const url = new URL("https://3.redirect.appmetrica.yandex.com/route");
3832
+ url.searchParams.set("start-lat", String(fromPoint.lat));
3833
+ url.searchParams.set("start-lon", String(fromPoint.lon));
3834
+ url.searchParams.set("end-lat", String(toPoint.lat));
3835
+ url.searchParams.set("end-lon", String(toPoint.lon));
3836
+ url.searchParams.set("tariffClass", tariff);
3837
+ url.searchParams.set("level", tariff);
3838
+ url.searchParams.set("ref", ref);
3839
+ url.searchParams.set("lang", lang);
3840
+ url.searchParams.set("appmetrica_tracking_id", "1178268795219780156");
3841
+ return url.toString();
3842
+ }
3843
+
3844
+ function printYandexGoDeeplinkResult(result) {
3845
+ console.log(formatYandexGoDeeplinkResult(result));
3846
+ }
3847
+
3848
+ function formatYandexGoDeeplinkResult(result) {
3849
+ return [
3850
+ `Откуда: ${result.fromPoint.address || result.from}`,
3851
+ `Куда: ${result.toPoint.address || result.to}`,
3852
+ `Тариф: ${result.tariff}`,
3853
+ `Ссылка Яндекс Go: ${result.url}`,
3854
+ "Детское кресло и повышенный спрос deeplink не кодирует напрямую; это выбирается/проверяется в интерфейсе Яндекс Go или через taxi_info после получения clid/apikey.",
3855
+ ].join("\n");
3856
+ }
3857
+
3858
+ function extractYandexGoRouteFromText(text) {
3859
+ const source = String(text || "").trim();
3860
+ const tariffMatch = source.match(/(эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*|econom|business|comfortplus|minivan|vip)/iu);
3861
+ const cleaned = source
3862
+ .replace(/^(?:построй|создай|дай|открой|сделай|подготовь|вызови|закажи)\s+/iu, "")
3863
+ .replace(/\b(?:яндекс\s*go|яндекс\s*го|такси|маршрут|ссылк[ау]?|диплинк|deeplink)\b/giu, " ")
3864
+ .replace(/\s+/g, " ")
3865
+ .trim();
3866
+ const match = cleaned.match(/(?:от|из|с)\s+(.+?)\s+(?:до|в|на)\s+(.+)$/iu);
3867
+ if (!match) return { from: "", to: "", tariff: tariffMatch ? normalizeYandexGoTariff(tariffMatch[1]) : "econom" };
3868
+ const from = match[1].replace(/[,.;]\s*$/u, "").trim();
3869
+ const to = match[2].replace(/[,.;]\s*(?:тариф|эконом|комфорт\+?|комфорт плюс|бизнес|минивен|детск\w*).*$/iu, "").trim();
3870
+ return { from, to, tariff: tariffMatch ? normalizeYandexGoTariff(tariffMatch[1]) : "econom" };
3871
+ }
3872
+
3469
3873
  async function handleYandexMailWatch(args = []) {
3470
3874
  const [action = "status", ...rest] = args;
3471
3875
  const options = parseOptions(rest);
@@ -3507,6 +3911,107 @@ async function handleYandexMailWatch(args = []) {
3507
3911
  throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
3508
3912
  }
3509
3913
 
3914
+ async function handleYandexDailyDigest(args = []) {
3915
+ const [action = "status", ...rest] = args;
3916
+ const options = parseOptions(rest);
3917
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3918
+ const time = options.time || rest.find((item) => /^\d{1,2}:\d{2}$/u.test(String(item))) || "09:00";
3919
+ const result = await yandexDailyDigestEnable({ time, email: Boolean(options.email), save: options.save !== false });
3920
+ console.log(`Ежедневный дайджест включен: каждый день ${result.time}. Email: ${result.email ? "yes" : "no"}.`);
3921
+ return;
3922
+ }
3923
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3924
+ await yandexDailyDigestDisable();
3925
+ console.log("Ежедневный дайджест выключен.");
3926
+ return;
3927
+ }
3928
+ if (action === "tick" || action === "run" || action === "check") {
3929
+ const result = await yandexDailyDigestTick({ force: true, email: Boolean(options.email), save: options.save !== false });
3930
+ console.log(result.text || "Дайджест пуст.");
3931
+ if (result.remote) console.log(`Сохранено на Диск: ${result.remote}`);
3932
+ return;
3933
+ }
3934
+ const config = await loadConfig();
3935
+ const digest = config.yandex?.dailyDigest || {};
3936
+ printKeyValue({
3937
+ enabled: digest.enabled ? "yes" : "no",
3938
+ time: digest.time || "-",
3939
+ email: digest.email ? "yes" : "no",
3940
+ lastRunAt: digest.lastRunAt || "-",
3941
+ lastRemote: digest.lastRemote || "-",
3942
+ cron: listCronJobs().some((job) => job.command === "yandex daily-digest tick") ? "yes" : "no",
3943
+ });
3944
+ }
3945
+
3946
+ async function handleYandexCalendarReminders(args = []) {
3947
+ const [action = "status", ...rest] = args;
3948
+ const options = parseOptions(rest);
3949
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3950
+ const minutes = Math.max(1, Number(options.minutes || rest.find((item) => /^\d+$/u.test(String(item))) || 15));
3951
+ const result = await yandexCalendarRemindersEnable(minutes);
3952
+ console.log(`Проверка календарных напоминаний включена: каждые ${result.minutes} минут.`);
3953
+ return;
3954
+ }
3955
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3956
+ await yandexCalendarRemindersDisable();
3957
+ console.log("Проверка календарных напоминаний выключена.");
3958
+ return;
3959
+ }
3960
+ if (action === "tick" || action === "run" || action === "check") {
3961
+ const result = await yandexCalendarRemindersTick({ force: true, horizonMinutes: Number(options.horizon || options.minutes || 60) });
3962
+ if (!result.events.length) console.log("Ближайших событий для напоминания нет.");
3963
+ else console.log(["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n"));
3964
+ return;
3965
+ }
3966
+ const config = await loadConfig();
3967
+ const reminders = config.yandex?.calendarReminders || {};
3968
+ printKeyValue({
3969
+ enabled: reminders.enabled ? "yes" : "no",
3970
+ minutes: reminders.minutes || "-",
3971
+ horizonMinutes: reminders.horizonMinutes || 60,
3972
+ lastRunAt: reminders.lastRunAt || "-",
3973
+ lastCount: reminders.lastCount ?? "-",
3974
+ cron: listCronJobs().some((job) => job.command === "yandex calendar-reminders tick") ? "yes" : "no",
3975
+ });
3976
+ }
3977
+
3978
+ async function handleYandexDiskMaintenance(args = []) {
3979
+ const [action = "status", ...rest] = args;
3980
+ const options = parseOptions(rest);
3981
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3982
+ const days = Math.max(1, Number(options.days || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
3983
+ const result = await yandexDiskMaintenanceEnable(days);
3984
+ console.log(`Проверка Яндекс Диска включена: каждые ${result.days} дней.`);
3985
+ return;
3986
+ }
3987
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3988
+ await yandexDiskMaintenanceDisable();
3989
+ console.log("Проверка Яндекс Диска выключена.");
3990
+ return;
3991
+ }
3992
+ if (action === "tick" || action === "run" || action === "check") {
3993
+ const result = await yandexDiskMaintenanceTick({ force: true });
3994
+ printKeyValue({
3995
+ used: formatBytes(result.usedSpace),
3996
+ total: formatBytes(result.totalSpace),
3997
+ trash: formatBytes(result.trashSize),
3998
+ docs: result.docs,
3999
+ publicLinks: result.publicLinks,
4000
+ remote: result.remote || "-",
4001
+ });
4002
+ return;
4003
+ }
4004
+ const config = await loadConfig();
4005
+ const disk = config.yandex?.diskMaintenance || {};
4006
+ printKeyValue({
4007
+ enabled: disk.enabled ? "yes" : "no",
4008
+ days: disk.days || "-",
4009
+ lastRunAt: disk.lastRunAt || "-",
4010
+ lastRemote: disk.lastRemote || "-",
4011
+ cron: listCronJobs().some((job) => job.command === "yandex disk-maintenance tick") ? "yes" : "no",
4012
+ });
4013
+ }
4014
+
3510
4015
  async function handleYandexContactsMaintenance(args = []) {
3511
4016
  const [action = "status", ...rest] = args;
3512
4017
  const options = parseOptions(rest);
@@ -3615,9 +4120,12 @@ async function chooseYandexServicesMenu() {
3615
4120
  }
3616
4121
  const config = await loadConfig();
3617
4122
  const serviceIds = getYandexConnectorMenuServiceIds();
3618
- const deleteNumber = serviceIds.length + 1;
4123
+ const cloudNumber = serviceIds.length + 1;
4124
+ const goNumber = serviceIds.length + 2;
4125
+ const deleteNumber = serviceIds.length + 3;
3619
4126
  const enabled = new Set(config.yandex?.enabledServices?.length ? config.yandex.enabledServices : ["identity", "disk"]);
3620
4127
  const authState = await getYandexServiceAuthState();
4128
+ const cloudStatus = await getYandexCloudConnectorSummary();
3621
4129
  console.log("Функции Яндекса.");
3622
4130
  console.log("Выберите номера функций через запятую:");
3623
4131
  serviceIds.forEach((id, index) => {
@@ -3627,6 +4135,8 @@ async function chooseYandexServicesMenu() {
3627
4135
  const authLabel = auth?.hasToken ? "подключено" : (auth?.authorized ? "нужен вход" : "нет прав");
3628
4136
  console.log(`${index + 1}. [${marker}] ${service.title} - ${service.hint} (${service.status}, ${authLabel})`);
3629
4137
  });
4138
+ console.log(`${cloudNumber}. Yandex Cloud Connector - геокодинг и YandexGPT (${cloudStatus})`);
4139
+ console.log(`${goNumber}. Yandex Go / Такси - deeplink и маршрут (готово, заказ через API ожидает clid/apikey)`);
3630
4140
  console.log(`${deleteNumber}. Удалить подключение-коннектор`);
3631
4141
  console.log("0. Отмена");
3632
4142
  const defaults = serviceIds.map((id, index) => enabled.has(id) ? String(index + 1) : "").filter(Boolean);
@@ -3646,6 +4156,17 @@ async function chooseYandexServicesMenu() {
3646
4156
  await deleteYandexConnectorToken();
3647
4157
  return;
3648
4158
  }
4159
+ if (selectedNumbers.includes(String(cloudNumber))) {
4160
+ if (selectedNumbers.length > 1) throw new Error("Yandex Cloud Connector выбирается отдельно, без других пунктов.");
4161
+ await chooseYandexCloudConnectorMenu();
4162
+ return;
4163
+ }
4164
+ if (selectedNumbers.includes(String(goNumber))) {
4165
+ if (selectedNumbers.length > 1) throw new Error("Yandex Go выбирается отдельно, без других пунктов.");
4166
+ await handleYandexGo(["status"]);
4167
+ console.log('Для маршрута: iola yandex go open --from "Адрес" --to "Адрес" --tariff econom');
4168
+ return;
4169
+ }
3649
4170
  const selected = selectedNumbers.map((item) => {
3650
4171
  const index = Number(item) - 1;
3651
4172
  if (!Number.isInteger(index) || index < 0 || index >= serviceIds.length) {
@@ -3663,10 +4184,51 @@ async function chooseYandexServicesMenu() {
3663
4184
 
3664
4185
  function getYandexConnectorMenuServiceIds() {
3665
4186
  return Object.entries(YANDEX_CONNECTOR_SERVICES)
3666
- .filter(([, service]) => service.status === "ready" || service.status === "research")
4187
+ .filter(([, service]) => (service.status === "ready" || service.status === "research") && service.scope)
3667
4188
  .map(([id]) => id);
3668
4189
  }
3669
4190
 
4191
+ async function chooseYandexCloudConnectorMenu() {
4192
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
4193
+ const enabled = new Set(config.yandex?.cloudConnector?.enabledServices || ["geocoder"]);
4194
+ const geocoderConfigured = Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey);
4195
+ const gptConfigured = Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
4196
+ && (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId));
4197
+ console.log("Yandex Cloud Connector.");
4198
+ console.log("1. Настроить/обновить ключи");
4199
+ console.log(`2. [${enabled.has("geocoder") ? "✓" : " "}] Геокодер (${geocoderConfigured ? "ключ есть" : "ключ не задан"})`);
4200
+ console.log(`3. [${enabled.has("yandexgpt") ? "✓" : " "}] YandexGPT (${gptConfigured ? "ключ и folder ID есть" : "не настроено"})`);
4201
+ console.log("4. Проверить подключение");
4202
+ console.log("5. Удалить Cloud Connector");
4203
+ console.log("0. Назад");
4204
+ const answer = (await askText("Номер: ")).trim();
4205
+ if (answer === "0" || !answer) return;
4206
+ if (answer === "1") return setupYandexCloudConnector({});
4207
+ if (answer === "2") {
4208
+ if (enabled.has("geocoder")) await updateYandexCloudEnabledServices(["geocoder"], false);
4209
+ else await updateYandexCloudEnabledServices(["geocoder"], true);
4210
+ return;
4211
+ }
4212
+ if (answer === "3") {
4213
+ if (enabled.has("yandexgpt")) await updateYandexCloudEnabledServices(["yandexgpt"], false);
4214
+ else await updateYandexCloudEnabledServices(["yandexgpt"], true);
4215
+ return;
4216
+ }
4217
+ if (answer === "4") return printYandexCloudConnectorStatus({ check: true });
4218
+ if (answer === "5") return handleYandexCloudConnector(["delete"]);
4219
+ }
4220
+
4221
+ async function getYandexCloudConnectorSummary() {
4222
+ const [config, secrets] = await Promise.all([loadConfig(), loadSecrets()]);
4223
+ const enabled = config.yandex?.cloudConnector?.enabledServices || [];
4224
+ const geocoder = Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey);
4225
+ const gpt = Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
4226
+ && (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId));
4227
+ if (geocoder && gpt) return `готово: ${enabled.join(", ") || "geocoder"}`;
4228
+ if (geocoder) return "частично: geocoder";
4229
+ return "не настроено";
4230
+ }
4231
+
3670
4232
  async function updateYandexEnabledServices(rawServices, enabled) {
3671
4233
  const config = await loadConfig();
3672
4234
  const current = new Set(config.yandex?.enabledServices || []);
@@ -4164,6 +4726,7 @@ async function executeYandexTool(tool, args = {}) {
4164
4726
  if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
4165
4727
  if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
4166
4728
  if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
4729
+ if (tool === "yandex_mail_meeting_pack") return yandexMailMeetingPack(args.uid || args.id, args);
4167
4730
  if (tool === "yandex_calendar_status") return yandexCalendarStatus();
4168
4731
  if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
4169
4732
  if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
@@ -4215,6 +4778,23 @@ async function executeYandexTool(tool, args = {}) {
4215
4778
  if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
4216
4779
  if (tool === "yandex_telemost_status") return yandexTelemostStatus();
4217
4780
  if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
4781
+ if (tool === "yandex_contact_full_pack") return yandexContactFullPack(args);
4782
+ if (tool === "yandex_daily_digest") return yandexDailyDigestTick({ ...args, force: true });
4783
+ if (tool === "yandex_calendar_reminders_tick") return yandexCalendarRemindersTick({ ...args, force: true });
4784
+ if (tool === "yandex_disk_maintenance_tick") return yandexDiskMaintenanceTick({ ...args, force: true });
4785
+ if (tool === "yandex_cloud_status") {
4786
+ const [secrets, config] = await Promise.all([loadSecrets(), loadConfig()]);
4787
+ return {
4788
+ geocoder: Boolean(process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY || secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey),
4789
+ yandexgpt: Boolean((process.env.YANDEXGPT_API_KEY || process.env.YANDEX_CLOUD_API_KEY || secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey)
4790
+ && (process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID || secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId)),
4791
+ enabled: config.yandex?.cloudConnector?.enabledServices || [],
4792
+ };
4793
+ }
4794
+ if (tool === "yandex_go_deeplink") {
4795
+ await ensureYandexGoGeocoderReady();
4796
+ return buildYandexGoDeeplinkFromOptions({ from: args.from, to: args.to, tariff: args.tariff || args.class || args.level, ref: args.ref, lang: args.lang });
4797
+ }
4218
4798
  throw new Error(`Yandex tool неизвестен: ${tool}`);
4219
4799
  }
4220
4800
 
@@ -4385,6 +4965,142 @@ async function yandexMailWatchDisable() {
4385
4965
  return { enabled: false };
4386
4966
  }
4387
4967
 
4968
+ async function yandexDailyDigestEnable(options = {}) {
4969
+ const config = await loadConfig();
4970
+ const time = normalizeDigestTime(options.time || "09:00");
4971
+ await saveConfig({
4972
+ yandex: {
4973
+ ...(config.yandex || {}),
4974
+ dailyDigest: { ...(config.yandex?.dailyDigest || {}), enabled: true, time, email: Boolean(options.email), save: options.save !== false, updatedAt: new Date().toISOString() },
4975
+ },
4976
+ });
4977
+ await upsertCronJob(`каждый день ${time}`, "yandex daily-digest tick", { replaceCommand: true });
4978
+ return { enabled: true, time, email: Boolean(options.email), save: options.save !== false };
4979
+ }
4980
+
4981
+ async function yandexDailyDigestDisable() {
4982
+ const config = await loadConfig();
4983
+ const current = config.yandex?.dailyDigest || {};
4984
+ await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
4985
+ deleteCronJobsByCommand("yandex daily-digest tick");
4986
+ return { enabled: false };
4987
+ }
4988
+
4989
+ async function yandexDailyDigestTick(options = {}) {
4990
+ const config = await loadConfig();
4991
+ const digest = config.yandex?.dailyDigest || {};
4992
+ if (!digest.enabled && !options.force) return { enabled: false, text: "" };
4993
+ const unread = await yandexMailList({ mailbox: "INBOX", limit: 10, unread: true }).catch(() => []);
4994
+ const events = await yandexCalendarList({ start: new Date().toISOString(), end: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), limit: 20 }).catch(() => []);
4995
+ const incomplete = await yandexContactsFindIncomplete({ limit: 10 }).catch(() => []);
4996
+ const text = [
4997
+ `# Дайджест IOLA за ${new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(new Date())}`,
4998
+ "",
4999
+ `Непрочитанных писем: ${unread.length}`,
5000
+ ...unread.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`),
5001
+ "",
5002
+ `Событий на 24 часа: ${events.length}`,
5003
+ ...events.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}`),
5004
+ "",
5005
+ `Неполных контактов: ${incomplete.length}`,
5006
+ ...incomplete.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexContact(row)}`),
5007
+ ].join("\n").trim();
5008
+ let remote = "";
5009
+ if (options.save !== false && digest.save !== false) {
5010
+ const saved = await yandexDocsCreateText({ title: `daily-digest-${timestampForFile()}`, text, format: "md", confirm: true });
5011
+ remote = saved.remote || "";
5012
+ }
5013
+ if (options.email || digest.email) {
5014
+ const profile = await getYandexIdentityProfile();
5015
+ const to = profile.defaultEmail || profile.emails?.[0];
5016
+ if (to) await yandexMailSend({ to: [to], subject: "Ежедневный дайджест IOLA", text, confirm: true });
5017
+ }
5018
+ await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...digest, enabled: digest.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: remote || digest.lastRemote || "" } } });
5019
+ return { enabled: true, text, remote, unread: unread.length, events: events.length, incomplete: incomplete.length };
5020
+ }
5021
+
5022
+ async function yandexCalendarRemindersEnable(minutes = 15) {
5023
+ const config = await loadConfig();
5024
+ const safeMinutes = Math.max(1, Number(minutes || 15));
5025
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...(config.yandex?.calendarReminders || {}), enabled: true, minutes: safeMinutes, horizonMinutes: 60, updatedAt: new Date().toISOString() } } });
5026
+ await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex calendar-reminders tick", { replaceCommand: true });
5027
+ return { enabled: true, minutes: safeMinutes };
5028
+ }
5029
+
5030
+ async function yandexCalendarRemindersDisable() {
5031
+ const config = await loadConfig();
5032
+ const current = config.yandex?.calendarReminders || {};
5033
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...current, enabled: false, seen: [], updatedAt: new Date().toISOString() } } });
5034
+ deleteCronJobsByCommand("yandex calendar-reminders tick");
5035
+ return { enabled: false };
5036
+ }
5037
+
5038
+ async function yandexCalendarRemindersTick(options = {}) {
5039
+ const config = await loadConfig();
5040
+ const reminders = config.yandex?.calendarReminders || {};
5041
+ if (!reminders.enabled && !options.force) return { enabled: false, events: [] };
5042
+ const horizon = Math.max(5, Number(options.horizonMinutes || reminders.horizonMinutes || 60));
5043
+ const now = new Date();
5044
+ const rows = await yandexCalendarList({ start: now.toISOString(), end: new Date(now.getTime() + horizon * 60 * 1000).toISOString(), limit: 30 });
5045
+ const seen = new Set(reminders.seen || []);
5046
+ const fresh = rows.filter((row) => {
5047
+ const key = `${row.uid}:${row.start}`;
5048
+ if (seen.has(key)) return false;
5049
+ seen.add(key);
5050
+ return true;
5051
+ });
5052
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...reminders, enabled: reminders.enabled !== false, lastRunAt: new Date().toISOString(), lastCount: fresh.length, seen: [...seen].slice(-500) } } });
5053
+ return { enabled: true, events: fresh };
5054
+ }
5055
+
5056
+ async function yandexDiskMaintenanceEnable(days = 7) {
5057
+ const config = await loadConfig();
5058
+ const safeDays = Math.max(1, Number(days || 7));
5059
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...(config.yandex?.diskMaintenance || {}), enabled: true, days: safeDays, updatedAt: new Date().toISOString() } } });
5060
+ await upsertCronJob(`каждые ${safeDays} дней`, "yandex disk-maintenance tick", { replaceCommand: true });
5061
+ return { enabled: true, days: safeDays };
5062
+ }
5063
+
5064
+ async function yandexDiskMaintenanceDisable() {
5065
+ const config = await loadConfig();
5066
+ const current = config.yandex?.diskMaintenance || {};
5067
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
5068
+ deleteCronJobsByCommand("yandex disk-maintenance tick");
5069
+ return { enabled: false };
5070
+ }
5071
+
5072
+ async function yandexDiskMaintenanceTick(options = {}) {
5073
+ const config = await loadConfig();
5074
+ const disk = config.yandex?.diskMaintenance || {};
5075
+ if (!disk.enabled && !options.force) return { enabled: false };
5076
+ const info = await yandexDiskInfo();
5077
+ const docs = await yandexDocsList({ limit: 500 }).catch(() => []);
5078
+ const all = await yandexDiskListRecursive(CLOUD_DEFAULT_REMOTE_DIR, { depth: 4, limit: 500 }).catch(() => []);
5079
+ const publicLinks = [];
5080
+ for (const row of all.slice(0, 100)) {
5081
+ const statRow = await yandexDiskStat(row.path).catch(() => null);
5082
+ if (statRow?.publicUrl) publicLinks.push(statRow);
5083
+ }
5084
+ const text = [
5085
+ `# Проверка Яндекс Диска ${new Date().toISOString()}`,
5086
+ "",
5087
+ `Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}`,
5088
+ `Корзина: ${formatBytes(info.trashSize)}`,
5089
+ `Документов: ${docs.length}`,
5090
+ `Публичных ссылок в /IOLA: ${publicLinks.length}`,
5091
+ ...publicLinks.slice(0, 20).map((row, index) => `${index + 1}. ${row.path} — ${row.publicUrl}`),
5092
+ ].join("\n");
5093
+ const saved = await yandexDocsCreateText({ title: `disk-maintenance-${timestampForFile()}`, text, format: "md", confirm: true });
5094
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...disk, enabled: disk.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: saved.remote || "", lastDocs: docs.length, lastPublicLinks: publicLinks.length } } });
5095
+ return { enabled: true, ...info, docs: docs.length, publicLinks: publicLinks.length, remote: saved.remote || "" };
5096
+ }
5097
+
5098
+ function normalizeDigestTime(value) {
5099
+ const match = String(value || "").match(/^(\d{1,2})(?::(\d{2}))?$/u);
5100
+ if (!match) return "09:00";
5101
+ return `${String(Math.min(23, Number(match[1]))).padStart(2, "0")}:${String(Math.min(59, Number(match[2] || 0))).padStart(2, "0")}`;
5102
+ }
5103
+
4388
5104
  async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
4389
5105
  const config = await loadConfig();
4390
5106
  const safeDays = Math.max(1, Number(days || 7));
@@ -4588,6 +5304,47 @@ async function yandexMailCreateCalendarEvent(uid, args = {}) {
4588
5304
  });
4589
5305
  }
4590
5306
 
5307
+ async function yandexMailMeetingPack(uid, args = {}) {
5308
+ if (!args.confirm) throw new Error("Для пакетного сценария по письму нужен аргумент confirm=true.");
5309
+ if (!uid) throw new Error("UID письма обязателен.");
5310
+ const mailbox = await resolveYandexMailbox(args.mailbox || args.folder || "INBOX");
5311
+ const row = await yandexMailRead(uid, { mailbox, markSeen: true });
5312
+ if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
5313
+ const senderEmail = extractEmailAddress(row.from);
5314
+ const saved = await yandexMailSaveToDisk(uid, { mailbox });
5315
+ const shared = await yandexDiskShareWithQr(saved.remote, { confirm: true });
5316
+ const detected = extractDateTimeFromText(`${args.text || ""}\n${row.subject}\n${row.snippet}`);
5317
+ const start = args.start || detected.start || new Date(Date.now() + 3600000).toISOString();
5318
+ const end = args.end || detected.end || new Date(new Date(start).getTime() + 3600000).toISOString();
5319
+ const event = await yandexCalendarCreateEvent({
5320
+ title: args.title || `Встреча по письму: ${row.subject || `#${uid}`}`,
5321
+ description: [
5322
+ `Создано из письма #${uid}.`,
5323
+ `От: ${row.from || "-"}`,
5324
+ `Письмо сохранено: ${saved.remote}`,
5325
+ `Ссылка: ${shared.publicUrl}`,
5326
+ `QR-код: ${shared.qrPublicUrl}`,
5327
+ "",
5328
+ row.snippet || "",
5329
+ ].join("\n"),
5330
+ location: args.location || detected.location || "",
5331
+ start,
5332
+ end,
5333
+ attendees: senderEmail ? [senderEmail] : [],
5334
+ confirm: true,
5335
+ });
5336
+ let sent = null;
5337
+ if ((args.send || args.email) && senderEmail) {
5338
+ sent = await yandexMailSend({
5339
+ to: [senderEmail],
5340
+ subject: args.subject || `Материалы к встрече: ${row.subject || `письмо #${uid}`}`,
5341
+ text: [`Создал встречу и сохранил письмо на Яндекс Диск.`, `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
5342
+ confirm: true,
5343
+ });
5344
+ }
5345
+ return { status: "mail-meeting-pack-created", uid: Number(uid), from: row.from, saved: saved.remote, publicUrl: shared.publicUrl, qrPublicUrl: shared.qrPublicUrl, event: event.uid, sentTo: sent?.to || [] };
5346
+ }
5347
+
4591
5348
  async function yandexMailSenderToContact(uid, args = {}) {
4592
5349
  if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
4593
5350
  if (!uid) throw new Error("UID письма обязателен.");
@@ -5645,6 +6402,47 @@ async function yandexContactFromPublicEntity(args = {}) {
5645
6402
  });
5646
6403
  }
5647
6404
 
6405
+ async function yandexContactFullPack(args = {}) {
6406
+ if (!args.confirm) throw new Error("Для полного сценария по контакту нужен аргумент confirm=true.");
6407
+ const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
6408
+ if (resolved.status !== "ok") return resolved;
6409
+ const contact = resolved.contact;
6410
+ const folder = await yandexContactCreateDiskFolder({ query: contact.email || contact.name || contact.phone, confirm: true, selectFirst: true });
6411
+ const noteText = [
6412
+ `# ${contact.name || contact.email || contact.phone}`,
6413
+ "",
6414
+ `Email: ${contact.email || "-"}`,
6415
+ `Телефон: ${contact.phone || "-"}`,
6416
+ `Адрес: ${contact.address || "-"}`,
6417
+ `Организация: ${contact.org || "-"}`,
6418
+ "",
6419
+ args.note || args.text || "Пакет контакта создан IOLA CLI.",
6420
+ ].join("\n");
6421
+ const doc = await yandexDocsCreateText({ path: path.posix.join(folder.remote, "meeting-note.md"), text: noteText, confirm: true });
6422
+ const shared = await yandexDiskShareWithQr(folder.remote, { confirm: true });
6423
+ const dateTime = args.start || args.date ? { start: args.start || args.date, end: args.end } : extractDateTimeFromText(args.text || args.source_question || "");
6424
+ let event = null;
6425
+ if (args.createEvent !== false && contact.email) {
6426
+ event = await yandexCalendarCreateEvent({
6427
+ ...dateTime,
6428
+ title: args.title || `Встреча: ${contact.name || contact.email}`,
6429
+ description: [`Папка контакта: ${folder.remote}`, `Ссылка: ${shared.publicUrl}`, args.note || ""].filter(Boolean).join("\n"),
6430
+ attendees: [contact.email],
6431
+ confirm: true,
6432
+ });
6433
+ }
6434
+ let sent = null;
6435
+ if ((args.send || args.email) && contact.email) {
6436
+ sent = await yandexMailSend({
6437
+ to: [contact.email],
6438
+ subject: args.subject || `Материалы IOLA: ${contact.name || contact.email}`,
6439
+ text: [args.message || "Подготовил материалы.", `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
6440
+ confirm: true,
6441
+ });
6442
+ }
6443
+ return { status: "contact-full-pack-created", contact: contact.name || contact.email, folder: folder.remote, doc: doc.remote, publicUrl: shared.publicUrl, qrPublicUrl: shared.qrPublicUrl, event: event?.uid || "", sentTo: sent?.to || [] };
6444
+ }
6445
+
5648
6446
  async function resolveYandexContact(query, args = {}) {
5649
6447
  const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
5650
6448
  const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
@@ -9147,6 +9945,17 @@ async function chooseOpenRouterModel() {
9147
9945
  async function ensureApiKeyForModelSelection(provider) {
9148
9946
  if (!["openai", "openrouter", "yandexgpt", "gigachat"].includes(provider)) return true;
9149
9947
  if (await getApiKey(provider) && (provider !== "yandexgpt" || await getYandexFolderId())) return true;
9948
+ if (provider === "yandexgpt") {
9949
+ console.log("YandexGPT требует Yandex Cloud Connector: API key и folder ID.");
9950
+ if (!process.stdin.isTTY) return false;
9951
+ const ok = await askYesNo("Включить Yandex Cloud Connector сейчас? [y/N] ", false);
9952
+ if (!ok) {
9953
+ console.log("Возврат в меню выбора модели.");
9954
+ return false;
9955
+ }
9956
+ await setupYandexCloudConnector({});
9957
+ return Boolean(await getApiKey("yandexgpt") && await getYandexFolderId());
9958
+ }
9150
9959
  const label = {
9151
9960
  openai: "OpenAI",
9152
9961
  openrouter: "OpenRouter",
@@ -9983,7 +10792,7 @@ async function getYandexGeocoderKey() {
9983
10792
  return process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY;
9984
10793
  }
9985
10794
  const secrets = await loadSecrets();
9986
- return secrets.yandexGeocoder?.apiKey || "";
10795
+ return secrets.yandexCloud?.geocoderApiKey || secrets.yandexGeocoder?.apiKey || "";
9987
10796
  }
9988
10797
 
9989
10798
  function openDatabase() {
@@ -11084,6 +11893,13 @@ function isCronDue(job) {
11084
11893
  return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
11085
11894
  }
11086
11895
  if (normalized.includes("каждый день") || normalized.includes("daily")) {
11896
+ const time = normalized.match(/(\d{1,2}):(\d{2})/u);
11897
+ if (time) {
11898
+ const dueAt = new Date(now);
11899
+ dueAt.setHours(Number(time[1]), Number(time[2]), 0, 0);
11900
+ const ranToday = lastRun && lastRun.toISOString().slice(0, 10) === now.toISOString().slice(0, 10);
11901
+ return now >= dueAt && !ranToday;
11902
+ }
11087
11903
  return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
11088
11904
  }
11089
11905
  if (normalized.includes("каждую неделю") || normalized.includes("weekly")) {
@@ -11635,6 +12451,60 @@ async function buildYandexDirectAnswer(question, history = []) {
11635
12451
  ].join("\n");
11636
12452
  }
11637
12453
 
12454
+ if (/(яндекс\s*go|яндекс\s*го|такси|deeplink|диплинк|ссылк.*маршрут)/iu.test(normalized)
12455
+ && /(маршрут|ссылк|откуда|куда|поездк|такси|от\s+.+\s+до\s+)/iu.test(normalized)) {
12456
+ const route = extractYandexGoRouteFromText(question);
12457
+ if (!route.from || !route.to) {
12458
+ return 'Для ссылки Яндекс Go нужны два адреса. Пример: "такси от Медведево, Школьная 15 до Медведево, Советская 20".';
12459
+ }
12460
+ await ensureYandexGoGeocoderReady();
12461
+ const result = await buildYandexGoDeeplinkFromOptions({ from: route.from, to: route.to, tariff: route.tariff });
12462
+ return formatYandexGoDeeplinkResult(result);
12463
+ }
12464
+
12465
+ if (/(дайджест|сводк)/iu.test(normalized) && /(яндекс|почт|календар|контакт|диск)/iu.test(normalized)) {
12466
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized) && /(кажд|ежеднев|авто|регуляр)/iu.test(normalized)) {
12467
+ const time = question.match(/(\d{1,2}:\d{2})/u)?.[1] || "09:00";
12468
+ const result = await yandexDailyDigestEnable({ time, email: /email|почт[уы]|письм/iu.test(normalized), save: true });
12469
+ return `Ежедневный дайджест включен: каждый день ${result.time}.`;
12470
+ }
12471
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12472
+ await yandexDailyDigestDisable();
12473
+ return "Ежедневный дайджест выключен.";
12474
+ }
12475
+ const result = await yandexDailyDigestTick({ force: true, save: true, email: /отправь|email|почт[уы]/iu.test(normalized) });
12476
+ return [result.text, result.remote ? `\nСохранено: ${result.remote}` : ""].join("").trim();
12477
+ }
12478
+
12479
+ if (/(календар|событи|встреч)/iu.test(normalized) && /(напомин|уведом|следи|монитор|авто|регуляр)/iu.test(normalized)) {
12480
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
12481
+ const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
12482
+ await yandexCalendarRemindersEnable(minutes);
12483
+ return `Проверка календарных напоминаний включена: каждые ${minutes} минут.`;
12484
+ }
12485
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12486
+ await yandexCalendarRemindersDisable();
12487
+ return "Проверка календарных напоминаний выключена.";
12488
+ }
12489
+ const result = await yandexCalendarRemindersTick({ force: true });
12490
+ if (!result.events.length) return "Ближайших событий для напоминания нет.";
12491
+ return ["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n");
12492
+ }
12493
+
12494
+ if (/(диск|яндекс.?диск|документ)/iu.test(normalized) && /(провер|обслуж|maintenance|аудит|регуляр|авто)/iu.test(normalized)) {
12495
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
12496
+ const days = Number(question.match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
12497
+ await yandexDiskMaintenanceEnable(days);
12498
+ return `Проверка Яндекс Диска включена: каждые ${days} дней.`;
12499
+ }
12500
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12501
+ await yandexDiskMaintenanceDisable();
12502
+ return "Проверка Яндекс Диска выключена.";
12503
+ }
12504
+ const result = await yandexDiskMaintenanceTick({ force: true });
12505
+ return `Проверка Яндекс Диска выполнена. Документов: ${result.docs}, публичных ссылок: ${result.publicLinks}. Отчет: ${result.remote}.`;
12506
+ }
12507
+
11638
12508
  if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
11639
12509
  return await buildYandexContactsDirectAnswer(question, normalized);
11640
12510
  }
@@ -11682,6 +12552,12 @@ async function buildYandexDirectAnswer(question, history = []) {
11682
12552
  const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
11683
12553
  return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
11684
12554
  }
12555
+ if (/(пакет|комплект|встреч).{0,80}(письм|письма)|(?:письм|письма).{0,80}(пакет|комплект|встреч)/iu.test(normalized) && /(диск|ссылк|qr|календар|встреч)/iu.test(normalized)) {
12556
+ const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
12557
+ if (!uid) return "Из какого письма создать пакет? Укажите номер из списка или UID.";
12558
+ const result = await yandexMailMeetingPack(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", ...extractDateTimeFromText(question), confirm: true });
12559
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12560
+ }
11685
12561
  if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
11686
12562
  const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
11687
12563
  if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
@@ -11983,7 +12859,9 @@ async function buildYandexDirectAnswer(question, history = []) {
11983
12859
  return ["Яндекс Контакты:", ...rows.map((row, index) => `${index + 1}. ${formatYandexContact(row)}`)].join("\n");
11984
12860
  }
11985
12861
  } catch (error) {
11986
- return `Не смог выполнить запрос к сервисам Яндекса: ${error instanceof Error ? error.message : String(error)}`;
12862
+ const message = error instanceof Error ? error.message : String(error);
12863
+ if (/^(?:Для Yandex Go deeplink|Для Cloud Connector нужен|Для YandexGPT нужны)/u.test(message)) return message;
12864
+ return `Не смог выполнить запрос к сервисам Яндекса: ${message}`;
11987
12865
  }
11988
12866
  return "";
11989
12867
  }
@@ -12122,6 +13000,11 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
12122
13000
  const result = await yandexContactCreateDiskFolder({ query, confirm: true });
12123
13001
  return formatToolResult({ rows: [result], outputs: [] }, {});
12124
13002
  }
13003
+ if (/(полный|комплект|пакет|подготовь).{0,80}(контакт|клиент|человек)/iu.test(text) || /(контакт|клиент|человек).{0,80}(полный|комплект|пакет)/iu.test(text)) {
13004
+ const query = cleanupYandexContactActionQuery(question);
13005
+ const result = await yandexContactFullPack({ query, ...extractDateTimeFromText(question), note: extractShareMessage(question), confirm: true });
13006
+ return formatToolResult({ rows: [result], outputs: [] }, {});
13007
+ }
12125
13008
  if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
12126
13009
  const remotePath = extractCloudPath(question);
12127
13010
  const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
@@ -12156,6 +13039,27 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
12156
13039
  async function buildUserSkillDirectAnswer(question) {
12157
13040
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
12158
13041
  if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
13042
+ if (/(шаблон|template|вариант)/iu.test(normalized) && /(покажи|список|какие|list)/iu.test(normalized)) {
13043
+ const rows = userSkillTemplates();
13044
+ return ["Шаблоны skills:", ...rows.map((row) => `- ${row.name}: ${row.description}`)].join("\n");
13045
+ }
13046
+ if (/(preview|предпросмотр|покажи\s+как)/iu.test(normalized)) {
13047
+ const name = extractUserSkillNameFromQuestion(question) || "user-skill";
13048
+ const template = normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || "");
13049
+ return buildUserSkillPreview({ name, template, instructions: question });
13050
+ }
13051
+ if (/(проверь|validate|doctor|валидац)/iu.test(normalized)) {
13052
+ const name = extractUserSkillNameFromQuestion(question);
13053
+ if (!name) return "Какой skill проверить? Укажите имя.";
13054
+ const result = await userSkillValidate(name);
13055
+ return ["Проверка skill:", ...result.checks.map((row) => `${row.status}: ${row.check} - ${row.message}`)].join("\n");
13056
+ }
13057
+ if (/(обнови|измени|update|edit)/iu.test(normalized)) {
13058
+ const name = extractUserSkillNameFromQuestion(question);
13059
+ if (!name) return "Какой skill обновить? Укажите имя.";
13060
+ const result = await userSkillUpdate(name, { instructions: question, tools: inferUserSkillTools(question), confirm: true });
13061
+ return `Skill обновлен: ${result.name}\nФайл: ${result.file}`;
13062
+ }
12159
13063
  if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
12160
13064
  const name = extractUserSkillNameFromQuestion(question) || "user-skill";
12161
13065
  const result = await userSkillCreate({
@@ -12163,6 +13067,7 @@ async function buildUserSkillDirectAnswer(question) {
12163
13067
  description: extractUserSkillDescription(question, name),
12164
13068
  instructions: question,
12165
13069
  tools: inferUserSkillTools(question),
13070
+ template: normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || ""),
12166
13071
  enable: true,
12167
13072
  confirm: true,
12168
13073
  });
@@ -12195,7 +13100,7 @@ async function buildUserSkillDirectAnswer(question) {
12195
13100
  }
12196
13101
 
12197
13102
  function isYandexServiceQuestion(normalized) {
12198
- return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
13103
+ return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин|такси|яндекс\s*go|яндекс\s*го|геокод|cloud|клауд)/iu.test(String(normalized || ""));
12199
13104
  }
12200
13105
 
12201
13106
  function isYandexIdentityQuestion(normalized) {
@@ -13415,9 +14320,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
13415
14320
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
13416
14321
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
13417
14322
  "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
13418
- "Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}.",
13419
- "Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
13420
- "User skill tools: user_skill_create {name,description,instructions,tools,enable,confirm}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай skill только по явной просьбе пользователя и с confirm=true.",
14323
+ "Yandex tools: yandex_identity_me {}, yandex_disk_info {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_stat {path}, yandex_disk_exists {path}, yandex_disk_read_text {path}, yandex_disk_save_text {path,text}, yandex_disk_upload {localPath,remotePath}, yandex_disk_download {remotePath,outputPath}, yandex_disk_move {from,to,confirm}, yandex_disk_copy {from,to,confirm}, yandex_disk_rename {path,name,confirm}, yandex_disk_share {path,confirm}, yandex_disk_share_qr {path,confirm}, yandex_disk_share_email {path,to,contact,subject,text,confirm}, yandex_disk_package_share_email {sourcePath,targetFolder,to,contact,mode,confirm}, yandex_disk_unshare {path}, yandex_disk_delete {path,confirm}, yandex_disk_trash_list {}, yandex_disk_restore {path,confirm}, yandex_disk_empty_trash {confirm}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_mail_send {to,subject,text,confirm}, yandex_mail_reply {uid,text,confirm}, yandex_mail_forward {uid,to,confirm}, yandex_mail_save_to_disk {uid,path}, yandex_mail_city_context {uid}, yandex_mail_map_addresses {uid}, yandex_mail_create_task {uid,title}, yandex_mail_meeting_pack {uid,start,end,send,confirm}, yandex_calendar_calendars {}, yandex_calendar_list {start,end}, yandex_calendar_search {query,start,end}, yandex_calendar_get {query}, yandex_calendar_create_event {title,start,end,location,attendees,reminders,confirm}, yandex_calendar_update {query,title,start,end,location,description,reminders,confirm}, yandex_calendar_move {query,start,end,confirm}, yandex_calendar_delete {query,confirm}, yandex_docs_list {path}, yandex_docs_find {query}, yandex_docs_create_text {title,text,format,confirm}, yandex_docs_read {path|query}, yandex_docs_share {path|query,confirm}, yandex_docs_rename {path|query,name,confirm}, yandex_docs_delete {path|query,confirm}, yandex_contacts_list {limit}, yandex_contacts_search {query}, yandex_contacts_get {query}, yandex_contacts_create {name,email,phone,address,note,confirm}, yandex_contacts_update {query,email,phone,address,note,birthday,org,title,confirm}, yandex_contacts_delete {query,confirm}, yandex_contacts_export_csv {}, yandex_contacts_find_incomplete {}, yandex_contacts_find_duplicates {}, yandex_contacts_backup_to_disk {format,confirm}, yandex_contact_send_mail {contact,subject,text,confirm}, yandex_contact_send_disk_link_qr {contact,path,confirm}, yandex_contact_create_disk_folder {contact,confirm}, yandex_contact_create_calendar_event {contact,start,end,title,confirm}, yandex_contact_create_telemost_event {contact,start,end,title,confirm}, yandex_contact_full_pack {contact,start,end,send,confirm}, yandex_cloud_status {}, yandex_go_deeplink {from,to,tariff}, yandex_daily_digest {save,email}, yandex_calendar_reminders_tick {}, yandex_disk_maintenance_tick {}.",
14324
+ "Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_share_qr, yandex_disk_share_email, yandex_disk_package_share_email, yandex_disk_delete, yandex_disk_move, yandex_disk_copy, yandex_disk_rename, yandex_disk_restore, yandex_disk_empty_trash, yandex_mail_send, yandex_mail_reply, yandex_mail_forward, yandex_mail_delete, yandex_mail_create_calendar_event, yandex_mail_sender_to_contact, yandex_mail_meeting_pack, yandex_contacts_create, yandex_contacts_update, yandex_contacts_delete, yandex_contacts_add_email, yandex_contacts_add_phone, yandex_contacts_add_address, yandex_contacts_backup_to_disk, yandex_contact_send_mail, yandex_contact_send_disk_link_qr, yandex_contact_create_disk_folder, yandex_contact_create_calendar_event, yandex_contact_create_telemost_event, yandex_contact_full_pack, yandex_calendar_create_event, yandex_calendar_update, yandex_calendar_move, yandex_calendar_delete, yandex_calendar_add_reminder, yandex_docs_create_text, yandex_docs_share, yandex_docs_rename, yandex_docs_delete, yandex_telemost_create_event.",
14325
+ "User skill tools: user_skill_create {name,description,instructions,tools,template,enable,confirm}, user_skill_update {name,instructions,tools,confirm}, user_skill_templates {}, user_skill_validate {name}, user_skill_preview {name,template,instructions}, user_skill_enable {name}, user_skill_disable {name}, user_skill_delete {name,confirm}, user_skill_list {}. Создавай или меняй skill только по явной просьбе пользователя и с confirm=true.",
13421
14326
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
13422
14327
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
13423
14328
  `Вопрос: ${question}`,
@@ -13497,6 +14402,12 @@ function inferToolPlan(question, options = {}) {
13497
14402
  }],
13498
14403
  };
13499
14404
  }
14405
+ if (/(шаблон|template)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
14406
+ return { steps: [{ tool: "user_skill_templates", args: {} }] };
14407
+ }
14408
+ if (/(проверь|validate|doctor)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
14409
+ return { steps: [{ tool: "user_skill_validate", args: { name: extractUserSkillNameFromQuestion(question) } }] };
14410
+ }
13500
14411
  if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
13501
14412
  return { steps: [{ tool: "yandex_identity_me", args: {} }] };
13502
14413
  }
@@ -13527,6 +14438,7 @@ function inferToolPlan(question, options = {}) {
13527
14438
  if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
13528
14439
  if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
13529
14440
  if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_mark", args: { uid, mailbox, seen: !/непрочитан/iu.test(normalized) } }] };
14441
+ if (/(пакет|комплект)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_meeting_pack", args: { uid, mailbox, ...extractDateTimeFromText(question), confirm: true } }] };
13530
14442
  if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
13531
14443
  if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
13532
14444
  return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
@@ -13542,6 +14454,7 @@ function inferToolPlan(question, options = {}) {
13542
14454
  return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
13543
14455
  }
13544
14456
  if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
14457
+ if (/(напомин|уведом|следи|монитор)/iu.test(normalized) && /(проверь|tick|сейчас)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_reminders_tick", args: { force: true } }] };
13545
14458
  if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
13546
14459
  const dateTime = extractDateTimeFromText(question);
13547
14460
  return { steps: [{ tool: /телемост/iu.test(normalized) ? "yandex_telemost_create_event" : "yandex_calendar_create_event", args: { ...dateTime, title: extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA"), confirm: true } }] };
@@ -13557,6 +14470,7 @@ function inferToolPlan(question, options = {}) {
13557
14470
  if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
13558
14471
  if (/(резерв|backup|бэкап|диск|яндекс.?диск)/iu.test(normalized) && /(контакт)/iu.test(normalized) && /(сохрани|экспорт|выгруз|резерв|backup|бэкап)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_backup_to_disk", args: { format: /csv/iu.test(normalized) ? "csv" : "vcard", confirm: true } }] };
13559
14472
  if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
14473
+ if (/(полный|комплект|пакет)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_full_pack", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
13560
14474
  if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
13561
14475
  if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_send_disk_link_qr", args: { contact: cleanupYandexContactActionQuery(question), path: extractCloudPath(question), confirm: true } }] };
13562
14476
  if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
@@ -14148,6 +15062,11 @@ function formatToolResult(result, options) {
14148
15062
  }
14149
15063
  if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
14150
15064
  if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
15065
+ if (row.status === "mail-meeting-pack-created") return `Пакет по письму #${row.uid} создан.\nПисьмо: ${row.saved}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
15066
+ if (row.status === "contact-full-pack-created") return `Пакет контакта создан: ${row.contact}\nПапка: ${row.folder}\nДокумент: ${row.doc}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
15067
+ if (row.url && row.fromPoint && row.toPoint) return formatYandexGoDeeplinkResult(row);
15068
+ if (row.geocoder !== undefined && row.yandexgpt !== undefined && row.enabled) return `Yandex Cloud Connector:\nГеокодер: ${row.geocoder ? "настроен" : "нет"}\nYandexGPT: ${row.yandexgpt ? "настроен" : "нет"}\nВключено: ${row.enabled.join(", ") || "-"}`;
15069
+ if (row.enabled && row.text && (row.unread !== undefined || row.events !== undefined)) return row.text;
14151
15070
  if (row.status === "ambiguous" && row.events) return [`Нашел несколько событий. Уточните:`, ...row.events.map((event, index) => `${index + 1}. ${event.title || event.uid} — ${event.startIso || event.start || "-"}`)].join("\n");
14152
15071
  if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
14153
15072
  if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
@@ -15277,6 +16196,7 @@ async function getApiKey(provider) {
15277
16196
  }
15278
16197
 
15279
16198
  const secrets = await loadSecrets();
16199
+ if (provider === "yandexgpt") return secrets.yandexCloud?.yandexgptApiKey || secrets.yandexgpt?.apiKey || "";
15280
16200
  return secrets[provider]?.apiKey || "";
15281
16201
  }
15282
16202
 
@@ -15285,7 +16205,7 @@ async function getYandexFolderId() {
15285
16205
  return process.env.YANDEXGPT_FOLDER_ID || process.env.YANDEX_CLOUD_FOLDER_ID;
15286
16206
  }
15287
16207
  const secrets = await loadSecrets();
15288
- return secrets.yandexgpt?.folderId || "";
16208
+ return secrets.yandexCloud?.folderId || secrets.yandexgpt?.folderId || "";
15289
16209
  }
15290
16210
 
15291
16211
  async function listLayers(args) {
@@ -15545,12 +16465,8 @@ async function onboard(args = []) {
15545
16465
  await chooseAndSaveApiModel("openrouter");
15546
16466
  }
15547
16467
  }
15548
- if (components.includes("yandexgpt")) {
15549
- await aiSetup(["yandexgpt"]);
15550
- if (process.stdin.isTTY) {
15551
- await setAiKey("yandexgpt");
15552
- await chooseAndSaveApiModel("yandexgpt");
15553
- }
16468
+ if (components.includes("yandex-cloud")) {
16469
+ await setupYandexCloudConnector({});
15554
16470
  }
15555
16471
  if (components.includes("gigachat")) {
15556
16472
  await aiSetup(["gigachat"]);
@@ -15559,11 +16475,6 @@ async function onboard(args = []) {
15559
16475
  await chooseAndSaveApiModel("gigachat");
15560
16476
  }
15561
16477
  }
15562
- if (components.includes("yandex-geocoder")) {
15563
- if (process.stdin.isTTY) {
15564
- await setYandexGeocoderKey();
15565
- }
15566
- }
15567
16478
  if (components.includes("cloud")) {
15568
16479
  if (process.stdin.isTTY) {
15569
16480
  const providerAnswer = (await askText("Облачный диск: 1 - Яндекс Диск, 2 - Облако Mail.ru, 0 - пропустить: ")).trim();
@@ -15613,7 +16524,7 @@ async function chooseOnboardComponents(status = null) {
15613
16524
  1: "workspace",
15614
16525
  2: "policy",
15615
16526
  3: "iola",
15616
- 4: "yandexgpt",
16527
+ 4: "yandex-cloud",
15617
16528
  5: "gigachat",
15618
16529
  6: "openai",
15619
16530
  7: "openrouter",
@@ -15623,8 +16534,8 @@ async function chooseOnboardComponents(status = null) {
15623
16534
  11: "index",
15624
16535
  12: "browser",
15625
16536
  13: "ollama",
15626
- 14: "yandex-geocoder",
15627
- 15: "cloud",
16537
+ 14: "cloud",
16538
+ 15: "yandex",
15628
16539
  16: "yandex",
15629
16540
  };
15630
16541
  return [...selected].map((item) => map[item] || item).filter(Boolean);
@@ -15656,7 +16567,7 @@ async function getOnboardComponentStatus() {
15656
16567
  policy: policyReady,
15657
16568
  iola: Boolean(readiness.iola),
15658
16569
  ollama: Boolean(ollamaVersion && readiness.ollama),
15659
- yandexgpt: Boolean(readiness.yandexgpt),
16570
+ "yandex-cloud": Boolean(yandexGeocoderKey || readiness.yandexgpt),
15660
16571
  gigachat: Boolean(readiness.gigachat),
15661
16572
  openai: Boolean(readiness.openai),
15662
16573
  openrouter: Boolean(readiness.openrouter),
@@ -15665,7 +16576,6 @@ async function getOnboardComponentStatus() {
15665
16576
  archive: Boolean(archive),
15666
16577
  index: false,
15667
16578
  browser: browser.installed === "yes",
15668
- "yandex-geocoder": Boolean(yandexGeocoderKey),
15669
16579
  cloud: Object.keys(cloudSecrets).length > 0,
15670
16580
  yandex: isYandexConnectorFullyConnected(secrets),
15671
16581
  };
@@ -15676,7 +16586,7 @@ function onboardComponentRows(status) {
15676
16586
  ["1", "workspace", "workspace и контекст", "рабочая папка, IOLA.md и .iola/context.md"],
15677
16587
  ["2", "policy", "policy analyst", "разрешения и профиль аналитика"],
15678
16588
  ["3", "iola", "IOLA локальная модель", "локальная модель найдена"],
15679
- ["4", "yandexgpt", "YandexGPT API", "ключ и folder ID сохранены или есть в env"],
16589
+ ["4", "yandex-cloud", "Yandex Cloud Connector", "геокодинг и YandexGPT"],
15680
16590
  ["5", "gigachat", "GigaChat API", "authorization key сохранен или есть в env"],
15681
16591
  ["6", "openai", "OpenAI API", "API-ключ сохранен или есть в env"],
15682
16592
  ["7", "openrouter", "OpenRouter API", "API-ключ сохранен или есть в env"],
@@ -15686,9 +16596,8 @@ function onboardComponentRows(status) {
15686
16596
  ["11", "index", "Индекс локальных документов", "настраивается под выбранную папку"],
15687
16597
  ["12", "browser", "Browser runtime", "Playwright/Chromium установлен"],
15688
16598
  ["13", "ollama", "Ollama", "опциональный локальный runtime"],
15689
- ["14", "yandex-geocoder", "Yandex Geocoder API", "ключ геокодера сохранен или есть в env"],
15690
- ["15", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
15691
- ["16", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
16599
+ ["14", "cloud", "Облачный диск", "Яндекс Диск или Облако Mail.ru"],
16600
+ ["15", "yandex", "Yandex Connector", "единый вход и категории сервисов Яндекса"],
15692
16601
  ];
15693
16602
  return rows.map(([number, key, title, hint]) => ({ number, key, title, hint, status: status[key] ? "готово" : "не настроено" }));
15694
16603
  }
@@ -15703,7 +16612,7 @@ function defaultOnboardSelection(status) {
15703
16612
  }
15704
16613
 
15705
16614
  function defaultOnboardComponents(status) {
15706
- 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", 15: "cloud", 16: "yandex" };
16615
+ const map = { 1: "workspace", 2: "policy", 3: "iola", 4: "yandex-cloud", 5: "gigachat", 6: "openai", 7: "openrouter", 8: "codex", 9: "codex-mcp", 10: "archive", 11: "index", 12: "browser", 13: "ollama", 14: "cloud", 15: "yandex", 16: "yandex" };
15707
16616
  return defaultOnboardSelection(status).map((item) => map[item]).filter(Boolean);
15708
16617
  }
15709
16618
 
@@ -15712,12 +16621,12 @@ function parseOptions(args) {
15712
16621
 
15713
16622
  for (let index = 0; index < args.length; index += 1) {
15714
16623
  const arg = args[index];
15715
- if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable") {
16624
+ if (arg === "--json" || arg === "--yes" || arg === "--silent" || arg === "--events" || arg === "--stream-json" || arg === "--stdio" || arg === "--system" || arg === "--headed" || arg === "--headless" || arg === "--no-history" || arg === "--summary" || arg === "--all" || arg === "--full" || arg === "--unread" || arg === "--once" || arg === "--local" || arg === "--cache" || arg === "--tools" || arg === "--files" || arg === "--plan" || arg === "--trace" || arg === "--diff" || arg === "--stage" || arg === "--fts" || arg === "--bare" || arg === "--quiet" || arg === "--optional" || arg === "--project" || arg === "--dry-run" || arg === "--no-color" || arg === "--fail-on-empty" || arg === "--debug" || arg === "--fix" || arg === "--force" || arg === "--append" || arg === "--preserve-active" || arg === "--open" || arg === "--print-url" || arg === "--enable" || arg === "--email" || arg === "--backup") {
15716
16625
  result[arg.slice(2)] = true;
15717
16626
  } else if (arg === "--check" || arg === "--upgrade-node") {
15718
16627
  result.check = true;
15719
16628
  result[arg.slice(2)] = true;
15720
- } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app") {
16629
+ } else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--instructions" || arg === "--allowed-tools" || arg === "--tool" || arg === "--uses" || arg === "--template" || arg === "--minutes" || arg === "--days" || arg === "--time" || arg === "--horizon" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-url" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address" || arg === "--token" || arg === "--app" || arg === "--tariff" || arg === "--class" || arg === "--level" || arg === "--ref" || arg === "--lang") {
15721
16630
  result[arg.slice(2)] = args[index + 1];
15722
16631
  index += 1;
15723
16632
  } else {
@@ -15941,9 +16850,13 @@ function printSkillsList(skills, config) {
15941
16850
  async function executeUserSkillTool(tool, args = {}) {
15942
16851
  if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
15943
16852
  if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
16853
+ if (tool === "user_skill_update") return userSkillUpdate(args.name, { ...args, confirm: args.confirm === true });
15944
16854
  if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
15945
16855
  if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
15946
16856
  if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
16857
+ if (tool === "user_skill_templates") return userSkillTemplates();
16858
+ if (tool === "user_skill_validate") return userSkillValidate(args.name);
16859
+ if (tool === "user_skill_preview") return { status: "preview", text: buildUserSkillPreview(args) };
15947
16860
  throw new Error(`Неизвестный user skill tool: ${tool}`);
15948
16861
  }
15949
16862
 
@@ -15951,22 +16864,41 @@ async function userSkillCreate(args = {}) {
15951
16864
  if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
15952
16865
  const name = normalizeUserSkillName(args.name || args.skill || args.title);
15953
16866
  if (!name) throw new Error("Имя skill обязательно.");
15954
- const description = String(args.description || `Пользовательский skill: ${name}`).trim();
15955
- const instructions = String(args.instructions || args.text || args.prompt || "").trim();
16867
+ const template = getUserSkillTemplate(args.template);
16868
+ const description = String(args.description || template?.description || `Пользовательский skill: ${name}`).trim();
16869
+ const instructions = String(args.instructions || args.text || args.prompt || template?.instructions || "").trim();
15956
16870
  if (!instructions) throw new Error("Инструкции skill обязательны.");
15957
16871
  const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
16872
+ const mergedTools = [...new Set([...(template?.tools || []), ...tools])];
15958
16873
  const dir = path.join(USER_SKILLS_DIR, name);
15959
16874
  const file = path.join(dir, "SKILL.md");
15960
16875
  if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
15961
16876
  await mkdir(dir, { recursive: true });
15962
- const body = buildUserSkillMarkdown({ name, description, instructions, tools });
16877
+ const body = buildUserSkillMarkdown({ name, description, instructions, tools: mergedTools });
15963
16878
  await writeFile(file, body, "utf8");
15964
16879
  let enabled = false;
15965
16880
  if (args.enable) {
15966
16881
  await userSkillSetEnabled(name, true);
15967
16882
  enabled = true;
15968
16883
  }
15969
- return { name, description, file, enabled, tools, status: "created" };
16884
+ return { name, description, file, enabled, tools: mergedTools, status: "created" };
16885
+ }
16886
+
16887
+ async function userSkillUpdate(name, args = {}) {
16888
+ if (!args.confirm) throw new Error("Для обновления пользовательского skill нужен confirm=true.");
16889
+ const skillName = normalizeUserSkillName(name || args.name || args.skill);
16890
+ if (!skillName) throw new Error("Имя skill обязательно.");
16891
+ const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
16892
+ if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
16893
+ const current = await readFile(file, "utf8");
16894
+ const meta = readSkillMeta(file);
16895
+ const description = String(args.description || meta.description || `Пользовательский skill: ${skillName}`).trim();
16896
+ const instructions = String(args.instructions || args.text || args.prompt || stripFrontmatter(current)).trim();
16897
+ const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions));
16898
+ const body = buildUserSkillMarkdown({ name: skillName, description, instructions, tools });
16899
+ await writeFile(file, body, "utf8");
16900
+ if (args.enable) await userSkillSetEnabled(skillName, true);
16901
+ return { name: skillName, description, file, tools, status: "updated" };
15970
16902
  }
15971
16903
 
15972
16904
  async function userSkillSetEnabled(name, enabled) {
@@ -16060,9 +16992,23 @@ function inferUserSkillTools(question) {
16060
16992
  }
16061
16993
  if (/(календар|событи|встреч|телемост)/iu.test(text)) {
16062
16994
  tools.add("yandex_calendar_list");
16995
+ tools.add("yandex_calendar_search");
16063
16996
  tools.add("yandex_calendar_create_event");
16997
+ tools.add("yandex_calendar_update");
16998
+ tools.add("yandex_calendar_move");
16999
+ tools.add("yandex_calendar_delete");
17000
+ tools.add("yandex_calendar_add_reminder");
16064
17001
  tools.add("yandex_telemost_create_event");
16065
17002
  }
17003
+ if (/(документ|docs|360)/iu.test(text)) {
17004
+ tools.add("yandex_docs_list");
17005
+ tools.add("yandex_docs_find");
17006
+ tools.add("yandex_docs_create_text");
17007
+ tools.add("yandex_docs_read");
17008
+ tools.add("yandex_docs_share");
17009
+ tools.add("yandex_docs_rename");
17010
+ tools.add("yandex_docs_delete");
17011
+ }
16066
17012
  if (/(контакт|адресн)/iu.test(text)) {
16067
17013
  tools.add("yandex_contacts_search");
16068
17014
  tools.add("yandex_contacts_get");
@@ -16103,6 +17049,69 @@ function inferUserSkillTools(question) {
16103
17049
  return [...tools];
16104
17050
  }
16105
17051
 
17052
+ function userSkillTemplates() {
17053
+ return [
17054
+ {
17055
+ name: "mail-triage",
17056
+ description: "Разбор почты: найти важные письма, прочитать, сохранить, создать задачу или событие.",
17057
+ tools: ["yandex_mail_list", "yandex_mail_search", "yandex_mail_read", "yandex_mail_save_to_disk", "yandex_mail_create_calendar_event", "yandex_mail_create_task"],
17058
+ instructions: "Помогай разбирать почту пользователя. Сначала показывай краткую сводку, затем выполняй только явно запрошенные действия: чтение, сохранение письма на Диск, создание события или задачи.",
17059
+ },
17060
+ {
17061
+ name: "family-calendar",
17062
+ description: "Семейный календарь: события, напоминания, встречи и ежедневные проверки.",
17063
+ tools: ["yandex_calendar_list", "yandex_calendar_search", "yandex_calendar_create_event", "yandex_calendar_move", "yandex_calendar_add_reminder", "yandex_calendar_delete", "yandex_calendar_reminders_tick"],
17064
+ instructions: "Помогай вести личный календарь. Для изменения календаря требуй явную просьбу пользователя. Всегда называй дату и время события.",
17065
+ },
17066
+ {
17067
+ name: "docs-organizer",
17068
+ description: "Организация документов на Яндекс Диске.",
17069
+ tools: ["yandex_docs_list", "yandex_docs_find", "yandex_docs_create_text", "yandex_docs_read", "yandex_docs_share", "yandex_docs_rename", "yandex_docs_delete", "yandex_disk_maintenance_tick"],
17070
+ instructions: "Помогай искать, создавать и приводить в порядок документы на Яндекс Диске. Не путай облачные документы с локальными файлами на компьютере.",
17071
+ },
17072
+ {
17073
+ name: "contact-workflow",
17074
+ description: "Работа с контактами, письмами, встречами и папками контактов.",
17075
+ tools: ["yandex_contacts_search", "yandex_contacts_get", "yandex_contact_send_mail", "yandex_contact_send_disk_link_qr", "yandex_contact_create_disk_folder", "yandex_contact_create_calendar_event", "yandex_contact_full_pack"],
17076
+ instructions: "Помогай выполнять действия вокруг контакта: найти карточку, отправить письмо, создать папку, отправить ссылку или создать встречу. Если найдено несколько контактов, проси уточнение.",
17077
+ },
17078
+ ];
17079
+ }
17080
+
17081
+ function getUserSkillTemplate(name) {
17082
+ const normalized = normalizeUserSkillName(name || "");
17083
+ return userSkillTemplates().find((item) => item.name === normalized) || null;
17084
+ }
17085
+
17086
+ function buildUserSkillPreview(args = {}) {
17087
+ const name = normalizeUserSkillName(args.name || args.skill || args.title || "user-skill");
17088
+ const template = getUserSkillTemplate(args.template);
17089
+ const description = args.description || template?.description || `Пользовательский skill: ${name}`;
17090
+ const instructions = args.instructions || args.text || args.prompt || template?.instructions || "Опишите, что должен делать skill.";
17091
+ const tools = [...new Set([...(template?.tools || []), ...parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions))])];
17092
+ return buildUserSkillMarkdown({ name, description, instructions, tools });
17093
+ }
17094
+
17095
+ async function userSkillValidate(name) {
17096
+ const config = await loadConfig();
17097
+ const skill = findSkill(normalizeUserSkillName(name), config);
17098
+ const checks = [];
17099
+ if (!skill) {
17100
+ return { status: "error", checks: [{ check: "exists", status: "error", message: `Skill не найден: ${name || "-"}` }] };
17101
+ }
17102
+ const text = await readFile(skill.file, "utf8");
17103
+ const meta = readSkillMeta(skill.file);
17104
+ checks.push({ check: "exists", status: "ok", message: skill.file });
17105
+ checks.push({ check: "name", status: meta.name ? "ok" : "error", message: meta.name || "Нет name во frontmatter" });
17106
+ checks.push({ check: "description", status: meta.description ? "ok" : "warn", message: meta.description || "Описание пустое" });
17107
+ checks.push({ check: "instructions", status: stripFrontmatter(text).trim().length >= 40 ? "ok" : "warn", message: "Инструкции должны быть понятными и достаточно подробными" });
17108
+ checks.push({ check: "secrets", status: /(token|api[_-]?key|парол|секрет)\s*[:=]\s*\S{8,}/iu.test(text) ? "error" : "ok", message: "Skill не должен содержать секреты" });
17109
+ const tools = [...text.matchAll(/`([a-z0-9_:-]+)`/giu)].map((match) => match[1]).filter((tool) => ALL_TOOL_ALIASES.includes(tool) || tool.startsWith("mcp:"));
17110
+ const unknownTools = tools.filter((tool) => !ALL_TOOL_ALIASES.includes(tool) && !tool.startsWith("mcp:"));
17111
+ checks.push({ check: "tools", status: unknownTools.length ? "warn" : "ok", message: unknownTools.length ? `Неизвестные tools: ${unknownTools.join(", ")}` : `${tools.length} tools` });
17112
+ return { status: checks.some((row) => row.status === "error") ? "error" : "ok", checks };
17113
+ }
17114
+
16106
17115
  function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
16107
17116
  const toolLines = tools.length
16108
17117
  ? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]