@iola_adm/iola-cli 0.2.35 → 0.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,7 +202,7 @@ Yandex Connector использует две встроенные OAuth-груп
202
202
 
203
203
  В `/yandex` функции выбираются номерами через запятую, как в мастере настройки. Там же есть пункт `Удалить подключение-коннектор`, который чистит локальные токены и настройки Yandex Connector. Мастер считает коннектор готовым только после токенов обеих групп.
204
204
 
205
- Yandex tools уже доступны: профиль Yandex ID, расширенная работа с Яндекс Диском (место, список, поиск, карточка, чтение текста, папки, загрузка, скачивание, ссылки, QR-коды к публичным ссылкам, отправка ссылки и QR по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты, полноценная работа с Календарем через CalDAV (календари, список, поиск, создание, перенос, редактирование, напоминания, повторы, удаление), Яндекс Документы/360 через Диск (создание текстовых документов, поиск, чтение, ссылки/QR, переименование, удаление), расширенные Яндекс Контакты (поиск, создание, обновление, удаление, импорт/экспорт, дубликаты, backup на Диск, дни рождения в календарь, регулярная contacts-maintenance проверка) и комбинированные сценарии: письмо контакту, ссылка+QR контакту, папка контакта на Диске, встреча/Телемост с контактом. Телемост пытается использовать прямой API, а если он недоступен текущему аккаунту, честно создает календарное событие без выдуманной ссылки. Отправка письма, удаление/перемещение файлов, публикация ссылок, изменение контактов, документов и событий требуют явного подтверждения.
205
+ Yandex tools уже доступны: профиль Yandex ID, расширенная работа с Яндекс Диском (место, список, поиск, карточка, чтение текста, папки, загрузка, скачивание, ссылки, QR-коды к публичным ссылкам, отправка ссылки и QR по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты, полноценная работа с Календарем через CalDAV (календари, список, поиск, создание, перенос, редактирование, напоминания, повторы, удаление), Яндекс Документы/360 через Диск (создание текстовых документов, поиск, чтение, ссылки/QR, переименование, удаление), расширенные Яндекс Контакты (поиск, создание, обновление, удаление, импорт/экспорт, дубликаты, backup на Диск, дни рождения в календарь, регулярная contacts-maintenance проверка) и комбинированные сценарии: пакет по письму (сохранить письмо на Диск, ссылка/QR, событие календаря), полный пакет по контакту (папка, документ, ссылка/QR, встреча), письмо контакту, ссылка+QR контакту, папка контакта на Диске, встреча/Телемост с контактом. Телемост пытается использовать прямой API, а если он недоступен текущему аккаунту, честно создает календарное событие без выдуманной ссылки. Отправка письма, удаление/перемещение файлов, публикация ссылок, изменение контактов, документов и событий требуют явного подтверждения.
206
206
 
207
207
  Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
208
208
 
@@ -260,6 +260,7 @@ iola version --check
260
260
  - ленивые skills, toolsets, permissions, memory, hooks и готовые agents;
261
261
  - личные облачные диски: Яндекс Диск и Облако Mail.ru для сохранения отчетов, backup и документов;
262
262
  - subagents, skill bundles, layered settings, usage/budget accounting и trajectory export;
263
+ - пользовательские skills: шаблоны, preview, validate, create/update/enable/disable/delete;
263
264
  - локальный MCP-сервер по stdio/http для подключения iola-cli к другим AI-клиентам;
264
265
  - ответы по открытым данным берутся из публичного MCP `https://apiiola.yasg.ru/mcp`;
265
266
  - локальная БД и прямой API используются как резерв, если публичный MCP временно недоступен;
@@ -272,5 +273,5 @@ iola version --check
272
273
  - чтение и индексирование `.docx`, `.xlsx`, `.pptx`, `.pdf`, `.md`, `.txt`, `.csv`, `.json`, `.html`;
273
274
  - работа с архивами через 7-Zip: `.zip`, `.7z`, `.rar`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` и другие;
274
275
  - расширенный `iola onboard` с установкой 7-Zip, браузерного runtime, IOLA local, Ollama, Codex CLI и настройкой выбранных компонентов;
275
- - cron-задачи, локальный daemon, web dashboard и RPC для автоматизаций;
276
+ - cron-задачи, локальный daemon, web dashboard и RPC для автоматизаций, включая автоопрос почты, ежедневный дайджест, календарные напоминания, проверку контактов и аудит Яндекс Диска;
276
277
  - контекстные файлы `IOLA.md` и `.iola/context.md`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.35",
3
+ "version": "0.2.36",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -48,6 +48,7 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
48
48
  - `yandex_mail_city_context` - найти в письме школы/детские сады/ИНН и подтянуть открытые городские слои.
49
49
  - `yandex_mail_map_addresses` - найти адреса в письме и дать ссылки на Яндекс.Карты.
50
50
  - `yandex_mail_create_task` - создать локальную задачу по письму.
51
+ - `yandex_mail_meeting_pack` - по письму сохранить его на Диск, сделать ссылку/QR и создать встречу в календаре с отправителем.
51
52
  - Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
52
53
  - `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
53
54
  - `yandex_calendar_calendars` - показать доступные календари.
@@ -98,9 +99,16 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
98
99
  - `yandex_contact_create_calendar_event` - создать встречу с контактом.
99
100
  - `yandex_contact_create_telemost_event` - создать календарное событие для Телемоста с контактом.
100
101
  - `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя, если у организации есть email или телефон.
102
+ - `yandex_contact_full_pack` - создать полный пакет контакта: папка на Диске, документ, ссылка/QR и при возможности встреча.
101
103
  - `yandex_telemost_status` - проверить режим Телемоста.
102
104
  - `yandex_telemost_create_event` - создать встречу: если прямой Telemost API доступен аккаунту, добавить ссылку; иначе создать календарное событие с честным fallback.
105
+ - `yandex_daily_digest` - собрать дайджест из почты, календаря и контактов; может сохранять Markdown-документ на Диск.
106
+ - `yandex_calendar_reminders_tick` - проверить ближайшие события календаря для напоминаний.
107
+ - `yandex_disk_maintenance_tick` - проверить место, документы и публичные ссылки в `/IOLA`, сохранить отчет на Диск.
103
108
  - Регулярная проверка контактов включается командой `iola yandex contacts-maintenance on --days 7`, выключается `iola yandex contacts-maintenance off`, ручная проверка `iola yandex contacts-maintenance tick`. Флаг `--backup` включает backup контактов на Диск при tick.
109
+ - Ежедневный дайджест включается командой `iola yandex daily-digest on --time 09:00`, выключается `iola yandex daily-digest off`, ручной запуск `iola yandex daily-digest tick`.
110
+ - Проверка календарных напоминаний включается `iola yandex calendar-reminders on --minutes 15`, выключается `iola yandex calendar-reminders off`.
111
+ - Проверка Яндекс Диска включается `iola yandex disk-maintenance on --days 7`, выключается `iola yandex disk-maintenance off`, ручной запуск `iola yandex disk-maintenance tick`.
104
112
 
105
113
  Комбинированные сценарии:
106
114
 
@@ -109,6 +117,9 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
109
117
  - Если пользователь просит "перенеси/скопируй файлы из папки, создай ссылку/QR и отправь", используй `yandex_disk_package_share_email`.
110
118
  - Если получатель указан именем, сначала ищи его в контактах. Если контактов несколько или email не найден, попроси уточнение.
111
119
  - Если пользователь просит отправить письмо контакту, создать встречу с контактом или отправить ссылку контакту, используй специализированные `yandex_contact_*` tools, а не ручную цепочку из нескольких tools.
120
+ - Если пользователь просит "пакет по письму", "подготовь встречу по письму", "сохрани письмо, сделай ссылку и событие", используй `yandex_mail_meeting_pack`.
121
+ - Если пользователь просит "полный пакет по контакту", используй `yandex_contact_full_pack`.
122
+ - Если пользователь просит ежедневную сводку/дайджест, используй `yandex_daily_digest` или команды `iola yandex daily-digest ...`.
112
123
  - Если пользователь просит создать папку для контакта на Яндекс Диске, используй `yandex_contact_create_disk_folder`.
113
124
  - Если пользователь просит создать/найти/прочитать/переименовать/удалить документ именно на Яндекс Диске или в Яндекс 360, используй `yandex_docs_*`, а не общий список файлов.
114
125
  - Если пользователь просит перенести событие, добавить напоминание или удалить встречу, используй `yandex_calendar_move`, `yandex_calendar_add_reminder`, `yandex_calendar_delete`.
package/src/cli.js CHANGED
@@ -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,10 @@ 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",
246
251
  ];
247
252
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
248
253
  const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
@@ -3065,6 +3070,29 @@ async function handleSkills(args) {
3065
3070
  return;
3066
3071
  }
3067
3072
 
3073
+ if (action === "templates") {
3074
+ printTable(userSkillTemplates().map((row) => ({ name: row.name, description: row.description, tools: row.tools.join(", ") })), [["name", "Шаблон"], ["description", "Описание"], ["tools", "Tools"]]);
3075
+ return;
3076
+ }
3077
+
3078
+ if (action === "preview") {
3079
+ const options = parseOptions(args.slice(2));
3080
+ console.log(buildUserSkillPreview({
3081
+ name,
3082
+ description: options.description || "",
3083
+ instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3084
+ tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3085
+ template: options.template,
3086
+ }));
3087
+ return;
3088
+ }
3089
+
3090
+ if (action === "validate") {
3091
+ const result = await userSkillValidate(name);
3092
+ printTable(result.checks, [["check", "Проверка"], ["status", "Статус"], ["message", "Сообщение"]]);
3093
+ return;
3094
+ }
3095
+
3068
3096
  if (action === "create" || action === "new") {
3069
3097
  const options = parseOptions(args.slice(2));
3070
3098
  const result = await userSkillCreate({
@@ -3072,6 +3100,7 @@ async function handleSkills(args) {
3072
3100
  description: options.description || "",
3073
3101
  instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3074
3102
  tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3103
+ template: options.template,
3075
3104
  enable: Boolean(options.enable),
3076
3105
  overwrite: Boolean(options.force),
3077
3106
  confirm: true,
@@ -3082,6 +3111,20 @@ async function handleSkills(args) {
3082
3111
  return;
3083
3112
  }
3084
3113
 
3114
+ if (action === "update" || action === "edit") {
3115
+ const options = parseOptions(args.slice(2));
3116
+ const result = await userSkillUpdate(name, {
3117
+ description: options.description,
3118
+ instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3119
+ tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3120
+ enable: options.enable,
3121
+ confirm: true,
3122
+ });
3123
+ console.log(`Skill обновлен: ${result.name}`);
3124
+ console.log(`Файл: ${result.file}`);
3125
+ return;
3126
+ }
3127
+
3085
3128
  if (action === "delete" || action === "remove" || action === "rm") {
3086
3129
  const options = parseOptions(args.slice(2));
3087
3130
  const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
@@ -3099,7 +3142,7 @@ async function handleSkills(args) {
3099
3142
  return;
3100
3143
  }
3101
3144
 
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.");
3145
+ 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
3146
  }
3104
3147
 
3105
3148
  async function handleTools(args) {
@@ -3395,6 +3438,21 @@ async function handleYandex(args) {
3395
3438
  return;
3396
3439
  }
3397
3440
 
3441
+ if (action === "daily-digest" || action === "digest") {
3442
+ await handleYandexDailyDigest([target, ...rest].filter(Boolean));
3443
+ return;
3444
+ }
3445
+
3446
+ if (action === "calendar-reminders" || action === "calendar-watch" || action === "reminders") {
3447
+ await handleYandexCalendarReminders([target, ...rest].filter(Boolean));
3448
+ return;
3449
+ }
3450
+
3451
+ if (action === "disk-maintenance" || action === "disk-watch" || action === "disk-doctor") {
3452
+ await handleYandexDiskMaintenance([target, ...rest].filter(Boolean));
3453
+ return;
3454
+ }
3455
+
3398
3456
  if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
3399
3457
  await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
3400
3458
  return;
@@ -3436,6 +3494,9 @@ async function handleYandex(args) {
3436
3494
  iola yandex status|doctor
3437
3495
  iola yandex services
3438
3496
  iola yandex mail-watch on|off|status|tick [--minutes 5]
3497
+ iola yandex daily-digest on|off|status|tick [--time 09:00] [--email]
3498
+ iola yandex calendar-reminders on|off|status|tick [--minutes 15]
3499
+ iola yandex disk-maintenance on|off|status|tick [--days 7]
3439
3500
  iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
3440
3501
  iola yandex enable disk mail calendar
3441
3502
  iola yandex disable mail
@@ -3507,6 +3568,107 @@ async function handleYandexMailWatch(args = []) {
3507
3568
  throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
3508
3569
  }
3509
3570
 
3571
+ async function handleYandexDailyDigest(args = []) {
3572
+ const [action = "status", ...rest] = args;
3573
+ const options = parseOptions(rest);
3574
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3575
+ const time = options.time || rest.find((item) => /^\d{1,2}:\d{2}$/u.test(String(item))) || "09:00";
3576
+ const result = await yandexDailyDigestEnable({ time, email: Boolean(options.email), save: options.save !== false });
3577
+ console.log(`Ежедневный дайджест включен: каждый день ${result.time}. Email: ${result.email ? "yes" : "no"}.`);
3578
+ return;
3579
+ }
3580
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3581
+ await yandexDailyDigestDisable();
3582
+ console.log("Ежедневный дайджест выключен.");
3583
+ return;
3584
+ }
3585
+ if (action === "tick" || action === "run" || action === "check") {
3586
+ const result = await yandexDailyDigestTick({ force: true, email: Boolean(options.email), save: options.save !== false });
3587
+ console.log(result.text || "Дайджест пуст.");
3588
+ if (result.remote) console.log(`Сохранено на Диск: ${result.remote}`);
3589
+ return;
3590
+ }
3591
+ const config = await loadConfig();
3592
+ const digest = config.yandex?.dailyDigest || {};
3593
+ printKeyValue({
3594
+ enabled: digest.enabled ? "yes" : "no",
3595
+ time: digest.time || "-",
3596
+ email: digest.email ? "yes" : "no",
3597
+ lastRunAt: digest.lastRunAt || "-",
3598
+ lastRemote: digest.lastRemote || "-",
3599
+ cron: listCronJobs().some((job) => job.command === "yandex daily-digest tick") ? "yes" : "no",
3600
+ });
3601
+ }
3602
+
3603
+ async function handleYandexCalendarReminders(args = []) {
3604
+ const [action = "status", ...rest] = args;
3605
+ const options = parseOptions(rest);
3606
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3607
+ const minutes = Math.max(1, Number(options.minutes || rest.find((item) => /^\d+$/u.test(String(item))) || 15));
3608
+ const result = await yandexCalendarRemindersEnable(minutes);
3609
+ console.log(`Проверка календарных напоминаний включена: каждые ${result.minutes} минут.`);
3610
+ return;
3611
+ }
3612
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3613
+ await yandexCalendarRemindersDisable();
3614
+ console.log("Проверка календарных напоминаний выключена.");
3615
+ return;
3616
+ }
3617
+ if (action === "tick" || action === "run" || action === "check") {
3618
+ const result = await yandexCalendarRemindersTick({ force: true, horizonMinutes: Number(options.horizon || options.minutes || 60) });
3619
+ if (!result.events.length) console.log("Ближайших событий для напоминания нет.");
3620
+ else console.log(["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n"));
3621
+ return;
3622
+ }
3623
+ const config = await loadConfig();
3624
+ const reminders = config.yandex?.calendarReminders || {};
3625
+ printKeyValue({
3626
+ enabled: reminders.enabled ? "yes" : "no",
3627
+ minutes: reminders.minutes || "-",
3628
+ horizonMinutes: reminders.horizonMinutes || 60,
3629
+ lastRunAt: reminders.lastRunAt || "-",
3630
+ lastCount: reminders.lastCount ?? "-",
3631
+ cron: listCronJobs().some((job) => job.command === "yandex calendar-reminders tick") ? "yes" : "no",
3632
+ });
3633
+ }
3634
+
3635
+ async function handleYandexDiskMaintenance(args = []) {
3636
+ const [action = "status", ...rest] = args;
3637
+ const options = parseOptions(rest);
3638
+ if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
3639
+ const days = Math.max(1, Number(options.days || rest.find((item) => /^\d+$/u.test(String(item))) || 7));
3640
+ const result = await yandexDiskMaintenanceEnable(days);
3641
+ console.log(`Проверка Яндекс Диска включена: каждые ${result.days} дней.`);
3642
+ return;
3643
+ }
3644
+ if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
3645
+ await yandexDiskMaintenanceDisable();
3646
+ console.log("Проверка Яндекс Диска выключена.");
3647
+ return;
3648
+ }
3649
+ if (action === "tick" || action === "run" || action === "check") {
3650
+ const result = await yandexDiskMaintenanceTick({ force: true });
3651
+ printKeyValue({
3652
+ used: formatBytes(result.usedSpace),
3653
+ total: formatBytes(result.totalSpace),
3654
+ trash: formatBytes(result.trashSize),
3655
+ docs: result.docs,
3656
+ publicLinks: result.publicLinks,
3657
+ remote: result.remote || "-",
3658
+ });
3659
+ return;
3660
+ }
3661
+ const config = await loadConfig();
3662
+ const disk = config.yandex?.diskMaintenance || {};
3663
+ printKeyValue({
3664
+ enabled: disk.enabled ? "yes" : "no",
3665
+ days: disk.days || "-",
3666
+ lastRunAt: disk.lastRunAt || "-",
3667
+ lastRemote: disk.lastRemote || "-",
3668
+ cron: listCronJobs().some((job) => job.command === "yandex disk-maintenance tick") ? "yes" : "no",
3669
+ });
3670
+ }
3671
+
3510
3672
  async function handleYandexContactsMaintenance(args = []) {
3511
3673
  const [action = "status", ...rest] = args;
3512
3674
  const options = parseOptions(rest);
@@ -4164,6 +4326,7 @@ async function executeYandexTool(tool, args = {}) {
4164
4326
  if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
4165
4327
  if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
4166
4328
  if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
4329
+ if (tool === "yandex_mail_meeting_pack") return yandexMailMeetingPack(args.uid || args.id, args);
4167
4330
  if (tool === "yandex_calendar_status") return yandexCalendarStatus();
4168
4331
  if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
4169
4332
  if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
@@ -4215,6 +4378,10 @@ async function executeYandexTool(tool, args = {}) {
4215
4378
  if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
4216
4379
  if (tool === "yandex_telemost_status") return yandexTelemostStatus();
4217
4380
  if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
4381
+ if (tool === "yandex_contact_full_pack") return yandexContactFullPack(args);
4382
+ if (tool === "yandex_daily_digest") return yandexDailyDigestTick({ ...args, force: true });
4383
+ if (tool === "yandex_calendar_reminders_tick") return yandexCalendarRemindersTick({ ...args, force: true });
4384
+ if (tool === "yandex_disk_maintenance_tick") return yandexDiskMaintenanceTick({ ...args, force: true });
4218
4385
  throw new Error(`Yandex tool неизвестен: ${tool}`);
4219
4386
  }
4220
4387
 
@@ -4385,6 +4552,142 @@ async function yandexMailWatchDisable() {
4385
4552
  return { enabled: false };
4386
4553
  }
4387
4554
 
4555
+ async function yandexDailyDigestEnable(options = {}) {
4556
+ const config = await loadConfig();
4557
+ const time = normalizeDigestTime(options.time || "09:00");
4558
+ await saveConfig({
4559
+ yandex: {
4560
+ ...(config.yandex || {}),
4561
+ dailyDigest: { ...(config.yandex?.dailyDigest || {}), enabled: true, time, email: Boolean(options.email), save: options.save !== false, updatedAt: new Date().toISOString() },
4562
+ },
4563
+ });
4564
+ await upsertCronJob(`каждый день ${time}`, "yandex daily-digest tick", { replaceCommand: true });
4565
+ return { enabled: true, time, email: Boolean(options.email), save: options.save !== false };
4566
+ }
4567
+
4568
+ async function yandexDailyDigestDisable() {
4569
+ const config = await loadConfig();
4570
+ const current = config.yandex?.dailyDigest || {};
4571
+ await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
4572
+ deleteCronJobsByCommand("yandex daily-digest tick");
4573
+ return { enabled: false };
4574
+ }
4575
+
4576
+ async function yandexDailyDigestTick(options = {}) {
4577
+ const config = await loadConfig();
4578
+ const digest = config.yandex?.dailyDigest || {};
4579
+ if (!digest.enabled && !options.force) return { enabled: false, text: "" };
4580
+ const unread = await yandexMailList({ mailbox: "INBOX", limit: 10, unread: true }).catch(() => []);
4581
+ const events = await yandexCalendarList({ start: new Date().toISOString(), end: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), limit: 20 }).catch(() => []);
4582
+ const incomplete = await yandexContactsFindIncomplete({ limit: 10 }).catch(() => []);
4583
+ const text = [
4584
+ `# Дайджест IOLA за ${new Intl.DateTimeFormat("ru-RU", { dateStyle: "long" }).format(new Date())}`,
4585
+ "",
4586
+ `Непрочитанных писем: ${unread.length}`,
4587
+ ...unread.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`),
4588
+ "",
4589
+ `Событий на 24 часа: ${events.length}`,
4590
+ ...events.slice(0, 10).map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}`),
4591
+ "",
4592
+ `Неполных контактов: ${incomplete.length}`,
4593
+ ...incomplete.slice(0, 5).map((row, index) => `${index + 1}. ${formatYandexContact(row)}`),
4594
+ ].join("\n").trim();
4595
+ let remote = "";
4596
+ if (options.save !== false && digest.save !== false) {
4597
+ const saved = await yandexDocsCreateText({ title: `daily-digest-${timestampForFile()}`, text, format: "md", confirm: true });
4598
+ remote = saved.remote || "";
4599
+ }
4600
+ if (options.email || digest.email) {
4601
+ const profile = await getYandexIdentityProfile();
4602
+ const to = profile.defaultEmail || profile.emails?.[0];
4603
+ if (to) await yandexMailSend({ to: [to], subject: "Ежедневный дайджест IOLA", text, confirm: true });
4604
+ }
4605
+ await saveConfig({ yandex: { ...(config.yandex || {}), dailyDigest: { ...digest, enabled: digest.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: remote || digest.lastRemote || "" } } });
4606
+ return { enabled: true, text, remote, unread: unread.length, events: events.length, incomplete: incomplete.length };
4607
+ }
4608
+
4609
+ async function yandexCalendarRemindersEnable(minutes = 15) {
4610
+ const config = await loadConfig();
4611
+ const safeMinutes = Math.max(1, Number(minutes || 15));
4612
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...(config.yandex?.calendarReminders || {}), enabled: true, minutes: safeMinutes, horizonMinutes: 60, updatedAt: new Date().toISOString() } } });
4613
+ await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex calendar-reminders tick", { replaceCommand: true });
4614
+ return { enabled: true, minutes: safeMinutes };
4615
+ }
4616
+
4617
+ async function yandexCalendarRemindersDisable() {
4618
+ const config = await loadConfig();
4619
+ const current = config.yandex?.calendarReminders || {};
4620
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...current, enabled: false, seen: [], updatedAt: new Date().toISOString() } } });
4621
+ deleteCronJobsByCommand("yandex calendar-reminders tick");
4622
+ return { enabled: false };
4623
+ }
4624
+
4625
+ async function yandexCalendarRemindersTick(options = {}) {
4626
+ const config = await loadConfig();
4627
+ const reminders = config.yandex?.calendarReminders || {};
4628
+ if (!reminders.enabled && !options.force) return { enabled: false, events: [] };
4629
+ const horizon = Math.max(5, Number(options.horizonMinutes || reminders.horizonMinutes || 60));
4630
+ const now = new Date();
4631
+ const rows = await yandexCalendarList({ start: now.toISOString(), end: new Date(now.getTime() + horizon * 60 * 1000).toISOString(), limit: 30 });
4632
+ const seen = new Set(reminders.seen || []);
4633
+ const fresh = rows.filter((row) => {
4634
+ const key = `${row.uid}:${row.start}`;
4635
+ if (seen.has(key)) return false;
4636
+ seen.add(key);
4637
+ return true;
4638
+ });
4639
+ await saveConfig({ yandex: { ...(config.yandex || {}), calendarReminders: { ...reminders, enabled: reminders.enabled !== false, lastRunAt: new Date().toISOString(), lastCount: fresh.length, seen: [...seen].slice(-500) } } });
4640
+ return { enabled: true, events: fresh };
4641
+ }
4642
+
4643
+ async function yandexDiskMaintenanceEnable(days = 7) {
4644
+ const config = await loadConfig();
4645
+ const safeDays = Math.max(1, Number(days || 7));
4646
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...(config.yandex?.diskMaintenance || {}), enabled: true, days: safeDays, updatedAt: new Date().toISOString() } } });
4647
+ await upsertCronJob(`каждые ${safeDays} дней`, "yandex disk-maintenance tick", { replaceCommand: true });
4648
+ return { enabled: true, days: safeDays };
4649
+ }
4650
+
4651
+ async function yandexDiskMaintenanceDisable() {
4652
+ const config = await loadConfig();
4653
+ const current = config.yandex?.diskMaintenance || {};
4654
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...current, enabled: false, lastRemote: "", updatedAt: new Date().toISOString() } } });
4655
+ deleteCronJobsByCommand("yandex disk-maintenance tick");
4656
+ return { enabled: false };
4657
+ }
4658
+
4659
+ async function yandexDiskMaintenanceTick(options = {}) {
4660
+ const config = await loadConfig();
4661
+ const disk = config.yandex?.diskMaintenance || {};
4662
+ if (!disk.enabled && !options.force) return { enabled: false };
4663
+ const info = await yandexDiskInfo();
4664
+ const docs = await yandexDocsList({ limit: 500 }).catch(() => []);
4665
+ const all = await yandexDiskListRecursive(CLOUD_DEFAULT_REMOTE_DIR, { depth: 4, limit: 500 }).catch(() => []);
4666
+ const publicLinks = [];
4667
+ for (const row of all.slice(0, 100)) {
4668
+ const statRow = await yandexDiskStat(row.path).catch(() => null);
4669
+ if (statRow?.publicUrl) publicLinks.push(statRow);
4670
+ }
4671
+ const text = [
4672
+ `# Проверка Яндекс Диска ${new Date().toISOString()}`,
4673
+ "",
4674
+ `Занято: ${formatBytes(info.usedSpace)} из ${formatBytes(info.totalSpace)}`,
4675
+ `Корзина: ${formatBytes(info.trashSize)}`,
4676
+ `Документов: ${docs.length}`,
4677
+ `Публичных ссылок в /IOLA: ${publicLinks.length}`,
4678
+ ...publicLinks.slice(0, 20).map((row, index) => `${index + 1}. ${row.path} — ${row.publicUrl}`),
4679
+ ].join("\n");
4680
+ const saved = await yandexDocsCreateText({ title: `disk-maintenance-${timestampForFile()}`, text, format: "md", confirm: true });
4681
+ await saveConfig({ yandex: { ...(config.yandex || {}), diskMaintenance: { ...disk, enabled: disk.enabled !== false, lastRunAt: new Date().toISOString(), lastRemote: saved.remote || "", lastDocs: docs.length, lastPublicLinks: publicLinks.length } } });
4682
+ return { enabled: true, ...info, docs: docs.length, publicLinks: publicLinks.length, remote: saved.remote || "" };
4683
+ }
4684
+
4685
+ function normalizeDigestTime(value) {
4686
+ const match = String(value || "").match(/^(\d{1,2})(?::(\d{2}))?$/u);
4687
+ if (!match) return "09:00";
4688
+ return `${String(Math.min(23, Number(match[1]))).padStart(2, "0")}:${String(Math.min(59, Number(match[2] || 0))).padStart(2, "0")}`;
4689
+ }
4690
+
4388
4691
  async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
