@iola_adm/iola-cli 0.2.34 → 0.2.35

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 по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты, события Календаря, расширенные Яндекс Контакты (поиск, создание, обновление, удаление, импорт/экспорт, дубликаты, backup на Диск, дни рождения в календарь, регулярная contacts-maintenance проверка) и комбинированные сценарии: письмо контакту, ссылка+QR контакту, папка контакта на Диске, встреча/Телемост с контактом. Отправка письма, удаление/перемещение файлов, публикация ссылок, изменение контактов и создание событий требуют явного подтверждения.
205
+ Yandex tools уже доступны: профиль Yandex ID, расширенная работа с Яндекс Диском (место, список, поиск, карточка, чтение текста, папки, загрузка, скачивание, ссылки, QR-коды к публичным ссылкам, отправка ссылки и QR по почте, перемещение, копирование, переименование, корзина), статус/список/поиск/чтение/отправка Яндекс Почты, полноценная работа с Календарем через CalDAV (календари, список, поиск, создание, перенос, редактирование, напоминания, повторы, удаление), Яндекс Документы/360 через Диск (создание текстовых документов, поиск, чтение, ссылки/QR, переименование, удаление), расширенные Яндекс Контакты (поиск, создание, обновление, удаление, импорт/экспорт, дубликаты, backup на Диск, дни рождения в календарь, регулярная contacts-maintenance проверка) и комбинированные сценарии: письмо контакту, ссылка+QR контакту, папка контакта на Диске, встреча/Телемост с контактом. Телемост пытается использовать прямой API, а если он недоступен текущему аккаунту, честно создает календарное событие без выдуманной ссылки. Отправка письма, удаление/перемещение файлов, публикация ссылок, изменение контактов, документов и событий требуют явного подтверждения.
206
206
 
207
207
  Инструкция: [Yandex Connector](https://github.com/adm-iola/iola-cli/wiki/Yandex-Connector).
208
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
@@ -50,8 +50,25 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
50
50
  - `yandex_mail_create_task` - создать локальную задачу по письму.
51
51
  - Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
52
52
  - `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
53
- - `yandex_calendar_list` - показать события.
54
- - `yandex_calendar_create_event` - создать событие.
53
+ - `yandex_calendar_calendars` - показать доступные календари.
54
+ - `yandex_calendar_list` - показать ближайшие события.
55
+ - `yandex_calendar_search` - найти событие по названию, описанию, месту, UID или дате.
56
+ - `yandex_calendar_get` - открыть карточку события.
57
+ - `yandex_calendar_create_event` - создать событие с участниками, местом и напоминаниями.
58
+ - `yandex_calendar_create_recurring_event` - создать повторяющееся событие.
59
+ - `yandex_calendar_update` - изменить название, описание, место, время, участников, повторы или напоминания.
60
+ - `yandex_calendar_move` - перенести событие на новую дату/время.
61
+ - `yandex_calendar_add_reminder` - добавить напоминание к событию.
62
+ - `yandex_calendar_delete` - удалить событие.
63
+ - `yandex_docs_status` - проверить работу Яндекс Документов / 360 через Диск.
64
+ - `yandex_docs_list` - показать документы на Яндекс Диске.
65
+ - `yandex_docs_find` - найти документ по названию.
66
+ - `yandex_docs_create_text` - создать текстовый/Markdown/HTML-документ на Яндекс Диске.
67
+ - `yandex_docs_read` - прочитать небольшой текстовый документ.
68
+ - `yandex_docs_share` - создать публичную ссылку и QR-код на документ.
69
+ - `yandex_docs_rename` - переименовать документ.
70
+ - `yandex_docs_delete` - удалить документ.
71
+ - `yandex_docs_save_answer` - сохранить текст ответа как документ.
55
72
  - `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
56
73
  - `yandex_contacts_list` - показать контакты.
57
74
  - `yandex_contacts_search` - найти контакт по имени, email, телефону, организации, адресу или заметке.
@@ -81,7 +98,8 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
81
98
  - `yandex_contact_create_calendar_event` - создать встречу с контактом.
82
99
  - `yandex_contact_create_telemost_event` - создать календарное событие для Телемоста с контактом.
83
100
  - `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя, если у организации есть email или телефон.
84
- - `yandex_telemost_create_event` - создать календарное событие для встречи.
101
+ - `yandex_telemost_status` - проверить режим Телемоста.
102
+ - `yandex_telemost_create_event` - создать встречу: если прямой Telemost API доступен аккаунту, добавить ссылку; иначе создать календарное событие с честным fallback.
85
103
  - Регулярная проверка контактов включается командой `iola yandex contacts-maintenance on --days 7`, выключается `iola yandex contacts-maintenance off`, ручная проверка `iola yandex contacts-maintenance tick`. Флаг `--backup` включает backup контактов на Диск при tick.
86
104
 
87
105
  Комбинированные сценарии:
@@ -92,6 +110,9 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
92
110
  - Если получатель указан именем, сначала ищи его в контактах. Если контактов несколько или email не найден, попроси уточнение.
93
111
  - Если пользователь просит отправить письмо контакту, создать встречу с контактом или отправить ссылку контакту, используй специализированные `yandex_contact_*` tools, а не ручную цепочку из нескольких tools.
94
112
  - Если пользователь просит создать папку для контакта на Яндекс Диске, используй `yandex_contact_create_disk_folder`.
113
+ - Если пользователь просит создать/найти/прочитать/переименовать/удалить документ именно на Яндекс Диске или в Яндекс 360, используй `yandex_docs_*`, а не общий список файлов.
114
+ - Если пользователь просит перенести событие, добавить напоминание или удалить встречу, используй `yandex_calendar_move`, `yandex_calendar_add_reminder`, `yandex_calendar_delete`.
115
+ - Если пользователь просит Телемост, используй `yandex_telemost_create_event`. Не обещай ссылку Телемоста, если API ее не вернул.
95
116
  - Если пользователь просит резервную копию контактов, используй `yandex_contacts_backup_to_disk`.
96
117
  - Если пользователь просит регулярную проверку контактов, дубликатов или неполных карточек, используй `iola yandex contacts-maintenance ...`.
97
118
  - Если пользователь просит импорт/экспорт контактов, используй CSV или vCard по расширению файла или явному формату.
@@ -100,7 +121,7 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
100
121
 
101
122
  Безопасность:
102
123
 
103
- - Для отправки письма, ответа на письмо, удаления письма, удаления файлов, перемещения/копирования/переименования объектов, восстановления/очистки корзины, публикации ссылки, создания QR-ссылки, отправки ссылки по почте и создания событий нужен явный запрос пользователя и `confirm=true`.
124
+ - Для отправки письма, ответа на письмо, удаления письма, удаления файлов, перемещения/копирования/переименования объектов, восстановления/очистки корзины, публикации ссылки, создания QR-ссылки, отправки ссылки по почте, создания/изменения/удаления событий и создания/изменения/удаления документов нужен явный запрос пользователя и `confirm=true`.
104
125
  - Для пересылки письма, создания контакта, создания события из письма и добавления отправителя в контакты также нужен явный запрос пользователя и `confirm=true`.
105
126
  - Не отправляй письма, не отвечай на письма, не пересылай письма, не создавай контакты/события, не удаляй письма/файлы, не перемещай/копируй/переименовывай объекты и не публикуй ссылки по косвенному намерению.
106
127
  - Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
package/src/cli.js CHANGED
@@ -193,8 +193,25 @@ const YANDEX_TOOLS = [
193
193
  "yandex_mail_map_addresses",
194
194
  "yandex_mail_create_task",
195
195
  "yandex_calendar_status",
196
+ "yandex_calendar_calendars",
196
197
  "yandex_calendar_create_event",
197
198
  "yandex_calendar_list",
199
+ "yandex_calendar_get",
200
+ "yandex_calendar_search",
201
+ "yandex_calendar_update",
202
+ "yandex_calendar_move",
203
+ "yandex_calendar_delete",
204
+ "yandex_calendar_create_recurring_event",
205
+ "yandex_calendar_add_reminder",
206
+ "yandex_docs_status",
207
+ "yandex_docs_list",
208
+ "yandex_docs_find",
209
+ "yandex_docs_create_text",
210
+ "yandex_docs_read",
211
+ "yandex_docs_share",
212
+ "yandex_docs_rename",
213
+ "yandex_docs_delete",
214
+ "yandex_docs_save_answer",
198
215
  "yandex_contacts_status",
199
216
  "yandex_contacts_list",
200
217
  "yandex_contacts_search",
@@ -224,6 +241,7 @@ const YANDEX_TOOLS = [
224
241
  "yandex_contact_create_calendar_event",
225
242
  "yandex_contact_create_telemost_event",
226
243
  "yandex_contact_from_public_entity",
244
+ "yandex_telemost_status",
227
245
  "yandex_telemost_create_event",
228
246
  ];
229
247
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
@@ -4147,8 +4165,25 @@ async function executeYandexTool(tool, args = {}) {
4147
4165
  if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
4148
4166
  if (tool === "yandex_mail_create_task") return yandexMailCreateTask(args.uid || args.id, args);
4149
4167
  if (tool === "yandex_calendar_status") return yandexCalendarStatus();
4168
+ if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
4150
4169
  if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
4151
4170
  if (tool === "yandex_calendar_list") return yandexCalendarList(args);
4171
+ if (tool === "yandex_calendar_get") return yandexCalendarGet(args.query || args.uid || args.title || "", args);
4172
+ if (tool === "yandex_calendar_search") return yandexCalendarSearch(args.query || args.title || "", args);
4173
+ if (tool === "yandex_calendar_update") return yandexCalendarUpdate(args.query || args.uid || args.title || "", args);
4174
+ if (tool === "yandex_calendar_move") return yandexCalendarMove(args.query || args.uid || args.title || "", args);
4175
+ if (tool === "yandex_calendar_delete") return yandexCalendarDelete(args.query || args.uid || args.title || "", args);
4176
+ if (tool === "yandex_calendar_create_recurring_event") return yandexCalendarCreateRecurringEvent(args);
4177
+ if (tool === "yandex_calendar_add_reminder") return yandexCalendarAddReminder(args.query || args.uid || args.title || "", args);
4178
+ if (tool === "yandex_docs_status") return yandexDocsStatus();
4179
+ if (tool === "yandex_docs_list") return yandexDocsList(args);
4180
+ if (tool === "yandex_docs_find") return yandexDocsFind(args.query || args.name || "", args);
4181
+ if (tool === "yandex_docs_create_text") return yandexDocsCreateText(args);
4182
+ if (tool === "yandex_docs_read") return yandexDocsRead(args.path || args.remotePath || args.query || args.name, args);
4183
+ if (tool === "yandex_docs_share") return yandexDocsShare(args.path || args.remotePath || args.query || args.name, args);
4184
+ if (tool === "yandex_docs_rename") return yandexDocsRename(args.path || args.remotePath || args.query || args.name, args.name || args.newName || args.to, args);
4185
+ if (tool === "yandex_docs_delete") return yandexDocsDelete(args.path || args.remotePath || args.query || args.name, args);
4186
+ if (tool === "yandex_docs_save_answer") return yandexDocsCreateText({ ...args, text: args.text || args.answer || args.content });
4152
4187
  if (tool === "yandex_contacts_status") return yandexContactsStatus();
4153
4188
  if (tool === "yandex_contacts_list") return yandexContactsList(args);
4154
4189
  if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
@@ -4178,6 +4213,7 @@ async function executeYandexTool(tool, args = {}) {
4178
4213
  if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
4179
4214
  if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
4180
4215
  if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
4216
+ if (tool === "yandex_telemost_status") return yandexTelemostStatus();
4181
4217
  if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
4182
4218
  throw new Error(`Yandex tool неизвестен: ${tool}`);
4183
4219
  }
@@ -4980,29 +5016,53 @@ async function yandexDavRequest(url, token, options = {}) {
4980
5016
 
4981
5017
  async function yandexCalendarStatus() {
4982
5018
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
4983
- const url = await yandexCalendarBaseUrl(token);
5019
+ const calendars = await yandexCalendarCollections(token);
5020
+ const selected = pickYandexCalendar(calendars, {});
5021
+ const url = selected.url;
4984
5022
  const text = await yandexDavRequest(url, token, { method: "PROPFIND", headers: { Depth: "0" }, xml: true, body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" });
4985
- return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || "calendar" };
5023
+ return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || selected.name || "calendar", calendars: calendars.length };
5024
+ }
5025
+
5026
+ async function yandexCalendarCalendars(args = {}) {
5027
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5028
+ const calendars = await yandexCalendarCollections(token);
5029
+ return calendars.slice(0, Number(args.limit || 50));
4986
5030
  }
4987
5031
 
4988
5032
  async function yandexCalendarCreateEvent(args = {}) {
4989
5033
  if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
4990
5034
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
4991
- const baseUrl = await yandexCalendarBaseUrl(token);
5035
+ const baseUrl = await yandexCalendarBaseUrl(token, args);
4992
5036
  const uid = `${randomUUID()}@iola-cli`;
4993
5037
  const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
4994
5038
  const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
4995
5039
  const summary = args.title || args.summary || "Событие IOLA";
4996
5040
  const description = args.description || "";
4997
- const ics = buildIcsEvent({ uid, start, end, summary, description, location: args.location || "", attendees: args.attendees || args.to || [] });
5041
+ const ics = buildIcsEvent({
5042
+ uid,
5043
+ start,
5044
+ end,
5045
+ summary,
5046
+ description,
5047
+ location: args.location || "",
5048
+ attendees: args.attendees || args.to || [],
5049
+ rrule: args.rrule || buildIcsRrule(args),
5050
+ reminders: normalizeCalendarReminders(args.reminders || args.reminder || args.alarm),
5051
+ });
4998
5052
  const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
4999
5053
  await yandexDavRequest(url, token, { method: "PUT", ics: true, body: ics, timeout: 45000 });
5000
- return { status: "created", uid, title: summary, start: args.start || args.date || "", url };
5054
+ return { status: "calendar-event-created", uid, title: summary, start: args.start || args.date || "", end: args.end || "", url };
5055
+ }
5056
+
5057
+ async function yandexCalendarCreateRecurringEvent(args = {}) {
5058
+ if (!args.confirm) throw new Error("Для создания повторяющегося события нужен аргумент confirm=true.");
5059
+ const rrule = args.rrule || buildIcsRrule({ ...args, repeat: args.repeat || args.frequency || "weekly" });
5060
+ return yandexCalendarCreateEvent({ ...args, rrule, confirm: true });
5001
5061
  }
5002
5062
 
5003
5063
  async function yandexCalendarList(args = {}) {
5004
5064
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5005
- const baseUrl = await yandexCalendarBaseUrl(token);
5065
+ const baseUrl = await yandexCalendarBaseUrl(token, args);
5006
5066
  const start = toIcsDate(args.start || new Date().toISOString());
5007
5067
  const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
5008
5068
  const body = `<?xml version="1.0"?>
@@ -5011,10 +5071,113 @@ async function yandexCalendarList(args = {}) {
5011
5071
  <c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VEVENT"><c:time-range start="${start}" end="${end}"/></c:comp-filter></c:comp-filter></c:filter>
5012
5072
  </c:calendar-query>`;
5013
5073
  const text = await yandexDavRequest(baseUrl, token, { method: "REPORT", headers: { Depth: "1" }, xml: true, body, timeout: 45000 });
5014
- return parseIcsEvents(text).slice(0, Number(args.limit || 20));
5074
+ return parseIcsEvents(text, { baseUrl }).slice(0, Number(args.limit || 20));
5075
+ }
5076
+
5077
+ async function yandexCalendarSearch(query, args = {}) {
5078
+ const needle = normalizeGeoText(query || args.query || args.title || "");
5079
+ const rows = await yandexCalendarList({
5080
+ ...args,
5081
+ start: args.start || new Date(Date.now() - 90 * 86400000).toISOString(),
5082
+ end: args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
5083
+ limit: Math.max(200, Number(args.limit || 20) * 10),
5084
+ });
5085
+ if (!needle) return rows.slice(0, Number(args.limit || 20));
5086
+ return rows.filter((row) => normalizeGeoText([
5087
+ row.title,
5088
+ row.description,
5089
+ row.location,
5090
+ row.uid,
5091
+ ].filter(Boolean).join(" ")).includes(needle)).slice(0, Number(args.limit || 20));
5092
+ }
5093
+
5094
+ async function yandexCalendarGet(query, args = {}) {
5095
+ const resolved = await resolveYandexCalendarEvent(query, args);
5096
+ if (resolved.status !== "ok") return resolved;
5097
+ return stripCalendarPrivateFields(resolved.event);
5098
+ }
5099
+
5100
+ async function yandexCalendarUpdate(query, args = {}) {
5101
+ if (!args.confirm) throw new Error("Для изменения события нужен аргумент confirm=true.");
5102
+ const resolved = await resolveYandexCalendarEvent(query, args);
5103
+ if (resolved.status !== "ok") return resolved;
5104
+ const event = resolved.event;
5105
+ const start = toIcsDate(args.start || args.date || event.startIso || event.start);
5106
+ const end = toIcsDate(args.end || event.endIso || event.end || new Date(new Date(args.start || event.startIso || Date.now()).getTime() + 3600000).toISOString());
5107
+ const next = buildIcsEvent({
5108
+ uid: event.uid || `${randomUUID()}@iola-cli`,
5109
+ start,
5110
+ end,
5111
+ summary: args.title || args.summary || event.title || "Событие IOLA",
5112
+ description: args.description ?? event.description ?? "",
5113
+ location: args.location ?? event.location ?? "",
5114
+ attendees: args.attendees || args.to || event.attendees || [],
5115
+ rrule: args.rrule === null ? "" : (args.rrule || event.rrule || buildIcsRrule(args)),
5116
+ reminders: args.reminders || args.reminder || args.alarm ? normalizeCalendarReminders(args.reminders || args.reminder || args.alarm) : event.reminders || [],
5117
+ });
5118
+ await yandexDavRequest(event.url, await requireYandexOAuthToken("organizer", "Яндекс Календарь"), { method: "PUT", ics: true, body: next, timeout: 45000 });
5119
+ return { status: "calendar-event-updated", uid: event.uid, title: args.title || event.title, href: event.href };
5120
+ }
5121
+
5122
+ async function yandexCalendarMove(query, args = {}) {
5123
+ if (!args.confirm) throw new Error("Для переноса события нужен аргумент confirm=true.");
5124
+ const dateTime = args.start || args.date ? {} : extractDateTimeFromText(args.text || args.source_question || "");
5125
+ const start = args.start || args.date || dateTime.start;
5126
+ if (!start) throw new Error("Укажите новую дату/время события.");
5127
+ const end = args.end || dateTime.end || new Date(new Date(start).getTime() + 3600000).toISOString();
5128
+ return yandexCalendarUpdate(query, { ...args, start, end, confirm: true });
5129
+ }
5130
+
5131
+ async function yandexCalendarAddReminder(query, args = {}) {
5132
+ if (!args.confirm) throw new Error("Для добавления напоминания нужен аргумент confirm=true.");
5133
+ const minutes = Number(args.minutes || args.beforeMinutes || String(args.reminder || "").match(/\d+/u)?.[0] || 15);
5134
+ return yandexCalendarUpdate(query, { ...args, reminders: [`-${minutes}`], confirm: true });
5015
5135
  }
5016
5136
 
5017
- async function yandexCalendarBaseUrl(token) {
5137
+ async function yandexCalendarDelete(query, args = {}) {
5138
+ if (!args.confirm) throw new Error("Для удаления события нужен аргумент confirm=true.");
5139
+ const resolved = await resolveYandexCalendarEvent(query, args);
5140
+ if (resolved.status !== "ok") return resolved;
5141
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5142
+ await yandexDavRequest(resolved.event.url, token, { method: "DELETE", timeout: 45000 });
5143
+ return { status: "calendar-event-deleted", uid: resolved.event.uid, title: resolved.event.title, href: resolved.event.href };
5144
+ }
5145
+
5146
+ async function resolveYandexCalendarEvent(query, args = {}) {
5147
+ const uid = String(args.uid || "").trim();
5148
+ const href = String(args.href || "").trim();
5149
+ if (href) {
5150
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5151
+ const url = new URL(href, "https://caldav.yandex.ru/").toString();
5152
+ const ics = await yandexDavRequest(url, token, { method: "GET", timeout: 45000 });
5153
+ const event = parseIcsEvents(ics, { baseUrl: url.replace(/[^/]+$/u, "") })[0];
5154
+ return event ? { status: "ok", event: { ...event, href, url, ics } } : { status: "not-found", query: href };
5155
+ }
5156
+ const needle = normalizeGeoText(uid || query || args.query || args.title || "");
5157
+ const rows = await yandexCalendarSearch("", {
5158
+ ...args,
5159
+ start: args.startRange || args.start || new Date(Date.now() - 365 * 86400000).toISOString(),
5160
+ end: args.endRange || args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
5161
+ limit: 500,
5162
+ });
5163
+ const matches = rows.filter((row) => {
5164
+ if (uid && row.uid === uid) return true;
5165
+ const haystack = normalizeGeoText([row.title, row.description, row.location, row.start, row.startIso, row.end, row.endIso, row.uid, row.href].filter(Boolean).join(" "));
5166
+ return needle && haystack.includes(needle);
5167
+ });
5168
+ if (!matches.length) return { status: "not-found", kind: "calendar-event", query: query || uid };
5169
+ if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", kind: "calendar-event", query: query || uid, events: matches.slice(0, 10).map(stripCalendarPrivateFields) };
5170
+ return { status: "ok", event: matches[0] };
5171
+ }
5172
+
5173
+ async function yandexCalendarBaseUrl(token, args = {}) {
5174
+ const calendars = await yandexCalendarCollections(token);
5175
+ const selected = pickYandexCalendar(calendars, args);
5176
+ if (!selected) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
5177
+ return selected.url;
5178
+ }
5179
+
5180
+ async function yandexCalendarCollections(token) {
5018
5181
  const root = "https://caldav.yandex.ru/";
5019
5182
  const principalXml = await yandexDavRequest(root, token, {
5020
5183
  method: "PROPFIND",
@@ -5038,9 +5201,99 @@ async function yandexCalendarBaseUrl(token) {
5038
5201
  xml: true,
5039
5202
  body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
5040
5203
  });
5041
- const calendarHref = extractCalendarCollectionHref(listXml);
5042
- if (!calendarHref) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
5043
- return new URL(calendarHref, root).toString();
5204
+ return extractCalendarCollections(listXml).map((row) => ({ ...row, url: new URL(row.href, root).toString() }));
5205
+ }
5206
+
5207
+ function pickYandexCalendar(calendars, args = {}) {
5208
+ const query = normalizeGeoText(args.calendar || args.calendarName || args.name || "");
5209
+ if (query) {
5210
+ const found = calendars.find((row) => normalizeGeoText(`${row.name} ${row.href}`).includes(query));
5211
+ if (found) return found;
5212
+ }
5213
+ return calendars.find((row) => !/\/(?:inbox|outbox)\//iu.test(row.href)) || calendars[0] || null;
5214
+ }
5215
+
5216
+ async function yandexDocsStatus() {
5217
+ const info = await yandexDiskInfo();
5218
+ await ensureYandexDiskDir(`${CLOUD_DEFAULT_REMOTE_DIR}/docs`, { allowExisting: true });
5219
+ return { status: "ok", provider: "yandex-disk", folder: `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, totalSpace: info.totalSpace, usedSpace: info.usedSpace };
5220
+ }
5221
+
5222
+ async function yandexDocsList(args = {}) {
5223
+ const folder = normalizeCloudUserPath(args.path || args.folder || `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, "yandex-disk");
5224
+ const rows = await yandexDiskListRecursive(folder, { depth: Number(args.depth || 3), limit: Number(args.limit || 100) }).catch(() => []);
5225
+ return rows.filter(isYandexDocumentResource).slice(0, Number(args.limit || 50));
5226
+ }
5227
+
5228
+ async function yandexDocsFind(query, args = {}) {
5229
+ const needle = normalizeGeoText(query || args.query || "");
5230
+ const rows = await yandexDocsList({ ...args, limit: Math.max(200, Number(args.limit || 20) * 10) });
5231
+ if (!needle) return rows.slice(0, Number(args.limit || 20));
5232
+ return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(needle)).slice(0, Number(args.limit || 20));
5233
+ }
5234
+
5235
+ async function yandexDocsCreateText(args = {}) {
5236
+ if (!args.confirm) throw new Error("Для создания документа нужен аргумент confirm=true.");
5237
+ const text = String(args.text || args.content || "").trim();
5238
+ if (!text) throw new Error("Текст документа пустой.");
5239
+ const ext = normalizeYandexDocExtension(args.format || args.ext || path.extname(args.path || args.remotePath || ""));
5240
+ const title = slugYandexDiskName(args.title || args.name || `document-${timestampForFile()}`);
5241
+ const remotePath = normalizeCloudUserPath(args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/docs/${title}${title.endsWith(ext) ? "" : ext}`, "yandex-disk");
5242
+ const saved = await yandexDiskSaveText(text, remotePath);
5243
+ return { ...saved, status: "document-created", title: path.posix.basename(remotePath), remote: remotePath };
5244
+ }
5245
+
5246
+ async function yandexDocsRead(target, args = {}) {
5247
+ const doc = await resolveYandexDoc(target, args);
5248
+ if (doc.status !== "ok") return doc;
5249
+ if (!/\.(txt|md|csv|json|html)$/iu.test(doc.path)) {
5250
+ return { status: "binary-document", remote: doc.path, name: doc.name, message: "Этот документ не текстовый. Его можно скачать, переименовать, удалить или опубликовать ссылкой." };
5251
+ }
5252
+ return yandexDiskReadText(doc.path, args);
5253
+ }
5254
+
5255
+ async function yandexDocsShare(target, args = {}) {
5256
+ if (!args.confirm) throw new Error("Для публикации документа нужен аргумент confirm=true.");
5257
+ const doc = await resolveYandexDoc(target, args);
5258
+ if (doc.status !== "ok") return doc;
5259
+ return yandexDiskShareWithQr(doc.path, { ...args, confirm: true });
5260
+ }
5261
+
5262
+ async function yandexDocsRename(target, newName, args = {}) {
5263
+ if (!args.confirm) throw new Error("Для переименования документа нужен аргумент confirm=true.");
5264
+ const doc = await resolveYandexDoc(target, args);
5265
+ if (doc.status !== "ok") return doc;
5266
+ if (!newName) throw new Error("Укажите новое имя документа.");
5267
+ return yandexDiskRename(doc.path, newName, { ...args, confirm: true, overwrite: args.overwrite !== false });
5268
+ }
5269
+
5270
+ async function yandexDocsDelete(target, args = {}) {
5271
+ if (!args.confirm) throw new Error("Для удаления документа нужен аргумент confirm=true.");
5272
+ const doc = await resolveYandexDoc(target, args);
5273
+ if (doc.status !== "ok") return doc;
5274
+ return yandexDiskDelete(doc.path, args);
5275
+ }
5276
+
5277
+ async function resolveYandexDoc(target, args = {}) {
5278
+ const value = String(target || "").trim();
5279
+ if (value.startsWith("/")) {
5280
+ const statRow = await yandexDiskStat(value);
5281
+ return isYandexDocumentResource(statRow) ? { status: "ok", ...statRow } : { status: "not-document", path: value };
5282
+ }
5283
+ const rows = await yandexDocsFind(value, { ...args, limit: 10 });
5284
+ if (!rows.length) return { status: "not-found", query: value };
5285
+ if (rows.length > 1 && !args.selectFirst) return { status: "ambiguous", query: value, docs: rows.slice(0, 10) };
5286
+ return { status: "ok", ...rows[0] };
5287
+ }
5288
+
5289
+ function isYandexDocumentResource(row = {}) {
5290
+ return row.type !== "dir" && /\.(docx|xlsx|pptx|pdf|txt|md|html|csv|json)$/iu.test(row.name || row.path || "");
5291
+ }
5292
+
5293
+ function normalizeYandexDocExtension(value) {
5294
+ const ext = String(value || "").replace(/^\./u, "").toLocaleLowerCase("en-US");
5295
+ if (["txt", "md", "html", "csv", "json"].includes(ext)) return `.${ext}`;
5296
+ return ".md";
5044
5297
  }