4389
4692
  const config = await loadConfig();
4390
4693
  const safeDays = Math.max(1, Number(days || 7));
@@ -4588,6 +4891,47 @@ async function yandexMailCreateCalendarEvent(uid, args = {}) {
4588
4891
  });
4589
4892
  }
4590
4893
 
4894
+ async function yandexMailMeetingPack(uid, args = {}) {
4895
+ if (!args.confirm) throw new Error("Для пакетного сценария по письму нужен аргумент confirm=true.");
4896
+ if (!uid) throw new Error("UID письма обязателен.");
4897
+ const mailbox = await resolveYandexMailbox(args.mailbox || args.folder || "INBOX");
4898
+ const row = await yandexMailRead(uid, { mailbox, markSeen: true });
4899
+ if (!row || row.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
4900
+ const senderEmail = extractEmailAddress(row.from);
4901
+ const saved = await yandexMailSaveToDisk(uid, { mailbox });
4902
+ const shared = await yandexDiskShareWithQr(saved.remote, { confirm: true });
4903
+ const detected = extractDateTimeFromText(`${args.text || ""}\n${row.subject}\n${row.snippet}`);
4904
+ const start = args.start || detected.start || new Date(Date.now() + 3600000).toISOString();
4905
+ const end = args.end || detected.end || new Date(new Date(start).getTime() + 3600000).toISOString();
4906
+ const event = await yandexCalendarCreateEvent({
4907
+ title: args.title || `Встреча по письму: ${row.subject || `#${uid}`}`,
4908
+ description: [
4909
+ `Создано из письма #${uid}.`,
4910
+ `От: ${row.from || "-"}`,
4911
+ `Письмо сохранено: ${saved.remote}`,
4912
+ `Ссылка: ${shared.publicUrl}`,
4913
+ `QR-код: ${shared.qrPublicUrl}`,
4914
+ "",
4915
+ row.snippet || "",
4916
+ ].join("\n"),
4917
+ location: args.location || detected.location || "",
4918
+ start,
4919
+ end,
4920
+ attendees: senderEmail ? [senderEmail] : [],
4921
+ confirm: true,
4922
+ });
4923
+ let sent = null;
4924
+ if ((args.send || args.email) && senderEmail) {
4925
+ sent = await yandexMailSend({
4926
+ to: [senderEmail],
4927
+ subject: args.subject || `Материалы к встрече: ${row.subject || `письмо #${uid}`}`,
4928
+ text: [`Создал встречу и сохранил письмо на Яндекс Диск.`, `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
4929
+ confirm: true,
4930
+ });
4931
+ }
4932
+ 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 || [] };
4933
+ }
4934
+
4591
4935
  async function yandexMailSenderToContact(uid, args = {}) {
4592
4936
  if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
4593
4937
  if (!uid) throw new Error("UID письма обязателен.");
@@ -5645,6 +5989,47 @@ async function yandexContactFromPublicEntity(args = {}) {
5645
5989
  });
5646
5990
  }
5647
5991
 
5992
+ async function yandexContactFullPack(args = {}) {
5993
+ if (!args.confirm) throw new Error("Для полного сценария по контакту нужен аргумент confirm=true.");
5994
+ const resolved = await resolveYandexContact(args.query || args.contact || args.name || "", args);
5995
+ if (resolved.status !== "ok") return resolved;
5996
+ const contact = resolved.contact;
5997
+ const folder = await yandexContactCreateDiskFolder({ query: contact.email || contact.name || contact.phone, confirm: true, selectFirst: true });
5998
+ const noteText = [
5999
+ `# ${contact.name || contact.email || contact.phone}`,
6000
+ "",
6001
+ `Email: ${contact.email || "-"}`,
6002
+ `Телефон: ${contact.phone || "-"}`,
6003
+ `Адрес: ${contact.address || "-"}`,
6004
+ `Организация: ${contact.org || "-"}`,
6005
+ "",
6006
+ args.note || args.text || "Пакет контакта создан IOLA CLI.",
6007
+ ].join("\n");
6008
+ const doc = await yandexDocsCreateText({ path: path.posix.join(folder.remote, "meeting-note.md"), text: noteText, confirm: true });
6009
+ const shared = await yandexDiskShareWithQr(folder.remote, { confirm: true });
6010
+ const dateTime = args.start || args.date ? { start: args.start || args.date, end: args.end } : extractDateTimeFromText(args.text || args.source_question || "");
6011
+ let event = null;
6012
+ if (args.createEvent !== false && contact.email) {
6013
+ event = await yandexCalendarCreateEvent({
6014
+ ...dateTime,
6015
+ title: args.title || `Встреча: ${contact.name || contact.email}`,
6016
+ description: [`Папка контакта: ${folder.remote}`, `Ссылка: ${shared.publicUrl}`, args.note || ""].filter(Boolean).join("\n"),
6017
+ attendees: [contact.email],
6018
+ confirm: true,
6019
+ });
6020
+ }
6021
+ let sent = null;
6022
+ if ((args.send || args.email) && contact.email) {
6023
+ sent = await yandexMailSend({
6024
+ to: [contact.email],
6025
+ subject: args.subject || `Материалы IOLA: ${contact.name || contact.email}`,
6026
+ text: [args.message || "Подготовил материалы.", `Ссылка: ${shared.publicUrl}`, `QR-код: ${shared.qrPublicUrl}`].join("\n"),
6027
+ confirm: true,
6028
+ });
6029
+ }
6030
+ 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 || [] };
6031
+ }
6032
+
5648
6033
  async function resolveYandexContact(query, args = {}) {
5649
6034
  const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
5650
6035
  const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
@@ -11084,6 +11469,13 @@ function isCronDue(job) {
11084
11469
  return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
11085
11470
  }
11086
11471
  if (normalized.includes("каждый день") || normalized.includes("daily")) {
11472
+ const time = normalized.match(/(\d{1,2}):(\d{2})/u);
11473
+ if (time) {
11474
+ const dueAt = new Date(now);
11475
+ dueAt.setHours(Number(time[1]), Number(time[2]), 0, 0);
11476
+ const ranToday = lastRun && lastRun.toISOString().slice(0, 10) === now.toISOString().slice(0, 10);
11477
+ return now >= dueAt && !ranToday;
11478
+ }
11087
11479
  return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
11088
11480
  }
11089
11481
  if (normalized.includes("каждую неделю") || normalized.includes("weekly")) {
@@ -11635,6 +12027,49 @@ async function buildYandexDirectAnswer(question, history = []) {
11635
12027
  ].join("\n");
11636
12028
  }
11637
12029
 
12030
+ if (/(дайджест|сводк)/iu.test(normalized) && /(яндекс|почт|календар|контакт|диск)/iu.test(normalized)) {
12031
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized) && /(кажд|ежеднев|авто|регуляр)/iu.test(normalized)) {
12032
+ const time = question.match(/(\d{1,2}:\d{2})/u)?.[1] || "09:00";
12033
+ const result = await yandexDailyDigestEnable({ time, email: /email|почт[уы]|письм/iu.test(normalized), save: true });
12034
+ return `Ежедневный дайджест включен: каждый день ${result.time}.`;
12035
+ }
12036
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12037
+ await yandexDailyDigestDisable();
12038
+ return "Ежедневный дайджест выключен.";
12039
+ }
12040
+ const result = await yandexDailyDigestTick({ force: true, save: true, email: /отправь|email|почт[уы]/iu.test(normalized) });
12041
+ return [result.text, result.remote ? `\nСохранено: ${result.remote}` : ""].join("").trim();
12042
+ }
12043
+
12044
+ if (/(календар|событи|встреч)/iu.test(normalized) && /(напомин|уведом|следи|монитор|авто|регуляр)/iu.test(normalized)) {
12045
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
12046
+ const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
12047
+ await yandexCalendarRemindersEnable(minutes);
12048
+ return `Проверка календарных напоминаний включена: каждые ${minutes} минут.`;
12049
+ }
12050
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12051
+ await yandexCalendarRemindersDisable();
12052
+ return "Проверка календарных напоминаний выключена.";
12053
+ }
12054
+ const result = await yandexCalendarRemindersTick({ force: true });
12055
+ if (!result.events.length) return "Ближайших событий для напоминания нет.";
12056
+ return ["Ближайшие события:", ...result.events.map((row, index) => `${index + 1}. ${row.title || row.uid} — ${row.startIso || row.start || "-"}`)].join("\n");
12057
+ }
12058
+
12059
+ if (/(диск|яндекс.?диск|документ)/iu.test(normalized) && /(провер|обслуж|maintenance|аудит|регуляр|авто)/iu.test(normalized)) {
12060
+ if (/(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
12061
+ const days = Number(question.match(/(\d+)\s*(?:дн|день|дня|дней)/iu)?.[1] || 7);
12062
+ await yandexDiskMaintenanceEnable(days);
12063
+ return `Проверка Яндекс Диска включена: каждые ${days} дней.`;
12064
+ }
12065
+ if (/(выключ|отключ|останов|убери)/iu.test(normalized)) {
12066
+ await yandexDiskMaintenanceDisable();
12067
+ return "Проверка Яндекс Диска выключена.";
12068
+ }
12069
+ const result = await yandexDiskMaintenanceTick({ force: true });
12070
+ return `Проверка Яндекс Диска выполнена. Документов: ${result.docs}, публичных ссылок: ${result.publicLinks}. Отчет: ${result.remote}.`;
12071
+ }
12072
+
11638
12073
  if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
11639
12074
  return await buildYandexContactsDirectAnswer(question, normalized);
11640
12075
  }