5045
5298
 
5046
5299
  async function yandexContactsStatus() {
@@ -5639,25 +5892,92 @@ async function yandexContactsBaseUrl(token) {
5639
5892
  return new URL(addressBookHref, root).toString();
5640
5893
  }
5641
5894
 
5895
+ async function yandexTelemostStatus() {
5896
+ const calendar = await yandexCalendarStatus();
5897
+ return {
5898
+ status: "calendar-fallback",
5899
+ provider: "yandex-telemost",
5900
+ calendar: calendar.displayName,
5901
+ message: "Для обычного OAuth-подключения CLI использует календарное событие. Прямой Telemost API проверяется отдельно и может быть доступен только аккаунтам/организациям Яндекс 360.",
5902
+ };
5903
+ }
5904
+
5642
5905
  async function yandexTelemostCreateEvent(args = {}) {
5906
+ if (!args.confirm) throw new Error("Для создания встречи нужен аргумент confirm=true.");
5907
+ const telemost = await tryCreateYandexTelemostMeeting(args).catch((error) => ({
5908
+ status: "telemost-api-unavailable",
5909
+ error: error instanceof Error ? error.message : String(error),
5910
+ }));
5643
5911
  const description = [
5644
5912
  args.description || "",
5645
5913
  "",
5646
- "Телемост: создайте ссылку в Яндекс Календаре, если интерфейс календаря предложит видеовстречу.",
5914
+ telemost.joinUrl ? `Ссылка Телемоста: ${telemost.joinUrl}` : "Телемост: прямое создание ссылки через API недоступно для текущего OAuth-подключения. Событие создано в календаре как встреча; ссылку можно добавить вручную в Яндекс Календаре.",
5647
5915
  ].join("\n").trim();
5648
- return yandexCalendarCreateEvent({ ...args, description });
5916
+ const event = await yandexCalendarCreateEvent({
5917
+ ...args,
5918
+ title: args.title || args.summary || "Телемост IOLA",
5919
+ description,
5920
+ location: telemost.joinUrl || args.location || "Яндекс Телемост",
5921
+ confirm: true,
5922
+ });
5923
+ return { ...event, status: telemost.joinUrl ? "telemost-event-created" : "telemost-calendar-fallback-created", telemost };
5649
5924
  }
5650
5925
 
5651
- function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [] }) {
5926
+ async function tryCreateYandexTelemostMeeting(args = {}) {
5927
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Телемост");
5928
+ const endpoints = [
5929
+ "https://cloud-api.yandex.net/v1/telemost/meetings",
5930
+ "https://api360.yandex.net/v1/telemost/meetings",
5931
+ ];
5932
+ let lastError = "";
5933
+ for (const endpoint of endpoints) {
5934
+ const response = await fetch(endpoint, {
5935
+ method: "POST",
5936
+ headers: {
5937
+ Authorization: `OAuth ${token}`,
5938
+ "content-type": "application/json",
5939
+ },
5940
+ body: JSON.stringify({
5941
+ title: args.title || args.summary || "Телемост IOLA",
5942
+ description: args.description || "",
5943
+ }),
5944
+ signal: AbortSignal.timeout(15000),
5945
+ }).catch((error) => ({ ok: false, status: 0, statusText: error.message, text: async () => error.message }));
5946
+ const text = await response.text().catch(() => "");
5947
+ if (response.ok) {
5948
+ const payload = text ? JSON.parse(text) : {};
5949
+ return {
5950
+ status: "telemost-api-created",
5951
+ endpoint,
5952
+ id: payload.id || payload.meeting_id || "",
5953
+ joinUrl: payload.join_url || payload.url || payload.link || payload.meeting_url || "",
5954
+ raw: payload,
5955
+ };
5956
+ }
5957
+ lastError = `${endpoint}: ${response.status} ${response.statusText} ${sanitizeSecretFromText(text.slice(0, 500), token)}`;
5958
+ if (![404, 405].includes(Number(response.status))) break;
5959
+ }
5960
+ throw new Error(lastError || "Telemost API недоступен.");
5961
+ }
5962
+
5963
+ function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [], rrule = "", reminders = [] }) {
5652
5964
  const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
5653
5965
  const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
5654
5966
  .map((email) => String(email || "").trim())
5655
5967
  .filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
5656
5968
  .map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
5969
+ const reminderBlocks = normalizeCalendarReminders(reminders).map((minutes) => [
5970
+ "BEGIN:VALARM",
5971
+ `TRIGGER:-PT${Math.max(1, Math.abs(Number(minutes) || 15))}M`,
5972
+ "ACTION:DISPLAY",
5973
+ `DESCRIPTION:${escape(summary || "Напоминание")}`,
5974
+ "END:VALARM",
5975
+ ].join("\r\n"));
5657
5976
  return [
5658
5977
  "BEGIN:VCALENDAR",
5659
5978
  "VERSION:2.0",
5660
5979
  "PRODID:-//IOLA CLI//Yandex Calendar//RU",
5980
+ "CALSCALE:GREGORIAN",
5661
5981
  "BEGIN:VEVENT",
5662
5982
  `UID:${uid}`,
5663
5983
  `DTSTAMP:${toIcsDate(new Date().toISOString())}`,
@@ -5666,7 +5986,9 @@ function buildIcsEvent({ uid, start, end, summary, description, location, attend
5666
5986
  `SUMMARY:${escape(summary)}`,
5667
5987
  description ? `DESCRIPTION:${escape(description)}` : "",
5668
5988
  location ? `LOCATION:${escape(location)}` : "",
5989
+ rrule ? `RRULE:${rrule}` : "",
5669
5990
  ...attendeeLines,
5991
+ ...reminderBlocks,
5670
5992
  "END:VEVENT",
5671
5993
  "END:VCALENDAR",
5672
5994
  "",
@@ -5679,15 +6001,105 @@ function toIcsDate(value) {
5679
6001
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
5680
6002
  }
5681
6003
 
5682
- function parseIcsEvents(xmlOrIcs) {
5683
- const decoded = decodeXml(stripXmlTags(xmlOrIcs));
5684
- return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => ({
5685
- uid: chunk.match(/\nUID:([^\n\r]+)/u)?.[1] || "",
5686
- title: chunk.match(/\nSUMMARY:([^\n\r]+)/u)?.[1] || "",
5687
- start: chunk.match(/\nDTSTART[^:]*:([^\n\r]+)/u)?.[1] || "",
5688
- end: chunk.match(/\nDTEND[^:]*:([^\n\r]+)/u)?.[1] || "",
5689
- location: chunk.match(/\nLOCATION:([^\n\r]+)/u)?.[1] || "",
5690
- })).filter((item) => item.uid || item.title);
6004
+ function parseIcsEvents(xmlOrIcs, options = {}) {
6005
+ const text = String(xmlOrIcs || "");
6006
+ const responses = text.split(/<[^:>]*:?response[^>]*>/iu).slice(1);
6007
+ if (responses.length) {
6008
+ return responses.flatMap((response) => {
6009
+ const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
6010
+ const calendarData = response.match(/<[^:>]*:?calendar-data[^>]*>([\s\S]*?)<\/[^:>]*:?calendar-data>/iu)?.[1] || response;
6011
+ return parseIcsEvents(calendarData, options).map((row) => ({
6012
+ ...row,
6013
+ href: row.href || href,
6014
+ url: row.url || (href ? new URL(href, options.baseUrl || "https://caldav.yandex.ru/").toString() : ""),
6015
+ }));
6016
+ });
6017
+ }
6018
+ const decoded = decodeXml(stripXmlTags(text));
6019
+ return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => {
6020
+ const lines = unfoldIcsLines(`BEGIN:VEVENT${chunk}`);
6021
+ const uid = icsValue(lines, "UID");
6022
+ const start = icsValue(lines, "DTSTART");
6023
+ const end = icsValue(lines, "DTEND");
6024
+ return {
6025
+ uid,
6026
+ title: unescapeIcsValue(icsValue(lines, "SUMMARY")),
6027
+ start,
6028
+ end,
6029
+ startIso: icsDateToIso(start),
6030
+ endIso: icsDateToIso(end),
6031
+ location: unescapeIcsValue(icsValue(lines, "LOCATION")),
6032
+ description: unescapeIcsValue(icsValue(lines, "DESCRIPTION")),
6033
+ rrule: icsValue(lines, "RRULE"),
6034
+ attendees: icsValues(lines, "ATTENDEE").map((value) => value.replace(/^mailto:/iu, "")),
6035
+ reminders: parseIcsReminderMinutes(lines),
6036
+ ics: decoded.includes("BEGIN:VCALENDAR") ? decoded : "",
6037
+ };
6038
+ }).filter((item) => item.uid || item.title);
6039
+ }
6040
+
6041
+ function unfoldIcsLines(value) {
6042
+ return String(value || "")
6043
+ .replace(/\r/g, "")
6044
+ .replace(/\n[ \t]/g, "")
6045
+ .split(/\n/u)
6046
+ .map((line) => line.trim())
6047
+ .filter(Boolean);
6048
+ }
6049
+
6050
+ function icsValues(lines, property) {
6051
+ const prop = String(property || "").toLocaleUpperCase("en-US");
6052
+ return lines
6053
+ .filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
6054
+ .map((line) => line.slice(line.indexOf(":") + 1).trim())
6055
+ .filter(Boolean);
6056
+ }
6057
+
6058
+ function icsValue(lines, property) {
6059
+ return icsValues(lines, property)[0] || "";
6060
+ }
6061
+
6062
+ function unescapeIcsValue(value) {
6063
+ return String(value || "")
6064
+ .replace(/\\n/giu, "\n")
6065
+ .replace(/\\,/gu, ",")
6066
+ .replace(/\\;/gu, ";")
6067
+ .replace(/\\\\/gu, "\\")
6068
+ .trim();
6069
+ }
6070
+
6071
+ function icsDateToIso(value) {
6072
+ const text = String(value || "").trim();
6073
+ const match = text.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/u);
6074
+ if (!match) return "";
6075
+ const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}${text.endsWith("Z") ? "Z" : ""}`;
6076
+ const date = new Date(iso);
6077
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
6078
+ }
6079
+
6080
+ function parseIcsReminderMinutes(lines) {
6081
+ return lines
6082
+ .filter((line) => /^TRIGGER:/iu.test(line))
6083
+ .map((line) => Number(line.match(/PT(\d+)M/iu)?.[1] || 0))
6084
+ .filter(Boolean);
6085
+ }
6086
+
6087
+ function normalizeCalendarReminders(value) {
6088
+ const rows = Array.isArray(value) ? value : String(value || "").split(/[;,]/u);
6089
+ return rows.map((item) => Number(String(item).match(/\d+/u)?.[0] || 0)).filter(Boolean);
6090
+ }
6091
+
6092
+ function buildIcsRrule(args = {}) {
6093
+ const repeat = String(args.repeat || args.frequency || "").toLocaleLowerCase("ru-RU");
6094
+ if (!repeat && !args.count && !args.until) return "";
6095
+ const freq = /день|daily|day/iu.test(repeat) ? "DAILY"
6096
+ : /месяц|monthly|month/iu.test(repeat) ? "MONTHLY"
6097
+ : /год|year|yearly|annual/iu.test(repeat) ? "YEARLY"
6098
+ : "WEEKLY";
6099
+ const parts = [`FREQ=${freq}`];
6100
+ if (args.count) parts.push(`COUNT=${Number(args.count)}`);
6101
+ if (args.until) parts.push(`UNTIL=${toIcsDate(args.until)}`);
6102
+ return parts.join(";");
5691
6103
  }
5692
6104
 
5693
6105
  function extractDavHref(xml, propertyName) {
@@ -5706,6 +6118,24 @@ function extractCalendarCollectionHref(xml) {
5706
6118
  return "";
5707
6119
  }
5708
6120
 
6121
+ function extractCalendarCollections(xml) {
6122
+ const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
6123
+ const rows = [];
6124
+ for (const response of responses) {
6125
+ if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
6126
+ const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
6127
+ if (!href || /\/(?:inbox|outbox)\//iu.test(href)) continue;
6128
+ const name = stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "").trim() || path.posix.basename(href.replace(/\/$/u, ""));
6129
+ rows.push({ provider: "yandex-calendar", href, name, type: "calendar" });
6130
+ }
6131
+ return rows;
6132
+ }
6133
+
6134
+ function stripCalendarPrivateFields(row = {}) {
6135
+ const { ics: _ics, url: _url, ...rest } = row;
6136
+ return rest;
6137
+ }
6138
+
5709
6139
  function extractAddressBookCollectionHref(xml) {
5710
6140
  const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
5711
6141
  for (const response of responses) {
@@ -11356,10 +11786,93 @@ async function buildYandexDirectAnswer(question, history = []) {
11356
11786
  return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
11357
11787
  }
11358
11788
 
11359
- if (/(календар|событи)/iu.test(normalized) && !/(создай|добавь|запланируй)/iu.test(normalized)) {
11789
+ if (/(документ|документы|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
11790
+ if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
11791
+ const result = await yandexDocsStatus();
11792
+ return `Яндекс Документы через Диск подключены. Папка: ${result.folder}.`;
11793
+ }
11794
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
11795
+ const text = extractShareMessage(question) || cleanupCloudSaveText(question);
11796
+ if (!text) return "Укажите текст документа. Пример: создай документ на Яндекс Диске текст: ...";
11797
+ const result = await yandexDocsCreateText({
11798
+ title: extractYandexDocTitle(question),
11799
+ text,
11800
+ format: /html/iu.test(normalized) ? "html" : /txt|текстов/iu.test(normalized) ? "txt" : "md",
11801
+ confirm: true,
11802
+ });
11803
+ return `Документ создан на Яндекс Диске: ${result.remote}.`;
11804
+ }
11805
+ if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
11806
+ const result = await yandexDocsRead(extractCloudPath(question) || cleanupYandexQuery(question), {});
11807
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11808
+ }
11809
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
11810
+ const result = await yandexDocsShare(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
11811
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11812
+ }
11813
+ if (/(переимен|rename)/iu.test(normalized)) {
11814
+ const result = await yandexDocsRename(extractCloudPath(question) || cleanupYandexQuery(question), extractCloudNewName(question), { confirm: true });
11815
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11816
+ }
11817
+ if (/(удали|удалить)/iu.test(normalized)) {
11818
+ const result = await yandexDocsDelete(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
11819
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11820
+ }
11821
+ const rows = /(найди|поиск)/iu.test(normalized)
11822
+ ? await yandexDocsFind(cleanupYandexQuery(question), { limit: 20 })
11823
+ : await yandexDocsList({ limit: 20 });
11824
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
11825
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
11826
+ }
11827
+
11828
+ if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
11829
+ if (/(статус|проверь|работает|доступ)/iu.test(normalized) && /(календар|телемост)/iu.test(normalized)) {
11830
+ const result = /телемост/iu.test(normalized) ? await yandexTelemostStatus() : await yandexCalendarStatus();
11831
+ return /телемост/iu.test(normalized)
11832
+ ? `${result.message} Календарь: ${result.calendar || "-"}.`
11833
+ : `Яндекс Календарь подключен: ${result.displayName || result.url}. Календарей: ${result.calendars || 1}.`;
11834
+ }
11835
+ if (/(какие|список).{0,30}(календар|календари)|календари/iu.test(normalized)) {
11836
+ const rows = await yandexCalendarCalendars({ limit: 20 });
11837
+ if (!rows.length) return "Календари Яндекса не найдены.";
11838
+ return ["Яндекс Календари:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.href}`)].join("\n");
11839
+ }
11840
+ if (/(напомин|уведом)/iu.test(normalized) && /(добав|постав|создай)/iu.test(normalized)) {
11841
+ const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
11842
+ const result = await yandexCalendarAddReminder(cleanupCalendarEventQuery(question), { minutes, confirm: true });
11843
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11844
+ }
11845
+ if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
11846
+ const dateTime = extractDateTimeFromText(question);
11847
+ const title = extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA");
11848
+ const args = { ...dateTime, title, description: extractShareMessage(question) || "", confirm: true };
11849
+ const result = /телемост/iu.test(normalized)
11850
+ ? await yandexTelemostCreateEvent(args)
11851
+ : /кажд|еженед|ежеднев|ежемесяч|повтор/iu.test(normalized)
11852
+ ? await yandexCalendarCreateRecurringEvent({ ...args, ...extractCalendarRepeat(question), confirm: true })
11853
+ : await yandexCalendarCreateEvent(args);
11854
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11855
+ }
11856
+ if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) {
11857
+ const result = await yandexCalendarMove(cleanupCalendarEventQuery(question), { ...extractDateTimeFromText(question), confirm: true });
11858
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11859
+ }
11860
+ if (/(переимен|измени\s+назв|смени\s+назв)/iu.test(normalized)) {
11861
+ const result = await yandexCalendarUpdate(cleanupCalendarEventQuery(question), { title: extractCalendarTitle(question), confirm: true });
11862
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11863
+ }
11864
+ if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) {
11865
+ const result = await yandexCalendarDelete(cleanupCalendarEventQuery(question), { confirm: true });
11866
+ return formatToolResult({ rows: [result], outputs: [] }, {});
11867
+ }
11868
+ if (/(найди|поиск|где|покажи).{0,40}(событи|встреч|телемост)/iu.test(normalized)) {
11869
+ const rows = await yandexCalendarSearch(cleanupCalendarEventQuery(question), { limit: 20 });
11870
+ if (!rows.length) return "События в Яндекс Календаре по запросу не найдены.";
11871
+ return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
11872
+ }
11360
11873
  const rows = await yandexCalendarList({ limit: 10 });