@@ -11682,6 +12117,12 @@ async function buildYandexDirectAnswer(question, history = []) {
11682
12117
  const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
11683
12118
  return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
11684
12119
  }
12120
+ if (/(пакет|комплект|встреч).{0,80}(письм|письма)|(?:письм|письма).{0,80}(пакет|комплект|встреч)/iu.test(normalized) && /(диск|ссылк|qr|календар|встреч)/iu.test(normalized)) {
12121
+ const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
12122
+ if (!uid) return "Из какого письма создать пакет? Укажите номер из списка или UID.";
12123
+ const result = await yandexMailMeetingPack(uid, { mailbox: extractYandexMailboxName(question) || "INBOX", ...extractDateTimeFromText(question), confirm: true });
12124
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12125
+ }
11685
12126
  if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
11686
12127
  const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
11687
12128
  if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
@@ -12122,6 +12563,11 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
12122
12563
  const result = await yandexContactCreateDiskFolder({ query, confirm: true });
12123
12564
  return formatToolResult({ rows: [result], outputs: [] }, {});
12124
12565
  }
12566
+ if (/(полный|комплект|пакет|подготовь).{0,80}(контакт|клиент|человек)/iu.test(text) || /(контакт|клиент|человек).{0,80}(полный|комплект|пакет)/iu.test(text)) {
12567
+ const query = cleanupYandexContactActionQuery(question);
12568
+ const result = await yandexContactFullPack({ query, ...extractDateTimeFromText(question), note: extractShareMessage(question), confirm: true });
12569
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12570
+ }
12125
12571
  if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
12126
12572
  const remotePath = extractCloudPath(question);
12127
12573
  const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
@@ -12156,6 +12602,27 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
12156
12602
  async function buildUserSkillDirectAnswer(question) {
12157
12603
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
12158
12604
  if (!/(skill|скилл|скил|навык)/iu.test(normalized)) return "";
12605
+ if (/(шаблон|template|вариант)/iu.test(normalized) && /(покажи|список|какие|list)/iu.test(normalized)) {
12606
+ const rows = userSkillTemplates();
12607
+ return ["Шаблоны skills:", ...rows.map((row) => `- ${row.name}: ${row.description}`)].join("\n");
12608
+ }
12609
+ if (/(preview|предпросмотр|покажи\s+как)/iu.test(normalized)) {
12610
+ const name = extractUserSkillNameFromQuestion(question) || "user-skill";
12611
+ const template = normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || "");
12612
+ return buildUserSkillPreview({ name, template, instructions: question });
12613
+ }
12614
+ if (/(проверь|validate|doctor|валидац)/iu.test(normalized)) {
12615
+ const name = extractUserSkillNameFromQuestion(question);
12616
+ if (!name) return "Какой skill проверить? Укажите имя.";
12617
+ const result = await userSkillValidate(name);
12618
+ return ["Проверка skill:", ...result.checks.map((row) => `${row.status}: ${row.check} - ${row.message}`)].join("\n");
12619
+ }
12620
+ if (/(обнови|измени|update|edit)/iu.test(normalized)) {
12621
+ const name = extractUserSkillNameFromQuestion(question);
12622
+ if (!name) return "Какой skill обновить? Укажите имя.";
12623
+ const result = await userSkillUpdate(name, { instructions: question, tools: inferUserSkillTools(question), confirm: true });
12624
+ return `Skill обновлен: ${result.name}\nФайл: ${result.file}`;
12625
+ }
12159
12626
  if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