11361
11874
  if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
11362
- return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
11875
+ return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
11363
11876
  }
11364
11877
 
11365
11878
  if (/(контакт|адресн)/iu.test(normalized)) {
@@ -11682,7 +12195,7 @@ async function buildUserSkillDirectAnswer(question) {
11682
12195
  }
11683
12196
 
11684
12197
  function isYandexServiceQuestion(normalized) {
11685
- return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
12198
+ return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
11686
12199
  }
11687
12200
 
11688
12201
  function isYandexIdentityQuestion(normalized) {
@@ -11967,6 +12480,38 @@ async function buildCloudDirectAnswer(question) {
11967
12480
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
11968
12481
  try {
11969
12482
  const provider = await getCloudProvider();
12483
+ if (provider === "yandex-disk" && /(документ|docs|360)/iu.test(normalized)) {
12484
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
12485
+ const result = await yandexDocsShare(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
12486
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12487
+ }
12488
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
12489
+ const text = extractShareMessage(question) || cleanupCloudSaveText(question);
12490
+ if (!text) return "Укажите текст документа.";
12491
+ const result = await yandexDocsCreateText({ title: extractYandexDocTitle(question), text, confirm: true });
12492
+ return `Документ создан на Яндекс Диске: ${result.remote}.`;
12493
+ }
12494
+ if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
12495
+ const result = await yandexDocsRead(extractCloudPath(question) || cleanupCloudQuery(question), {});
12496
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12497
+ }
12498
+ if (/(переимен|rename)/iu.test(normalized)) {
12499
+ const result = await yandexDocsRename(extractCloudPath(question) || cleanupCloudQuery(question), extractCloudNewName(question), { confirm: true });
12500
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12501
+ }
12502
+ if (/(удали|удалить)/iu.test(normalized)) {
12503
+ const result = await yandexDocsDelete(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
12504
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12505
+ }
12506
+ if (/(найди|поиск)/iu.test(normalized)) {
12507
+ const rows = await yandexDocsFind(cleanupCloudQuery(question), { limit: 20 });
12508
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
12509
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
12510
+ }
12511
+ const rows = await yandexDocsList({ limit: 20 });
12512
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
12513
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
12514
+ }
11970
12515
  if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
11971
12516
  const info = await yandexDiskInfo();
11972
12517
  return [
@@ -12200,6 +12745,7 @@ function extractCloudPath(question) {
12200
12745
  function cleanupCloudPathCandidate(value) {
12201
12746
  return String(value || "")
12202
12747
  .replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
12748
+ .replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, "")
12203
12749
  .replace(/[.!?]+$/u, "")
12204
12750
  .trim();
12205
12751
  }
@@ -12249,7 +12795,50 @@ function extractMailSubject(question) {
12249
12795
  }
12250
12796
 
12251
12797
  function extractShareMessage(question) {
12252
- return String(question || "").match(/(?:текст|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
12798
+ return String(question || "").match(/(?:текст|text|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
12799
+ }
12800
+
12801
+ function extractYandexDocTitle(question) {
12802
+ const text = String(question || "");
12803
+ const raw = text.match(/(?:названи(?:е|ем)|имя|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
12804
+ || text.match(/(?:документ|файл)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
12805
+ || "";
12806
+ return raw.replace(/\s+(?:текст|text|body|сообщение)\s*:?.*$/iu, "").trim();
12807
+ }
12808
+
12809
+ function extractCalendarTitle(question) {
12810
+ const text = String(question || "");
12811
+ const raw = text.match(/(?:тема|названи(?:е|ем)|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
12812
+ || text.match(/(?:событи[ея]|встреч[ауи]|телемост)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
12813
+ || "";
12814
+ return raw
12815
+ .replace(/\s+(?:сегодня|завтра|послезавтра)(?:\s|$).*$/iu, "")
12816
+ .replace(/\s+(?:в\s+\d{1,2}(?::\d{2})?|на\s+\d{1,2}[.\-/]\d{1,2}[\d.\-/]*)(?:\s|$).*$/iu, "")
12817
+ .trim();
12818
+ }
12819
+
12820
+ function extractCalendarRepeat(question) {
12821
+ const text = String(question || "").toLocaleLowerCase("ru-RU");
12822
+ const count = Number(text.match(/(\d+)\s*(?:раз|повтор)/iu)?.[1] || 0);
12823
+ return {
12824
+ repeat: /ежеднев|каждый\s+день/iu.test(text) ? "daily"
12825
+ : /ежемесяч|каждый\s+месяц/iu.test(text) ? "monthly"
12826
+ : /ежегод|каждый\s+год/iu.test(text) ? "yearly"
12827
+ : "weekly",
12828
+ count: count || undefined,
12829
+ };
12830
+ }
12831
+
12832
+ function cleanupCalendarEventQuery(question) {
12833
+ return String(question || "")
12834
+ .replace(/(?:создай|добавь|запланируй|назначь|перенеси|перемести|измени|смени|удали|удалить|отмени|отменить|найди|поиск|покажи|добавь\s+напоминание|поставь\s+напоминание)/giu, " ")
12835
+ .replace(/(?:^|\s)(?:событи\p{L}*|встреч\p{L}*|телемост\p{L}*|календар\p{L}*|напомин\p{L}*|уведом\p{L}*|яндекс|на|к|ко|в|во|сегодня|завтра|послезавтра|час|часа|часов|минут|минуты)(?=\s|$)/giu, " ")
12836
+ .replace(/\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?/gu, " ")
12837
+ .replace(/\d{1,2}[:.]\d{2}/gu, " ")
12838
+ .replace(/(?:^|\s)\d{1,3}(?=\s|$)/gu, " ")
12839
+ .replace(/[,:;.!?«»"()]+/gu, " ")
12840
+ .replace(/\s+/g, " ")
12841
+ .trim();
12253
12842
  }
12254
12843
 
12255
12844
  function parseYandexDiskPackageRequest(question) {
@@ -12826,8 +13415,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
12826
13415
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
12827
13416
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
12828
13417
  "Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
12829
- "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_list {start,end}, yandex_calendar_create_event {title,start,end,location,attendees,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}.",
12830
- "Опасные 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_telemost_create_event.",
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.",
12831
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.",
12832
13421
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
12833
13422
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
@@ -12942,8 +13531,25 @@ function inferToolPlan(question, options = {}) {
12942
13531
  if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
12943
13532
  return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
12944
13533
  }
13534
+ if (/(документ|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
13535
+ const target = extractCloudPath(question) || cleanupYandexQuery(question);
13536
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_create_text", args: { title: extractYandexDocTitle(question), text: extractShareMessage(question) || cleanupCloudSaveText(question), confirm: true } }] };
13537
+ if (/(прочитай|открой|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_read", args: { query: target } }] };
13538
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_share", args: { query: target, confirm: true } }] };
13539
+ if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_rename", args: { query: target, name: extractCloudNewName(question), confirm: true } }] };
13540
+ if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_delete", args: { query: target, confirm: true } }] };
13541
+ if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_find", args: { query: cleanupYandexQuery(question), limit: 20 } }] };
13542
+ return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
13543
+ }
12945
13544
  if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
12946
- return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
13545
+ if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
13546
+ const dateTime = extractDateTimeFromText(question);
13547
+ 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 } }] };
13548
+ }
13549
+ if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_move", args: { query: cleanupCalendarEventQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
13550
+ if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_delete", args: { query: cleanupCalendarEventQuery(question), confirm: true } }] };
13551
+ if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_search", args: { query: cleanupCalendarEventQuery(question), limit: 20 } }] };
13552
+ return { steps: [{ tool: "yandex_calendar_list", args: { limit: 20 } }] };
12947
13553
  }
12948
13554
  if (/(контакт|адресн)/iu.test(normalized)) {
12949
13555
  if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
@@ -13537,6 +14143,18 @@ function formatToolResult(result, options) {
13537
14143
  return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
13538
14144
  }
13539
14145
  if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
14146
+ if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
14147
+ return `${row.status === "calendar-event-created" ? "Событие" : "Телемост"} создан: ${row.title || row.uid}${row.start ? `, ${row.start}` : ""}${row.telemost?.joinUrl ? `\nСсылка: ${row.telemost.joinUrl}` : row.status === "telemost-calendar-fallback-created" ? "\nПрямая ссылка Телемоста через API недоступна, создано событие календаря." : ""}`;
14148
+ }
14149
+ if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
14150
+ if (row.status === "calendar-event-deleted") return `Событие удалено: ${row.title || row.uid}`;
14151
+ 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
+ if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
14153
+ if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
14154
+ if (row.status === "binary-document") return `${row.name || row.remote}: ${row.message}`;
14155
+ if (row.status === "not-document") return `Это не документ: ${row.path}`;
14156
+ if (row.status === "ambiguous" && row.docs) return [`Нашел несколько документов. Уточните:`, ...row.docs.map((doc, index) => `${index + 1}. ${doc.name} — ${doc.path}`)].join("\n");
14157
+ if (row.status === "not-found" && row.docs !== undefined) return `Документ не найден: ${row.query}`;
13540
14158
  if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
13541
14159
  if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
13542
14160
  if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
@@ -15554,7 +16172,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
15554
16172
  if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
15555
16173
  const cloudQuestion = isCloudQuestion(normalized);
15556
16174
  if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
15557
- if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|облако)/iu.test(normalized)) selected.add("yandex-services");
16175
+ if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|документ|docs|360|облако)/iu.test(normalized)) selected.add("yandex-services");
15558
16176
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
15559
16177
  if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
15560
16178
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
@@ -68,8 +68,9 @@ Toolset `yandex` включает локальные tools для пользов
68
68
  - Yandex ID: профиль, логин, email;
69
69
  - Яндекс Диск: список, создание папок, поиск, сохранение текста, загрузка, скачивание, публичные ссылки;
70
70
  - Яндекс Почта: статус, список писем, поиск, чтение, отправка;
71
- - Яндекс Календарь: статус, список событий, создание события;
71
+ - Яндекс Календарь: календари, список, поиск, создание, перенос, редактирование, повторы, напоминания, удаление событий;
72
+ - Яндекс Документы / 360: работа через Яндекс Диск - список, поиск, создание текстовых документов, чтение, ссылка/QR, переименование, удаление;
72
73
  - Яндекс Контакты: статус, список, поиск, карточка, создание, обновление, удаление, импорт/экспорт, дубликаты, неполные карточки, backup на Диск, дни рождения в календарь, письмо/ссылка/встреча по контакту;
73
- - Телемост: подготовка встречи через календарное событие.
74
+ - Телемост: попытка прямого API и fallback через календарное событие, если API недоступен аккаунту.
74
75
 
75
- Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок и создание событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
76
+ Опасные действия ограничены: отправка письма, удаление файлов, публикация ссылок, создание/изменение документов и создание/изменение/удаление событий требуют явного подтверждения в tool-вызове. Токены хранятся локально в `~/.iola/secrets.json`.
@@ -143,8 +143,24 @@ QR-код:
143
143
  - `yandex_mail_read` - прочитать письмо по UID;
144
144
  - `yandex_mail_send` - отправить письмо;
145
145
  - `yandex_calendar_status` - проверить доступ к Календарю;
146
- - `yandex_calendar_list` - показать события;
147
- - `yandex_calendar_create_event` - создать событие;
146
+ - `yandex_calendar_calendars` - показать доступные календари;
147
+ - `yandex_calendar_list` - показать ближайшие события;
148
+ - `yandex_calendar_search` - найти событие;
149
+ - `yandex_calendar_get` - открыть карточку события;
150
+ - `yandex_calendar_create_event` - создать событие с участниками, местом и напоминаниями;
151
+ - `yandex_calendar_create_recurring_event` - создать повторяющееся событие;
152
+ - `yandex_calendar_update` - изменить событие;
153
+ - `yandex_calendar_move` - перенести событие;
154
+ - `yandex_calendar_add_reminder` - добавить напоминание;
155
+ - `yandex_calendar_delete` - удалить событие;
156
+ - `yandex_docs_status` - проверить Документы / 360 через Диск;
157
+ - `yandex_docs_list` - показать документы;
158
+ - `yandex_docs_find` - найти документ;
159
+ - `yandex_docs_create_text` - создать текстовый документ на Яндекс Диске;
160
+ - `yandex_docs_read` - прочитать небольшой текстовый документ;
161
+ - `yandex_docs_share` - создать ссылку и QR-код на документ;
162
+ - `yandex_docs_rename` - переименовать документ;
163
+ - `yandex_docs_delete` - удалить документ;
148
164
  - `yandex_contacts_status` - проверить доступ к Контактам;
149
165
  - `yandex_contacts_list` - показать контакты;
150
166
  - `yandex_contacts_search` - найти контакт по имени, email, телефону, организации, адресу или заметке;
@@ -166,7 +182,8 @@ QR-код:
166
182
  - `yandex_contact_create_calendar_event` - создать встречу с контактом;
167
183
  - `yandex_contact_create_telemost_event` - создать событие для Телемоста с контактом;
168
184
  - `yandex_contact_from_public_entity` - создать контакт из открытого городского слоя;
169
- - `yandex_telemost_create_event` - создать календарное событие для встречи.
185
+ - `yandex_telemost_status` - проверить режим Телемоста;
186
+ - `yandex_telemost_create_event` - создать встречу: прямой Telemost API используется только если он доступен аккаунту; иначе создается календарное событие без выдуманной ссылки.
170
187
 
171
188
  Регулярная проверка контактов:
172
189
 
@@ -233,7 +233,47 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
233
233
  Работать с Яндекс Календарем:
234
234
 
235
235
  - показать ближайшие события;
236
- - создать событие только после явного подтверждения.
236
+ - показать список календарей;
237
+ - найти событие по названию, месту, описанию или дате;
238
+ - создать событие только после явного подтверждения;
239
+ - создать повторяющееся событие;
240
+ - перенести встречу на новую дату или время;
241
+ - изменить название, описание или место события;
242
+ - добавить напоминание;
243
+ - удалить событие по явной просьбе.
244
+
245
+ Примеры:
246
+
247
+ - `покажи яндекс календарь`;
248
+ - `найди событие собрание в яндекс календаре`;
249
+ - `создай событие название: прием врача завтра в 10:00`;
250
+ - `перенеси встречу прием врача на послезавтра в 11:00`;
251
+ - `добавь напоминание 20 минут к событию прием врача`;
252
+ - `удали событие прием врача`.
253
+
254
+ ### yandex-docs
255
+
256
+ Работать с документами Яндекс 360 через Яндекс Диск:
257
+
258
+ - показать документы в папке `/IOLA/docs`;
259
+ - найти документ по названию;
260
+ - создать текстовый, Markdown или HTML-документ;
261
+ - прочитать небольшой текстовый документ;
262
+ - создать публичную ссылку и QR-код на документ;
263
+ - переименовать документ;
264
+ - удалить документ по явной просьбе.
265
+
266
+ Важно: CLI работает с документами как с файлами на Яндекс Диске. Он не подменяет веб-редактор Яндекс Документов и не обещает создание "нативного" документа, если Яндекс не дает стабильный API для этого действия.
267
+
268
+ Примеры:
269
+
270
+ - `покажи документы на Яндекс Диске`;
271
+ - `найди документ справка на Яндекс Диске`;
272
+ - `создай документ яндекс 360 название: заметка текст: купить лекарства`;
273
+ - `прочитай документ /IOLA/docs/заметка.md`;
274
+ - `сделай ссылку на документ /IOLA/docs/заметка.md`;
275
+ - `переименуй документ /IOLA/docs/заметка.md в лекарства.md`;
276
+ - `удали документ /IOLA/docs/лекарства.md`.
237
277
 
238
278
  ### yandex-contacts
239
279
 
@@ -294,7 +334,13 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
294
334
 
295
335
  ### yandex-telemost
296
336
 
297
- Подготовить встречу через календарное событие. CLI не должен сам нажимать финальные кнопки в интерфейсе Яндекса без явного действия пользователя.
337
+ Подготовить встречу через Телемост:
338
+
339
+ - проверить режим Телемоста;
340
+ - попробовать создать ссылку через прямой API, если он доступен текущему аккаунту;
341
+ - если прямой API недоступен, создать календарное событие и честно сообщить, что ссылка Телемоста не была получена через API.
342
+
343
+ CLI не должен сам нажимать финальные кнопки в веб-интерфейсе Яндекса без явного действия пользователя и не должен выдумывать ссылку Телемоста.
298
344
 
299
345
  ## Yandex Connector backlog
300
346