12160
12627
  const name = extractUserSkillNameFromQuestion(question) || "user-skill";
12161
12628
  const result = await userSkillCreate({
@@ -12163,6 +12630,7 @@ async function buildUserSkillDirectAnswer(question) {
12163
12630
  description: extractUserSkillDescription(question, name),
12164
12631
  instructions: question,
12165
12632
  tools: inferUserSkillTools(question),
12633
+ template: normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || ""),
12166
12634
  enable: true,
12167
12635
  confirm: true,
12168
12636
  });
@@ -13415,9 +13883,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
13415
13883
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
13416
13884
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
13417
13885
  "Минимальные 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.",
13886
+ "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_daily_digest {save,email}, yandex_calendar_reminders_tick {}, yandex_disk_maintenance_tick {}.",
13887
+ "Опасные 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.",
13888
+ "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
13889
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
13422
13890
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
13423
13891
  `Вопрос: ${question}`,
@@ -13497,6 +13965,12 @@ function inferToolPlan(question, options = {}) {
13497
13965
  }],
13498
13966
  };
13499
13967
  }
13968
+ if (/(шаблон|template)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
13969
+ return { steps: [{ tool: "user_skill_templates", args: {} }] };
13970
+ }
13971
+ if (/(проверь|validate|doctor)/iu.test(normalized) && /(skill|скилл|скил|навык)/iu.test(normalized)) {
13972
+ return { steps: [{ tool: "user_skill_validate", args: { name: extractUserSkillNameFromQuestion(question) } }] };
13973
+ }
13500
13974
  if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
13501
13975
  return { steps: [{ tool: "yandex_identity_me", args: {} }] };
13502
13976
  }
@@ -13527,6 +14001,7 @@ function inferToolPlan(question, options = {}) {
13527
14001
  if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
13528
14002
  if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
13529
14003
  if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_mark", args: { uid, mailbox, seen: !/непрочитан/iu.test(normalized) } }] };
14004
+ if (/(пакет|комплект)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_meeting_pack", args: { uid, mailbox, ...extractDateTimeFromText(question), confirm: true } }] };
13530
14005
  if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
13531
14006
  if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
13532
14007
  return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
@@ -13542,6 +14017,7 @@ function inferToolPlan(question, options = {}) {
13542
14017
  return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
13543
14018
  }
13544
14019
  if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
14020
+ if (/(напомин|уведом|следи|монитор)/iu.test(normalized) && /(проверь|tick|сейчас)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_reminders_tick", args: { force: true } }] };
13545
14021
  if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
13546
14022
  const dateTime = extractDateTimeFromText(question);
13547
14023
  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 +14033,7 @@ function inferToolPlan(question, options = {}) {
13557
14033
  if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
13558
14034
  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
14035
  if (/(создай|добавь|запиши|сохрани)\s+контакт/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_create", args: { ...parseYandexContactCreateRequest(question), confirm: true } }] };
14036
+ if (/(полный|комплект|пакет)/iu.test(normalized)) return { steps: [{ tool: "yandex_contact_full_pack", args: { contact: cleanupYandexContactActionQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
13560
14037
  if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
13561
14038
  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
14039
  if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
@@ -14148,6 +14625,9 @@ function formatToolResult(result, options) {
14148
14625
  }
14149
14626
  if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
14150
14627
  if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
14628
+ if (row.status === "mail-meeting-pack-created") return `Пакет по письму #${row.uid} создан.\nПисьмо: ${row.saved}\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}\nСобытие: ${row.event || "-"}`;
14629
+ 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 || "-"}`;
14630
+ if (row.enabled && row.text && (row.unread !== undefined || row.events !== undefined)) return row.text;
14151
14631
  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
14632
  if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
14153
14633
  if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
@@ -15712,12 +16192,12 @@ function parseOptions(args) {
15712
16192
 
15713
16193
  for (let index = 0; index < args.length; index += 1) {
15714
16194
  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") {
16195
+ 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
16196
  result[arg.slice(2)] = true;
15717
16197
  } else if (arg === "--check" || arg === "--upgrade-node") {
15718
16198
  result.check = true;
15719
16199
  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") {
16200
+ } 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") {
15721
16201
  result[arg.slice(2)] = args[index + 1];
15722
16202
  index += 1;
15723
16203
  } else {
@@ -15941,9 +16421,13 @@ function printSkillsList(skills, config) {
15941
16421
  async function executeUserSkillTool(tool, args = {}) {
15942
16422
  if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
15943
16423
  if (tool === "user_skill_create") return userSkillCreate({ ...args, confirm: args.confirm === true });
16424
+ if (tool === "user_skill_update") return userSkillUpdate(args.name, { ...args, confirm: args.confirm === true });
15944
16425
  if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
15945
16426
  if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
15946
16427
  if (tool === "user_skill_delete") return userSkillDelete(args.name, { confirm: args.confirm === true });
16428
+ if (tool === "user_skill_templates") return userSkillTemplates();
16429
+ if (tool === "user_skill_validate") return userSkillValidate(args.name);
16430
+ if (tool === "user_skill_preview") return { status: "preview", text: buildUserSkillPreview(args) };
15947
16431
  throw new Error(`Неизвестный user skill tool: ${tool}`);
15948
16432
  }
15949
16433
 
@@ -15951,22 +16435,41 @@ async function userSkillCreate(args = {}) {
15951
16435
  if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
15952
16436
  const name = normalizeUserSkillName(args.name || args.skill || args.title);
15953
16437
  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();
16438
+ const template = getUserSkillTemplate(args.template);
16439
+ const description = String(args.description || template?.description || `Пользовательский skill: ${name}`).trim();
16440
+ const instructions = String(args.instructions || args.text || args.prompt || template?.instructions || "").trim();
15956
16441
  if (!instructions) throw new Error("Инструкции skill обязательны.");
15957
16442
  const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
16443
+ const mergedTools = [...new Set([...(template?.tools || []), ...tools])];
15958
16444
  const dir = path.join(USER_SKILLS_DIR, name);
15959
16445
  const file = path.join(dir, "SKILL.md");
15960
16446
  if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
15961
16447
  await mkdir(dir, { recursive: true });
15962
- const body = buildUserSkillMarkdown({ name, description, instructions, tools });
16448
+ const body = buildUserSkillMarkdown({ name, description, instructions, tools: mergedTools });
15963
16449
  await writeFile(file, body, "utf8");
15964
16450
  let enabled = false;
15965
16451
  if (args.enable) {
15966
16452
  await userSkillSetEnabled(name, true);
15967
16453
  enabled = true;
15968
16454
  }
15969
- return { name, description, file, enabled, tools, status: "created" };
16455
+ return { name, description, file, enabled, tools: mergedTools, status: "created" };
16456
+ }
16457
+
16458
+ async function userSkillUpdate(name, args = {}) {
16459
+ if (!args.confirm) throw new Error("Для обновления пользовательского skill нужен confirm=true.");
16460
+ const skillName = normalizeUserSkillName(name || args.name || args.skill);
16461
+ if (!skillName) throw new Error("Имя skill обязательно.");
16462
+ const file = path.join(USER_SKILLS_DIR, skillName, "SKILL.md");
16463
+ if (!existsSync(file)) throw new Error(`Пользовательский skill не найден: ${skillName}`);
16464
+ const current = await readFile(file, "utf8");
16465
+ const meta = readSkillMeta(file);
16466
+ const description = String(args.description || meta.description || `Пользовательский skill: ${skillName}`).trim();
16467
+ const instructions = String(args.instructions || args.text || args.prompt || stripFrontmatter(current)).trim();
16468
+ const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions));
16469
+ const body = buildUserSkillMarkdown({ name: skillName, description, instructions, tools });
16470
+ await writeFile(file, body, "utf8");
16471
+ if (args.enable) await userSkillSetEnabled(skillName, true);
16472
+ return { name: skillName, description, file, tools, status: "updated" };
15970
16473
  }
15971
16474
 
15972
16475
  async function userSkillSetEnabled(name, enabled) {
@@ -16060,9 +16563,23 @@ function inferUserSkillTools(question) {
16060
16563
  }
16061
16564
  if (/(календар|событи|встреч|телемост)/iu.test(text)) {
16062
16565
  tools.add("yandex_calendar_list");
16566
+ tools.add("yandex_calendar_search");
16063
16567
  tools.add("yandex_calendar_create_event");
16568
+ tools.add("yandex_calendar_update");
16569
+ tools.add("yandex_calendar_move");
16570
+ tools.add("yandex_calendar_delete");
16571
+ tools.add("yandex_calendar_add_reminder");
16064
16572
  tools.add("yandex_telemost_create_event");
16065
16573
  }
16574
+ if (/(документ|docs|360)/iu.test(text)) {
16575
+ tools.add("yandex_docs_list");
16576
+ tools.add("yandex_docs_find");
16577
+ tools.add("yandex_docs_create_text");
16578
+ tools.add("yandex_docs_read");
16579
+ tools.add("yandex_docs_share");
16580
+ tools.add("yandex_docs_rename");
16581
+ tools.add("yandex_docs_delete");
16582
+ }
16066
16583
  if (/(контакт|адресн)/iu.test(text)) {
16067
16584
  tools.add("yandex_contacts_search");
16068
16585
  tools.add("yandex_contacts_get");
@@ -16103,6 +16620,69 @@ function inferUserSkillTools(question) {
16103
16620
  return [...tools];
16104
16621
  }
16105
16622
 
16623
+ function userSkillTemplates() {
16624
+ return [
16625
+ {
16626
+ name: "mail-triage",
16627
+ description: "Разбор почты: найти важные письма, прочитать, сохранить, создать задачу или событие.",
16628
+ tools: ["yandex_mail_list", "yandex_mail_search", "yandex_mail_read", "yandex_mail_save_to_disk", "yandex_mail_create_calendar_event", "yandex_mail_create_task"],
16629
+ instructions: "Помогай разбирать почту пользователя. Сначала показывай краткую сводку, затем выполняй только явно запрошенные действия: чтение, сохранение письма на Диск, создание события или задачи.",
16630
+ },
16631
+ {
16632
+ name: "family-calendar",
16633
+ description: "Семейный календарь: события, напоминания, встречи и ежедневные проверки.",
16634
+ 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"],
16635
+ instructions: "Помогай вести личный календарь. Для изменения календаря требуй явную просьбу пользователя. Всегда называй дату и время события.",
16636
+ },
16637
+ {
16638
+ name: "docs-organizer",
16639
+ description: "Организация документов на Яндекс Диске.",
16640
+ 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"],
16641
+ instructions: "Помогай искать, создавать и приводить в порядок документы на Яндекс Диске. Не путай облачные документы с локальными файлами на компьютере.",
16642
+ },
16643
+ {
16644
+ name: "contact-workflow",
16645
+ description: "Работа с контактами, письмами, встречами и папками контактов.",
16646
+ 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"],
16647
+ instructions: "Помогай выполнять действия вокруг контакта: найти карточку, отправить письмо, создать папку, отправить ссылку или создать встречу. Если найдено несколько контактов, проси уточнение.",
16648
+ },
16649
+ ];
16650
+ }
16651
+
16652
+ function getUserSkillTemplate(name) {
16653
+ const normalized = normalizeUserSkillName(name || "");
16654
+ return userSkillTemplates().find((item) => item.name === normalized) || null;
16655
+ }
16656
+
16657
+ function buildUserSkillPreview(args = {}) {
16658
+ const name = normalizeUserSkillName(args.name || args.skill || args.title || "user-skill");
16659
+ const template = getUserSkillTemplate(args.template);
16660
+ const description = args.description || template?.description || `Пользовательский skill: ${name}`;
16661
+ const instructions = args.instructions || args.text || args.prompt || template?.instructions || "Опишите, что должен делать skill.";
16662
+ const tools = [...new Set([...(template?.tools || []), ...parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || inferUserSkillTools(instructions))])];
16663
+ return buildUserSkillMarkdown({ name, description, instructions, tools });
16664
+ }
16665
+
16666
+ async function userSkillValidate(name) {
16667
+ const config = await loadConfig();
16668
+ const skill = findSkill(normalizeUserSkillName(name), config);
16669
+ const checks = [];
16670
+ if (!skill) {
16671
+ return { status: "error", checks: [{ check: "exists", status: "error", message: `Skill не найден: ${name || "-"}` }] };
16672
+ }
16673
+ const text = await readFile(skill.file, "utf8");
16674
+ const meta = readSkillMeta(skill.file);
16675
+ checks.push({ check: "exists", status: "ok", message: skill.file });
16676
+ checks.push({ check: "name", status: meta.name ? "ok" : "error", message: meta.name || "Нет name во frontmatter" });
16677
+ checks.push({ check: "description", status: meta.description ? "ok" : "warn", message: meta.description || "Описание пустое" });
16678
+ checks.push({ check: "instructions", status: stripFrontmatter(text).trim().length >= 40 ? "ok" : "warn", message: "Инструкции должны быть понятными и достаточно подробными" });
16679
+ checks.push({ check: "secrets", status: /(token|api[_-]?key|парол|секрет)\s*[:=]\s*\S{8,}/iu.test(text) ? "error" : "ok", message: "Skill не должен содержать секреты" });
16680
+ const tools = [...text.matchAll(/`([a-z0-9_:-]+)`/giu)].map((match) => match[1]).filter((tool) => ALL_TOOL_ALIASES.includes(tool) || tool.startsWith("mcp:"));
16681
+ const unknownTools = tools.filter((tool) => !ALL_TOOL_ALIASES.includes(tool) && !tool.startsWith("mcp:"));
16682
+ checks.push({ check: "tools", status: unknownTools.length ? "warn" : "ok", message: unknownTools.length ? `Неизвестные tools: ${unknownTools.join(", ")}` : `${tools.length} tools` });
16683
+ return { status: checks.some((row) => row.status === "error") ? "error" : "ok", checks };
16684
+ }
16685
+
16106
16686
  function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
16107
16687
  const toolLines = tools.length
16108
16688
  ? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
@@ -1,35 +1,53 @@
1
- # Daemon, RPC и cron
2
-
3
- Локальный daemon запускает HTTP/RPC endpoint на компьютере пользователя:
4
-
5
- ```bash
6
- iola daemon status
7
- iola daemon start
8
- ```
9
-
10
- По умолчанию endpoint:
11
-
12
- ```text
13
- http://127.0.0.1:18790
14
- ```
1
+ # Daemon, RPC и cron
2
+
3
+ Локальный daemon запускает HTTP/RPC endpoint на компьютере пользователя:
4
+
5
+ ```bash
6
+ iola daemon status
7
+ iola daemon start
8
+ ```
9
+
10
+ По умолчанию endpoint:
11
+
12
+ ```text
13
+ http://127.0.0.1:18790
14
+ ```
15
+
16
+ RPC-команды:
17
+
18
+ ```bash
19
+ iola rpc call status
20
+ iola rpc call search --query Петрова --dataset schools
21
+ iola rpc call card --query "школа 29"
22
+ iola rpc call quality
23
+ ```
24
+
25
+ Cron-задачи:
26
+
27
+ ```bash
28
+ iola cron add "каждый день 09:00 -- quality"
29
+ iola cron list
30
+ iola cron run 1
31
+ iola cron tick
32
+ iola cron delete 1
33
+ ```
34
+
35
+ `cron tick` проверяет задачи, которые пора выполнить. Его можно запускать вручную, через Windows Task Scheduler или другой планировщик.
15
36
 
16
- RPC-команды:
37
+ ## Готовые cron-сценарии Яндекса
17
38
 
18
39
  ```bash
19
- iola rpc call status
20
- iola rpc call search --query Петрова --dataset schools
21
- iola rpc call card --query "школа 29"
22
- iola rpc call quality
40
+ iola yandex mail-watch on --minutes 5
41
+ iola yandex daily-digest on --time 09:00
42
+ iola yandex calendar-reminders on --minutes 15
43
+ iola yandex contacts-maintenance on --days 7 --backup
44
+ iola yandex disk-maintenance on --days 7
23
45
  ```
24
46
 
25
- Cron-задачи:
26
-
27
- ```bash
28
- iola cron add "каждый день 09:00 -- quality"
29
- iola cron list
30
- iola cron run 1
31
- iola cron tick
32
- iola cron delete 1
33
- ```
47
+ - `mail-watch` проверяет новые письма.
48
+ - `daily-digest` собирает сводку по почте, календарю и контактам, сохраняет Markdown-документ на Яндекс Диск.
49
+ - `calendar-reminders` проверяет ближайшие события календаря.
50
+ - `contacts-maintenance` ищет неполные карточки и дубликаты, опционально делает backup.
51
+ - `disk-maintenance` проверяет место, документы и публичные ссылки в папке `/IOLA`, сохраняет отчет на Диск.
34
52
 
35
- `cron tick` проверяет задачи, которые пора выполнить. Его можно запускать вручную, через Windows Task Scheduler или другой планировщик.
53
+ Чтобы расписания выполнялись автоматически, должен регулярно запускаться `iola cron tick`: вручную, через daemon или системный планировщик.
@@ -53,12 +53,27 @@ Skill - это инструкция для агента, а не произво
53
53
 
54
54
  - `user_skill_list` - список пользовательских skills;
55
55
  - `user_skill_create` - создать skill;
56
+ - `user_skill_update` - обновить существующий skill;
57
+ - `user_skill_templates` - показать готовые шаблоны;
58
+ - `user_skill_preview` - показать будущий `SKILL.md` до записи;
59
+ - `user_skill_validate` - проверить структуру и безопасность skill;
56
60
  - `user_skill_enable` - включить skill;
57
61
  - `user_skill_disable` - выключить skill;
58
62
  - `user_skill_delete` - удалить пользовательский skill.
59
63
 
60
64
  При создании через AI пользователь может написать обычным языком: `создай скилл, который при просьбе "сводка почты" берет непрочитанные письма и группирует по отправителям`. CLI создаст локальный `SKILL.md` и включит его.
61
65
 
66
+ Команды:
67
+
68
+ ```bash
69
+ iola skills templates
70
+ iola skills preview family --template family-calendar
71
+ iola skills create family --template family-calendar --enable
72
+ iola skills validate family
73
+ iola skills update family --instructions "..."
74
+ iola skills delete family --yes
75
+ ```
76
+
62
77
  Для опасных действий внутри пользовательского skill нужно явное подтверждение пользователя: отправка писем, удаление данных, публикация ссылок, запись файлов и изменение внешних сервисов.
63
78
 
64
79
  ## Yandex toolset
@@ -71,6 +86,7 @@ Toolset `yandex` включает локальные tools для пользов
71
86
  - Яндекс Календарь: календари, список, поиск, создание, перенос, редактирование, повторы, напоминания, удаление событий;
72
87
  - Яндекс Документы / 360: работа через Яндекс Диск - список, поиск, создание текстовых документов, чтение, ссылка/QR, переименование, удаление;
73
88
  - Яндекс Контакты: статус, список, поиск, карточка, создание, обновление, удаление, импорт/экспорт, дубликаты, неполные карточки, backup на Диск, дни рождения в календарь, письмо/ссылка/встреча по контакту;
89
+ - Комбинированные сценарии: пакет по письму, полный пакет по контакту, ежедневный дайджест, календарные напоминания, аудит Яндекс Диска;
74
90
  - Телемост: попытка прямого API и fallback через календарное событие, если API недоступен аккаунту.
75
91
 
76
92
  Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок, создание/изменение документов и создание/изменение/удаление событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
@@ -182,9 +182,21 @@ QR-код:
182
182
  - `yandex_contact_create_calendar_event` - создать встречу с контактом;
183
183
  - `yandex_contact_create_telemost_event` - создать событие для Телемоста с контактом;
184
184
  - `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя;
185
+ - `yandex_contact_full_pack` - собрать полный пакет по контакту: папка на Диске, заметка, публичная ссылка/QR, событие календаря и опциональное письмо;
186
+ - `yandex_mail_meeting_pack` - собрать пакет по письму: сохранить письмо на Диск, создать ссылку/QR, поставить встречу в календарь и опционально отправить ссылку отправителю;
185
187
  - `yandex_telemost_status` - проверить режим Телемоста;
186
188
  - `yandex_telemost_create_event` - создать встречу: прямой Telemost API используется только если он доступен аккаунту; иначе создается календарное событие без выдуманной ссылки.
187
189
 
190
+ Комбинированные сценарии:
191
+
192
+ ```bash
193
+ iola ask "по последнему письму создай встречу завтра в 14:00, сохрани письмо на диск и пришли отправителю ссылку"
194
+ iola ask "собери полный пакет по контакту Петров: папка на диске, заметка, встреча завтра в 12 и ссылка с QR"
195
+ iola ask "создай папку для школы 2 на яндекс диске и отправь ссылку контакту Иванов"
196
+ ```
197
+
198
+ CLI использует уже подключенные сервисы: Почту, Диск, Контакты и Календарь. Если не хватает email, найдено несколько контактов или неясна дата встречи, CLI должен уточнить, а не выбирать случайно.
199
+
188
200
  Регулярная проверка контактов:
189
201
 
190
202
  ```bash
@@ -197,6 +209,32 @@ iola yandex contacts-maintenance off
197
209
 
198
210
  Проверка ищет дубликаты и неполные карточки. Если включен `--backup`, при tick сохраняется CSV-копия контактов на Яндекс Диск.
199
211
 
212
+ Регулярная автоматизация:
213
+
214
+ ```bash
215
+ iola yandex daily-digest on --time 09:00
216
+ iola yandex daily-digest on --time 09:00 --email
217
+ iola yandex daily-digest status
218
+ iola yandex daily-digest tick
219
+ iola yandex daily-digest off
220
+
221
+ iola yandex calendar-reminders on --minutes 15
222
+ iola yandex calendar-reminders status
223
+ iola yandex calendar-reminders tick
224
+ iola yandex calendar-reminders off
225
+
226
+ iola yandex disk-maintenance on --days 7
227
+ iola yandex disk-maintenance status
228
+ iola yandex disk-maintenance tick
229
+ iola yandex disk-maintenance off
230
+ ```
231
+
232
+ `daily-digest` собирает короткую сводку: непрочитанные письма, события ближайших суток и неполные контакты. По умолчанию сводка сохраняется документом в `/IOLA/docs`; с `--email` дополнительно отправляется на email подключенного Yandex ID.
233
+
234
+ `calendar-reminders` проверяет события в ближайшие минуты и показывает напоминания без повторов по уже увиденным событиям.
235
+
236
+ `disk-maintenance` проверяет место на Диске, публичные ссылки и состояние папки `/IOLA`, затем сохраняет отчет в `/IOLA/docs`.
237
+
200
238
  Включение сервиса в `/yandex` разрешает CLI использовать соответствующую категорию. Отправка письма, удаление файлов, публикация ссылок и создание событий требуют явного намерения пользователя и подтверждения в tool-вызове.
201
239
 
202
240
  ## Иконка приложения
@@ -322,6 +322,7 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
322
322
  - создать папку контакта на Яндекс Диске и сохранить туда `contact.vcf` и `README.txt`;
323
323
  - создать встречу с контактом в Яндекс Календаре;
324
324
  - создать календарное событие для Телемоста с контактом;
325
+ - создать полный пакет контакта: папка на Диске, Markdown-документ, публичная ссылка, QR-код и встреча в календаре;
325
326
  - создать контакт из открытого городского слоя, если у школы или детского сада есть публичный email или телефон.
326
327
 
327
328
  Примеры:
@@ -329,9 +330,26 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
329
330
  - `отправь Петрову письмо текст: встреча завтра в 14:00`;
330
331
  - `отправь Иванову ссылку и QR-код на /IOLA/report.pdf`;
331
332
  - `создай папку на Яндекс Диске для контакта Иван Петров`;
333
+ - `подготовь полный пакет по контакту Иван Петров завтра в 15:00`;
332
334
  - `создай встречу с Иваном Петровым завтра в 15:00`;
333
335
  - `создай контакт из школы № 7`.
334
336
 
337
+ ### yandex-mail-combined
338
+
339
+ Комбинированные сценарии по письмам:
340
+
341
+ - сохранить письмо на Яндекс Диск в Markdown;
342
+ - создать публичную ссылку и QR-код на сохраненное письмо;
343
+ - создать событие календаря по письму;
344
+ - пригласить отправителя письма как участника, если в отправителе есть email;
345
+ - при явной просьбе отправить отправителю ссылку и QR-код.
346
+
347
+ Примеры:
348
+
349
+ - `по письму #2382 создай пакет: сохрани на диск, сделай ссылку и событие`;
350
+ - `по самому свежему письму подготовь встречу завтра в 14:00`;
351
+ - `сохрани письмо #2382 на Диск и создай событие`.
352
+
335
353
  ### yandex-telemost
336
354
 
337
355
  Подготовить встречу через Телемост:
@@ -342,6 +360,24 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
342
360
 
343
361
  CLI не должен сам нажимать финальные кнопки в веб-интерфейсе Яндекса без явного действия пользователя и не должен выдумывать ссылку Телемоста.
344
362
 
363
+ ### yandex-automation
364
+
365
+ Регулярные сценарии через локальный cron:
366
+
367
+ - автоопрос новых писем;
368
+ - ежедневный дайджест: непрочитанные письма, события на 24 часа, неполные контакты, сохранение отчета на Диск;
369
+ - календарные напоминания: проверка ближайших событий;
370
+ - регулярная проверка контактов: неполные карточки, дубликаты, backup;
371
+ - аудит Яндекс Диска: место, корзина, документы и публичные ссылки в `/IOLA`.
372
+
373
+ Команды:
374
+
375
+ - `iola yandex mail-watch on --minutes 5`;
376
+ - `iola yandex daily-digest on --time 09:00`;
377
+ - `iola yandex calendar-reminders on --minutes 15`;
378
+ - `iola yandex contacts-maintenance on --days 7 --backup`;
379
+ - `iola yandex disk-maintenance on --days 7`.
380
+
345
381
  ## Yandex Connector backlog
346
382
 
347
383
  Эти сценарии зафиксированы для следующего этапа. Они не должны оформлять заказ, списывать деньги или нажимать финальную кнопку вместо пользователя.