@iola_adm/iola-cli 0.2.34 → 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/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,9 +192,27 @@ 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",
197
+ "yandex_calendar_calendars",
196
198
  "yandex_calendar_create_event",
197
199
  "yandex_calendar_list",
200
+ "yandex_calendar_get",
201
+ "yandex_calendar_search",
202
+ "yandex_calendar_update",
203
+ "yandex_calendar_move",
204
+ "yandex_calendar_delete",
205
+ "yandex_calendar_create_recurring_event",
206
+ "yandex_calendar_add_reminder",
207
+ "yandex_docs_status",
208
+ "yandex_docs_list",
209
+ "yandex_docs_find",
210
+ "yandex_docs_create_text",
211
+ "yandex_docs_read",
212
+ "yandex_docs_share",
213
+ "yandex_docs_rename",
214
+ "yandex_docs_delete",
215
+ "yandex_docs_save_answer",
198
216
  "yandex_contacts_status",
199
217
  "yandex_contacts_list",
200
218
  "yandex_contacts_search",
@@ -224,7 +242,12 @@ const YANDEX_TOOLS = [
224
242
  "yandex_contact_create_calendar_event",
225
243
  "yandex_contact_create_telemost_event",
226
244
  "yandex_contact_from_public_entity",
245
+ "yandex_telemost_status",
227
246
  "yandex_telemost_create_event",
247
+ "yandex_contact_full_pack",
248
+ "yandex_daily_digest",
249
+ "yandex_calendar_reminders_tick",
250
+ "yandex_disk_maintenance_tick",
228
251
  ];
229
252
  const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS, ...USER_SKILL_TOOLS];
230
253
  const ALL_TOOL_ALIASES = [...ALL_LOCAL_TOOLS, ...LEGACY_LOCAL_TOOLS];
@@ -3047,6 +3070,29 @@ async function handleSkills(args) {
3047
3070
  return;
3048
3071
  }
3049
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
+
3050
3096
  if (action === "create" || action === "new") {
3051
3097
  const options = parseOptions(args.slice(2));
3052
3098
  const result = await userSkillCreate({
@@ -3054,6 +3100,7 @@ async function handleSkills(args) {
3054
3100
  description: options.description || "",
3055
3101
  instructions: options.instructions || options.text || options.prompt || options._.join(" "),
3056
3102
  tools: parseCommaList(options["allowed-tools"] || options.tool || options.uses || ""),
3103
+ template: options.template,
3057
3104
  enable: Boolean(options.enable),
3058
3105
  overwrite: Boolean(options.force),
3059
3106
  confirm: true,
@@ -3064,6 +3111,20 @@ async function handleSkills(args) {
3064
3111
  return;
3065
3112
  }
3066
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
+
3067
3128
  if (action === "delete" || action === "remove" || action === "rm") {
3068
3129
  const options = parseOptions(args.slice(2));
3069
3130
  const result = await userSkillDelete(name, { confirm: Boolean(options.yes || options.force) });
@@ -3081,7 +3142,7 @@ async function handleSkills(args) {
3081
3142
  return;
3082
3143
  }
3083
3144
 
3084
- 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.");
3085
3146
  }
3086
3147
 
3087
3148
  async function handleTools(args) {
@@ -3377,6 +3438,21 @@ async function handleYandex(args) {
3377
3438
  return;
3378
3439
  }
3379
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
+
3380
3456
  if (action === "contacts-maintenance" || action === "contacts-watch" || action === "contacts-doctor") {
3381
3457
  await handleYandexContactsMaintenance([target, ...rest].filter(Boolean));
3382
3458
  return;
@@ -3418,6 +3494,9 @@ async function handleYandex(args) {
3418
3494
  iola yandex status|doctor
3419
3495
  iola yandex services
3420
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]
3421
3500
  iola yandex contacts-maintenance on|off|status|tick [--days 7] [--backup]
3422
3501
  iola yandex enable disk mail calendar
3423
3502
  iola yandex disable mail
@@ -3489,6 +3568,107 @@ async function handleYandexMailWatch(args = []) {
3489
3568
  throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
3490
3569
  }
3491
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
+
3492
3672
  async function handleYandexContactsMaintenance(args = []) {
3493
3673
  const [action = "status", ...rest] = args;
3494
3674
  const options = parseOptions(rest);
@@ -4146,9 +4326,27 @@ async function executeYandexTool(tool, args = {}) {
4146
4326
  if (tool === "yandex_mail_city_context") return yandexMailCityContext(args.uid || args.id, args);
4147
4327
  if (tool === "yandex_mail_map_addresses") return yandexMailMapAddresses(args.uid || args.id, args);
4148
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);
4149
4330
  if (tool === "yandex_calendar_status") return yandexCalendarStatus();
4331
+ if (tool === "yandex_calendar_calendars") return yandexCalendarCalendars(args);
4150
4332
  if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
4151
4333
  if (tool === "yandex_calendar_list") return yandexCalendarList(args);
4334
+ if (tool === "yandex_calendar_get") return yandexCalendarGet(args.query || args.uid || args.title || "", args);
4335
+ if (tool === "yandex_calendar_search") return yandexCalendarSearch(args.query || args.title || "", args);
4336
+ if (tool === "yandex_calendar_update") return yandexCalendarUpdate(args.query || args.uid || args.title || "", args);
4337
+ if (tool === "yandex_calendar_move") return yandexCalendarMove(args.query || args.uid || args.title || "", args);
4338
+ if (tool === "yandex_calendar_delete") return yandexCalendarDelete(args.query || args.uid || args.title || "", args);
4339
+ if (tool === "yandex_calendar_create_recurring_event") return yandexCalendarCreateRecurringEvent(args);
4340
+ if (tool === "yandex_calendar_add_reminder") return yandexCalendarAddReminder(args.query || args.uid || args.title || "", args);
4341
+ if (tool === "yandex_docs_status") return yandexDocsStatus();
4342
+ if (tool === "yandex_docs_list") return yandexDocsList(args);
4343
+ if (tool === "yandex_docs_find") return yandexDocsFind(args.query || args.name || "", args);
4344
+ if (tool === "yandex_docs_create_text") return yandexDocsCreateText(args);
4345
+ if (tool === "yandex_docs_read") return yandexDocsRead(args.path || args.remotePath || args.query || args.name, args);
4346
+ if (tool === "yandex_docs_share") return yandexDocsShare(args.path || args.remotePath || args.query || args.name, args);
4347
+ if (tool === "yandex_docs_rename") return yandexDocsRename(args.path || args.remotePath || args.query || args.name, args.name || args.newName || args.to, args);
4348
+ if (tool === "yandex_docs_delete") return yandexDocsDelete(args.path || args.remotePath || args.query || args.name, args);
4349
+ if (tool === "yandex_docs_save_answer") return yandexDocsCreateText({ ...args, text: args.text || args.answer || args.content });
4152
4350
  if (tool === "yandex_contacts_status") return yandexContactsStatus();
4153
4351
  if (tool === "yandex_contacts_list") return yandexContactsList(args);
4154
4352
  if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
@@ -4178,7 +4376,12 @@ async function executeYandexTool(tool, args = {}) {
4178
4376
  if (tool === "yandex_contact_create_calendar_event") return yandexContactCreateCalendarEvent(args);
4179
4377
  if (tool === "yandex_contact_create_telemost_event") return yandexContactCreateTelemostEvent(args);
4180
4378
  if (tool === "yandex_contact_from_public_entity") return yandexContactFromPublicEntity(args);
4379
+ if (tool === "yandex_telemost_status") return yandexTelemostStatus();
4181
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 });
4182
4385
  throw new Error(`Yandex tool неизвестен: ${tool}`);
4183
4386
  }
4184
4387
 
@@ -4349,6 +4552,142 @@ async function yandexMailWatchDisable() {
4349
4552
  return { enabled: false };
4350
4553
  }
4351
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
+
4352
4691
  async function yandexContactsMaintenanceEnable(days = 7, options = {}) {
4353
4692
  const config = await loadConfig();
4354
4693
  const safeDays = Math.max(1, Number(days || 7));
@@ -4552,6 +4891,47 @@ async function yandexMailCreateCalendarEvent(uid, args = {}) {
4552
4891
  });
4553
4892
  }
4554
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
+
4555
4935
  async function yandexMailSenderToContact(uid, args = {}) {
4556
4936
  if (!args.confirm) throw new Error("Для добавления отправителя в контакты нужен аргумент confirm=true.");
4557
4937
  if (!uid) throw new Error("UID письма обязателен.");
@@ -4980,29 +5360,53 @@ async function yandexDavRequest(url, token, options = {}) {
4980
5360
 
4981
5361
  async function yandexCalendarStatus() {
4982
5362
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
4983
- const url = await yandexCalendarBaseUrl(token);
5363
+ const calendars = await yandexCalendarCollections(token);
5364
+ const selected = pickYandexCalendar(calendars, {});
5365
+ const url = selected.url;
4984
5366
  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" };
5367
+ return { status: "ok", url, displayName: stripXmlTags(text.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "") || selected.name || "calendar", calendars: calendars.length };
5368
+ }
5369
+
5370
+ async function yandexCalendarCalendars(args = {}) {
5371
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5372
+ const calendars = await yandexCalendarCollections(token);
5373
+ return calendars.slice(0, Number(args.limit || 50));
4986
5374
  }
4987
5375
 
4988
5376
  async function yandexCalendarCreateEvent(args = {}) {
4989
5377
  if (!args.confirm) throw new Error("Для создания события в календаре нужен аргумент confirm=true.");
4990
5378
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
4991
- const baseUrl = await yandexCalendarBaseUrl(token);
5379
+ const baseUrl = await yandexCalendarBaseUrl(token, args);
4992
5380
  const uid = `${randomUUID()}@iola-cli`;
4993
5381
  const start = toIcsDate(args.start || args.date || new Date(Date.now() + 3600000).toISOString());
4994
5382
  const end = toIcsDate(args.end || new Date(Date.now() + 7200000).toISOString());
4995
5383
  const summary = args.title || args.summary || "Событие IOLA";
4996
5384
  const description = args.description || "";
4997
- const ics = buildIcsEvent({ uid, start, end, summary, description, location: args.location || "", attendees: args.attendees || args.to || [] });
5385
+ const ics = buildIcsEvent({
5386
+ uid,
5387
+ start,
5388
+ end,
5389
+ summary,
5390
+ description,
5391
+ location: args.location || "",
5392
+ attendees: args.attendees || args.to || [],
5393
+ rrule: args.rrule || buildIcsRrule(args),
5394
+ reminders: normalizeCalendarReminders(args.reminders || args.reminder || args.alarm),
5395
+ });
4998
5396
  const url = `${baseUrl}${encodeURIComponent(uid)}.ics`;
4999
5397
  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 };
5398
+ return { status: "calendar-event-created", uid, title: summary, start: args.start || args.date || "", end: args.end || "", url };
5399
+ }
5400
+
5401
+ async function yandexCalendarCreateRecurringEvent(args = {}) {
5402
+ if (!args.confirm) throw new Error("Для создания повторяющегося события нужен аргумент confirm=true.");
5403
+ const rrule = args.rrule || buildIcsRrule({ ...args, repeat: args.repeat || args.frequency || "weekly" });
5404
+ return yandexCalendarCreateEvent({ ...args, rrule, confirm: true });
5001
5405
  }
5002
5406
 
5003
5407
  async function yandexCalendarList(args = {}) {
5004
5408
  const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5005
- const baseUrl = await yandexCalendarBaseUrl(token);
5409
+ const baseUrl = await yandexCalendarBaseUrl(token, args);
5006
5410
  const start = toIcsDate(args.start || new Date().toISOString());
5007
5411
  const end = toIcsDate(args.end || new Date(Date.now() + 14 * 86400000).toISOString());
5008
5412
  const body = `<?xml version="1.0"?>
@@ -5011,10 +5415,113 @@ async function yandexCalendarList(args = {}) {
5011
5415
  <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
5416
  </c:calendar-query>`;
5013
5417
  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));
5418
+ return parseIcsEvents(text, { baseUrl }).slice(0, Number(args.limit || 20));
5419
+ }
5420
+
5421
+ async function yandexCalendarSearch(query, args = {}) {
5422
+ const needle = normalizeGeoText(query || args.query || args.title || "");
5423
+ const rows = await yandexCalendarList({
5424
+ ...args,
5425
+ start: args.start || new Date(Date.now() - 90 * 86400000).toISOString(),
5426
+ end: args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
5427
+ limit: Math.max(200, Number(args.limit || 20) * 10),
5428
+ });
5429
+ if (!needle) return rows.slice(0, Number(args.limit || 20));
5430
+ return rows.filter((row) => normalizeGeoText([
5431
+ row.title,
5432
+ row.description,
5433
+ row.location,
5434
+ row.uid,
5435
+ ].filter(Boolean).join(" ")).includes(needle)).slice(0, Number(args.limit || 20));
5436
+ }
5437
+
5438
+ async function yandexCalendarGet(query, args = {}) {
5439
+ const resolved = await resolveYandexCalendarEvent(query, args);
5440
+ if (resolved.status !== "ok") return resolved;
5441
+ return stripCalendarPrivateFields(resolved.event);
5015
5442
  }
5016
5443
 
5017
- async function yandexCalendarBaseUrl(token) {
5444
+ async function yandexCalendarUpdate(query, args = {}) {
5445
+ if (!args.confirm) throw new Error("Для изменения события нужен аргумент confirm=true.");
5446
+ const resolved = await resolveYandexCalendarEvent(query, args);
5447
+ if (resolved.status !== "ok") return resolved;
5448
+ const event = resolved.event;
5449
+ const start = toIcsDate(args.start || args.date || event.startIso || event.start);
5450
+ const end = toIcsDate(args.end || event.endIso || event.end || new Date(new Date(args.start || event.startIso || Date.now()).getTime() + 3600000).toISOString());
5451
+ const next = buildIcsEvent({
5452
+ uid: event.uid || `${randomUUID()}@iola-cli`,
5453
+ start,
5454
+ end,
5455
+ summary: args.title || args.summary || event.title || "Событие IOLA",
5456
+ description: args.description ?? event.description ?? "",
5457
+ location: args.location ?? event.location ?? "",
5458
+ attendees: args.attendees || args.to || event.attendees || [],
5459
+ rrule: args.rrule === null ? "" : (args.rrule || event.rrule || buildIcsRrule(args)),
5460
+ reminders: args.reminders || args.reminder || args.alarm ? normalizeCalendarReminders(args.reminders || args.reminder || args.alarm) : event.reminders || [],
5461
+ });
5462
+ await yandexDavRequest(event.url, await requireYandexOAuthToken("organizer", "Яндекс Календарь"), { method: "PUT", ics: true, body: next, timeout: 45000 });
5463
+ return { status: "calendar-event-updated", uid: event.uid, title: args.title || event.title, href: event.href };
5464
+ }
5465
+
5466
+ async function yandexCalendarMove(query, args = {}) {
5467
+ if (!args.confirm) throw new Error("Для переноса события нужен аргумент confirm=true.");
5468
+ const dateTime = args.start || args.date ? {} : extractDateTimeFromText(args.text || args.source_question || "");
5469
+ const start = args.start || args.date || dateTime.start;
5470
+ if (!start) throw new Error("Укажите новую дату/время события.");
5471
+ const end = args.end || dateTime.end || new Date(new Date(start).getTime() + 3600000).toISOString();
5472
+ return yandexCalendarUpdate(query, { ...args, start, end, confirm: true });
5473
+ }
5474
+
5475
+ async function yandexCalendarAddReminder(query, args = {}) {
5476
+ if (!args.confirm) throw new Error("Для добавления напоминания нужен аргумент confirm=true.");
5477
+ const minutes = Number(args.minutes || args.beforeMinutes || String(args.reminder || "").match(/\d+/u)?.[0] || 15);
5478
+ return yandexCalendarUpdate(query, { ...args, reminders: [`-${minutes}`], confirm: true });
5479
+ }
5480
+
5481
+ async function yandexCalendarDelete(query, args = {}) {
5482
+ if (!args.confirm) throw new Error("Для удаления события нужен аргумент confirm=true.");
5483
+ const resolved = await resolveYandexCalendarEvent(query, args);
5484
+ if (resolved.status !== "ok") return resolved;
5485
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5486
+ await yandexDavRequest(resolved.event.url, token, { method: "DELETE", timeout: 45000 });
5487
+ return { status: "calendar-event-deleted", uid: resolved.event.uid, title: resolved.event.title, href: resolved.event.href };
5488
+ }
5489
+
5490
+ async function resolveYandexCalendarEvent(query, args = {}) {
5491
+ const uid = String(args.uid || "").trim();
5492
+ const href = String(args.href || "").trim();
5493
+ if (href) {
5494
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Календарь");
5495
+ const url = new URL(href, "https://caldav.yandex.ru/").toString();
5496
+ const ics = await yandexDavRequest(url, token, { method: "GET", timeout: 45000 });
5497
+ const event = parseIcsEvents(ics, { baseUrl: url.replace(/[^/]+$/u, "") })[0];
5498
+ return event ? { status: "ok", event: { ...event, href, url, ics } } : { status: "not-found", query: href };
5499
+ }
5500
+ const needle = normalizeGeoText(uid || query || args.query || args.title || "");
5501
+ const rows = await yandexCalendarSearch("", {
5502
+ ...args,
5503
+ start: args.startRange || args.start || new Date(Date.now() - 365 * 86400000).toISOString(),
5504
+ end: args.endRange || args.end || new Date(Date.now() + 365 * 86400000).toISOString(),
5505
+ limit: 500,
5506
+ });
5507
+ const matches = rows.filter((row) => {
5508
+ if (uid && row.uid === uid) return true;
5509
+ const haystack = normalizeGeoText([row.title, row.description, row.location, row.start, row.startIso, row.end, row.endIso, row.uid, row.href].filter(Boolean).join(" "));
5510
+ return needle && haystack.includes(needle);
5511
+ });
5512
+ if (!matches.length) return { status: "not-found", kind: "calendar-event", query: query || uid };
5513
+ if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", kind: "calendar-event", query: query || uid, events: matches.slice(0, 10).map(stripCalendarPrivateFields) };
5514
+ return { status: "ok", event: matches[0] };
5515
+ }
5516
+
5517
+ async function yandexCalendarBaseUrl(token, args = {}) {
5518
+ const calendars = await yandexCalendarCollections(token);
5519
+ const selected = pickYandexCalendar(calendars, args);
5520
+ if (!selected) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
5521
+ return selected.url;
5522
+ }
5523
+
5524
+ async function yandexCalendarCollections(token) {
5018
5525
  const root = "https://caldav.yandex.ru/";
5019
5526
  const principalXml = await yandexDavRequest(root, token, {
5020
5527
  method: "PROPFIND",
@@ -5038,9 +5545,99 @@ async function yandexCalendarBaseUrl(token) {
5038
5545
  xml: true,
5039
5546
  body: "<?xml version=\"1.0\"?><d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>",
5040
5547
  });
5041
- const calendarHref = extractCalendarCollectionHref(listXml);
5042
- if (!calendarHref) throw new Error("В Яндекс Календаре не найдена календарная коллекция.");
5043
- return new URL(calendarHref, root).toString();
5548
+ return extractCalendarCollections(listXml).map((row) => ({ ...row, url: new URL(row.href, root).toString() }));
5549
+ }
5550
+
5551
+ function pickYandexCalendar(calendars, args = {}) {
5552
+ const query = normalizeGeoText(args.calendar || args.calendarName || args.name || "");
5553
+ if (query) {
5554
+ const found = calendars.find((row) => normalizeGeoText(`${row.name} ${row.href}`).includes(query));
5555
+ if (found) return found;
5556
+ }
5557
+ return calendars.find((row) => !/\/(?:inbox|outbox)\//iu.test(row.href)) || calendars[0] || null;
5558
+ }
5559
+
5560
+ async function yandexDocsStatus() {
5561
+ const info = await yandexDiskInfo();
5562
+ await ensureYandexDiskDir(`${CLOUD_DEFAULT_REMOTE_DIR}/docs`, { allowExisting: true });
5563
+ return { status: "ok", provider: "yandex-disk", folder: `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, totalSpace: info.totalSpace, usedSpace: info.usedSpace };
5564
+ }
5565
+
5566
+ async function yandexDocsList(args = {}) {
5567
+ const folder = normalizeCloudUserPath(args.path || args.folder || `${CLOUD_DEFAULT_REMOTE_DIR}/docs`, "yandex-disk");
5568
+ const rows = await yandexDiskListRecursive(folder, { depth: Number(args.depth || 3), limit: Number(args.limit || 100) }).catch(() => []);
5569
+ return rows.filter(isYandexDocumentResource).slice(0, Number(args.limit || 50));
5570
+ }
5571
+
5572
+ async function yandexDocsFind(query, args = {}) {
5573
+ const needle = normalizeGeoText(query || args.query || "");
5574
+ const rows = await yandexDocsList({ ...args, limit: Math.max(200, Number(args.limit || 20) * 10) });
5575
+ if (!needle) return rows.slice(0, Number(args.limit || 20));
5576
+ return rows.filter((row) => normalizeGeoText(`${row.name} ${row.path}`).includes(needle)).slice(0, Number(args.limit || 20));
5577
+ }
5578
+
5579
+ async function yandexDocsCreateText(args = {}) {
5580
+ if (!args.confirm) throw new Error("Для создания документа нужен аргумент confirm=true.");
5581
+ const text = String(args.text || args.content || "").trim();
5582
+ if (!text) throw new Error("Текст документа пустой.");
5583
+ const ext = normalizeYandexDocExtension(args.format || args.ext || path.extname(args.path || args.remotePath || ""));
5584
+ const title = slugYandexDiskName(args.title || args.name || `document-${timestampForFile()}`);
5585
+ const remotePath = normalizeCloudUserPath(args.path || args.remotePath || `${CLOUD_DEFAULT_REMOTE_DIR}/docs/${title}${title.endsWith(ext) ? "" : ext}`, "yandex-disk");
5586
+ const saved = await yandexDiskSaveText(text, remotePath);
5587
+ return { ...saved, status: "document-created", title: path.posix.basename(remotePath), remote: remotePath };
5588
+ }
5589
+
5590
+ async function yandexDocsRead(target, args = {}) {
5591
+ const doc = await resolveYandexDoc(target, args);
5592
+ if (doc.status !== "ok") return doc;
5593
+ if (!/\.(txt|md|csv|json|html)$/iu.test(doc.path)) {
5594
+ return { status: "binary-document", remote: doc.path, name: doc.name, message: "Этот документ не текстовый. Его можно скачать, переименовать, удалить или опубликовать ссылкой." };
5595
+ }
5596
+ return yandexDiskReadText(doc.path, args);
5597
+ }
5598
+
5599
+ async function yandexDocsShare(target, args = {}) {
5600
+ if (!args.confirm) throw new Error("Для публикации документа нужен аргумент confirm=true.");
5601
+ const doc = await resolveYandexDoc(target, args);
5602
+ if (doc.status !== "ok") return doc;
5603
+ return yandexDiskShareWithQr(doc.path, { ...args, confirm: true });
5604
+ }
5605
+
5606
+ async function yandexDocsRename(target, newName, args = {}) {
5607
+ if (!args.confirm) throw new Error("Для переименования документа нужен аргумент confirm=true.");
5608
+ const doc = await resolveYandexDoc(target, args);
5609
+ if (doc.status !== "ok") return doc;
5610
+ if (!newName) throw new Error("Укажите новое имя документа.");
5611
+ return yandexDiskRename(doc.path, newName, { ...args, confirm: true, overwrite: args.overwrite !== false });
5612
+ }
5613
+
5614
+ async function yandexDocsDelete(target, args = {}) {
5615
+ if (!args.confirm) throw new Error("Для удаления документа нужен аргумент confirm=true.");
5616
+ const doc = await resolveYandexDoc(target, args);
5617
+ if (doc.status !== "ok") return doc;
5618
+ return yandexDiskDelete(doc.path, args);
5619
+ }
5620
+
5621
+ async function resolveYandexDoc(target, args = {}) {
5622
+ const value = String(target || "").trim();
5623
+ if (value.startsWith("/")) {
5624
+ const statRow = await yandexDiskStat(value);
5625
+ return isYandexDocumentResource(statRow) ? { status: "ok", ...statRow } : { status: "not-document", path: value };
5626
+ }
5627
+ const rows = await yandexDocsFind(value, { ...args, limit: 10 });
5628
+ if (!rows.length) return { status: "not-found", query: value };
5629
+ if (rows.length > 1 && !args.selectFirst) return { status: "ambiguous", query: value, docs: rows.slice(0, 10) };
5630
+ return { status: "ok", ...rows[0] };
5631
+ }
5632
+
5633
+ function isYandexDocumentResource(row = {}) {
5634
+ return row.type !== "dir" && /\.(docx|xlsx|pptx|pdf|txt|md|html|csv|json)$/iu.test(row.name || row.path || "");
5635
+ }
5636
+
5637
+ function normalizeYandexDocExtension(value) {
5638
+ const ext = String(value || "").replace(/^\./u, "").toLocaleLowerCase("en-US");
5639
+ if (["txt", "md", "html", "csv", "json"].includes(ext)) return `.${ext}`;
5640
+ return ".md";
5044
5641
  }
5045
5642
 
5046
5643
  async function yandexContactsStatus() {
@@ -5392,6 +5989,47 @@ async function yandexContactFromPublicEntity(args = {}) {
5392
5989
  });
5393
5990
  }
5394
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
+
5395
6033
  async function resolveYandexContact(query, args = {}) {
5396
6034
  const rows = await yandexContactsList({ limit: Math.max(500, Number(args.limit || 100)), full: true });
5397
6035
  const normalized = normalizeContactLookupText(query || args.query || args.name || args.email || args.phone || "");
@@ -5639,25 +6277,92 @@ async function yandexContactsBaseUrl(token) {
5639
6277
  return new URL(addressBookHref, root).toString();
5640
6278
  }
5641
6279
 
6280
+ async function yandexTelemostStatus() {
6281
+ const calendar = await yandexCalendarStatus();
6282
+ return {
6283
+ status: "calendar-fallback",
6284
+ provider: "yandex-telemost",
6285
+ calendar: calendar.displayName,
6286
+ message: "Для обычного OAuth-подключения CLI использует календарное событие. Прямой Telemost API проверяется отдельно и может быть доступен только аккаунтам/организациям Яндекс 360.",
6287
+ };
6288
+ }
6289
+
5642
6290
  async function yandexTelemostCreateEvent(args = {}) {
6291
+ if (!args.confirm) throw new Error("Для создания встречи нужен аргумент confirm=true.");
6292
+ const telemost = await tryCreateYandexTelemostMeeting(args).catch((error) => ({
6293
+ status: "telemost-api-unavailable",
6294
+ error: error instanceof Error ? error.message : String(error),
6295
+ }));
5643
6296
  const description = [
5644
6297
  args.description || "",
5645
6298
  "",
5646
- "Телемост: создайте ссылку в Яндекс Календаре, если интерфейс календаря предложит видеовстречу.",
6299
+ telemost.joinUrl ? `Ссылка Телемоста: ${telemost.joinUrl}` : "Телемост: прямое создание ссылки через API недоступно для текущего OAuth-подключения. Событие создано в календаре как встреча; ссылку можно добавить вручную в Яндекс Календаре.",
5647
6300
  ].join("\n").trim();
5648
- return yandexCalendarCreateEvent({ ...args, description });
6301
+ const event = await yandexCalendarCreateEvent({
6302
+ ...args,
6303
+ title: args.title || args.summary || "Телемост IOLA",
6304
+ description,
6305
+ location: telemost.joinUrl || args.location || "Яндекс Телемост",
6306
+ confirm: true,
6307
+ });
6308
+ return { ...event, status: telemost.joinUrl ? "telemost-event-created" : "telemost-calendar-fallback-created", telemost };
5649
6309
  }
5650
6310
 
5651
- function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [] }) {
6311
+ async function tryCreateYandexTelemostMeeting(args = {}) {
6312
+ const token = await requireYandexOAuthToken("organizer", "Яндекс Телемост");
6313
+ const endpoints = [
6314
+ "https://cloud-api.yandex.net/v1/telemost/meetings",
6315
+ "https://api360.yandex.net/v1/telemost/meetings",
6316
+ ];
6317
+ let lastError = "";
6318
+ for (const endpoint of endpoints) {
6319
+ const response = await fetch(endpoint, {
6320
+ method: "POST",
6321
+ headers: {
6322
+ Authorization: `OAuth ${token}`,
6323
+ "content-type": "application/json",
6324
+ },
6325
+ body: JSON.stringify({
6326
+ title: args.title || args.summary || "Телемост IOLA",
6327
+ description: args.description || "",
6328
+ }),
6329
+ signal: AbortSignal.timeout(15000),
6330
+ }).catch((error) => ({ ok: false, status: 0, statusText: error.message, text: async () => error.message }));
6331
+ const text = await response.text().catch(() => "");
6332
+ if (response.ok) {
6333
+ const payload = text ? JSON.parse(text) : {};
6334
+ return {
6335
+ status: "telemost-api-created",
6336
+ endpoint,
6337
+ id: payload.id || payload.meeting_id || "",
6338
+ joinUrl: payload.join_url || payload.url || payload.link || payload.meeting_url || "",
6339
+ raw: payload,
6340
+ };
6341
+ }
6342
+ lastError = `${endpoint}: ${response.status} ${response.statusText} ${sanitizeSecretFromText(text.slice(0, 500), token)}`;
6343
+ if (![404, 405].includes(Number(response.status))) break;
6344
+ }
6345
+ throw new Error(lastError || "Telemost API недоступен.");
6346
+ }
6347
+
6348
+ function buildIcsEvent({ uid, start, end, summary, description, location, attendees = [], rrule = "", reminders = [] }) {
5652
6349
  const escape = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
5653
6350
  const attendeeLines = (Array.isArray(attendees) ? attendees : [attendees])
5654
6351
  .map((email) => String(email || "").trim())
5655
6352
  .filter((email) => /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(email))
5656
6353
  .map((email) => `ATTENDEE;CN=${escape(email)};ROLE=REQ-PARTICIPANT:mailto:${email}`);
6354
+ const reminderBlocks = normalizeCalendarReminders(reminders).map((minutes) => [
6355
+ "BEGIN:VALARM",
6356
+ `TRIGGER:-PT${Math.max(1, Math.abs(Number(minutes) || 15))}M`,
6357
+ "ACTION:DISPLAY",
6358
+ `DESCRIPTION:${escape(summary || "Напоминание")}`,
6359
+ "END:VALARM",
6360
+ ].join("\r\n"));
5657
6361
  return [
5658
6362
  "BEGIN:VCALENDAR",
5659
6363
  "VERSION:2.0",
5660
6364
  "PRODID:-//IOLA CLI//Yandex Calendar//RU",
6365
+ "CALSCALE:GREGORIAN",
5661
6366
  "BEGIN:VEVENT",
5662
6367
  `UID:${uid}`,
5663
6368
  `DTSTAMP:${toIcsDate(new Date().toISOString())}`,
@@ -5666,7 +6371,9 @@ function buildIcsEvent({ uid, start, end, summary, description, location, attend
5666
6371
  `SUMMARY:${escape(summary)}`,
5667
6372
  description ? `DESCRIPTION:${escape(description)}` : "",
5668
6373
  location ? `LOCATION:${escape(location)}` : "",
6374
+ rrule ? `RRULE:${rrule}` : "",
5669
6375
  ...attendeeLines,
6376
+ ...reminderBlocks,
5670
6377
  "END:VEVENT",
5671
6378
  "END:VCALENDAR",
5672
6379
  "",
@@ -5679,15 +6386,105 @@ function toIcsDate(value) {
5679
6386
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
5680
6387
  }
5681
6388
 
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);
6389
+ function parseIcsEvents(xmlOrIcs, options = {}) {
6390
+ const text = String(xmlOrIcs || "");
6391
+ const responses = text.split(/<[^:>]*:?response[^>]*>/iu).slice(1);
6392
+ if (responses.length) {
6393
+ return responses.flatMap((response) => {
6394
+ const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
6395
+ const calendarData = response.match(/<[^:>]*:?calendar-data[^>]*>([\s\S]*?)<\/[^:>]*:?calendar-data>/iu)?.[1] || response;
6396
+ return parseIcsEvents(calendarData, options).map((row) => ({
6397
+ ...row,
6398
+ href: row.href || href,
6399
+ url: row.url || (href ? new URL(href, options.baseUrl || "https://caldav.yandex.ru/").toString() : ""),
6400
+ }));
6401
+ });
6402
+ }
6403
+ const decoded = decodeXml(stripXmlTags(text));
6404
+ return decoded.split("BEGIN:VEVENT").slice(1).map((chunk) => {
6405
+ const lines = unfoldIcsLines(`BEGIN:VEVENT${chunk}`);
6406
+ const uid = icsValue(lines, "UID");
6407
+ const start = icsValue(lines, "DTSTART");
6408
+ const end = icsValue(lines, "DTEND");
6409
+ return {
6410
+ uid,
6411
+ title: unescapeIcsValue(icsValue(lines, "SUMMARY")),
6412
+ start,
6413
+ end,
6414
+ startIso: icsDateToIso(start),
6415
+ endIso: icsDateToIso(end),
6416
+ location: unescapeIcsValue(icsValue(lines, "LOCATION")),
6417
+ description: unescapeIcsValue(icsValue(lines, "DESCRIPTION")),
6418
+ rrule: icsValue(lines, "RRULE"),
6419
+ attendees: icsValues(lines, "ATTENDEE").map((value) => value.replace(/^mailto:/iu, "")),
6420
+ reminders: parseIcsReminderMinutes(lines),
6421
+ ics: decoded.includes("BEGIN:VCALENDAR") ? decoded : "",
6422
+ };
6423
+ }).filter((item) => item.uid || item.title);
6424
+ }
6425
+
6426
+ function unfoldIcsLines(value) {
6427
+ return String(value || "")
6428
+ .replace(/\r/g, "")
6429
+ .replace(/\n[ \t]/g, "")
6430
+ .split(/\n/u)
6431
+ .map((line) => line.trim())
6432
+ .filter(Boolean);
6433
+ }
6434
+
6435
+ function icsValues(lines, property) {
6436
+ const prop = String(property || "").toLocaleUpperCase("en-US");
6437
+ return lines
6438
+ .filter((line) => line.toLocaleUpperCase("en-US").startsWith(`${prop};`) || line.toLocaleUpperCase("en-US").startsWith(`${prop}:`))
6439
+ .map((line) => line.slice(line.indexOf(":") + 1).trim())
6440
+ .filter(Boolean);
6441
+ }
6442
+
6443
+ function icsValue(lines, property) {
6444
+ return icsValues(lines, property)[0] || "";
6445
+ }
6446
+
6447
+ function unescapeIcsValue(value) {
6448
+ return String(value || "")
6449
+ .replace(/\\n/giu, "\n")
6450
+ .replace(/\\,/gu, ",")
6451
+ .replace(/\\;/gu, ";")
6452
+ .replace(/\\\\/gu, "\\")
6453
+ .trim();
6454
+ }
6455
+
6456
+ function icsDateToIso(value) {
6457
+ const text = String(value || "").trim();
6458
+ const match = text.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/u);
6459
+ if (!match) return "";
6460
+ const iso = `${match[1]}-${match[2]}-${match[3]}T${match[4] || "00"}:${match[5] || "00"}:${match[6] || "00"}${text.endsWith("Z") ? "Z" : ""}`;
6461
+ const date = new Date(iso);
6462
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
6463
+ }
6464
+
6465
+ function parseIcsReminderMinutes(lines) {
6466
+ return lines
6467
+ .filter((line) => /^TRIGGER:/iu.test(line))
6468
+ .map((line) => Number(line.match(/PT(\d+)M/iu)?.[1] || 0))
6469
+ .filter(Boolean);
6470
+ }
6471
+
6472
+ function normalizeCalendarReminders(value) {
6473
+ const rows = Array.isArray(value) ? value : String(value || "").split(/[;,]/u);
6474
+ return rows.map((item) => Number(String(item).match(/\d+/u)?.[0] || 0)).filter(Boolean);
6475
+ }
6476
+
6477
+ function buildIcsRrule(args = {}) {
6478
+ const repeat = String(args.repeat || args.frequency || "").toLocaleLowerCase("ru-RU");
6479
+ if (!repeat && !args.count && !args.until) return "";
6480
+ const freq = /день|daily|day/iu.test(repeat) ? "DAILY"
6481
+ : /месяц|monthly|month/iu.test(repeat) ? "MONTHLY"
6482
+ : /год|year|yearly|annual/iu.test(repeat) ? "YEARLY"
6483
+ : "WEEKLY";
6484
+ const parts = [`FREQ=${freq}`];
6485
+ if (args.count) parts.push(`COUNT=${Number(args.count)}`);
6486
+ if (args.until) parts.push(`UNTIL=${toIcsDate(args.until)}`);
6487
+ return parts.join(";");
5691
6488
  }
5692
6489
 
5693
6490
  function extractDavHref(xml, propertyName) {
@@ -5706,6 +6503,24 @@ function extractCalendarCollectionHref(xml) {
5706
6503
  return "";
5707
6504
  }
5708
6505
 
6506
+ function extractCalendarCollections(xml) {
6507
+ const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
6508
+ const rows = [];
6509
+ for (const response of responses) {
6510
+ if (!/<[^:>]*:?calendar(?:\s|>|\/)/iu.test(response)) continue;
6511
+ const href = decodeXml(stripXmlTags(response.match(/<[^:>]*:?href[^>]*>([\s\S]*?)<\/[^:>]*:?href>/iu)?.[1] || "")).trim();
6512
+ if (!href || /\/(?:inbox|outbox)\//iu.test(href)) continue;
6513
+ const name = stripXmlTags(response.match(/<[^:>]*:?displayname[^>]*>([\s\S]*?)<\/[^:>]*:?displayname>/iu)?.[1] || "").trim() || path.posix.basename(href.replace(/\/$/u, ""));
6514
+ rows.push({ provider: "yandex-calendar", href, name, type: "calendar" });
6515
+ }
6516
+ return rows;
6517
+ }
6518
+
6519
+ function stripCalendarPrivateFields(row = {}) {
6520
+ const { ics: _ics, url: _url, ...rest } = row;
6521
+ return rest;
6522
+ }
6523
+
5709
6524
  function extractAddressBookCollectionHref(xml) {
5710
6525
  const responses = String(xml || "").split(/<[^:>]*:?response[^>]*>/iu).slice(1);
5711
6526
  for (const response of responses) {
@@ -10654,6 +11469,13 @@ function isCronDue(job) {
10654
11469
  return !lastRun || now.getTime() - lastRun.getTime() >= Number(everyDays[1]) * 24 * 60 * 60 * 1000;
10655
11470
  }
10656
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
+ }
10657
11479
  return !lastRun || now.toISOString().slice(0, 10) !== lastRun.toISOString().slice(0, 10);
10658
11480
  }
10659
11481
  if (normalized.includes("каждую неделю") || normalized.includes("weekly")) {
@@ -11205,6 +12027,49 @@ async function buildYandexDirectAnswer(question, history = []) {
11205
12027
  ].join("\n");
11206
12028
  }
11207
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
+
11208
12073
  if (/(контакт|адресн)/iu.test(normalized) && !mailFollowup && !isExplicitYandexDiskPathDelete(question)) {
11209
12074
  return await buildYandexContactsDirectAnswer(question, normalized);
11210
12075
  }
@@ -11252,6 +12117,12 @@ async function buildYandexDirectAnswer(question, history = []) {
11252
12117
  const result = await yandexMailSaveToDisk(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
11253
12118
  return `Письмо #${uid} сохранено на Яндекс Диск: ${result.remote || result.path}.`;
11254
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
+ }
11255
12126
  if (/(создай|добавь).{0,40}(событи|встреч|календар)/iu.test(normalized)) {
11256
12127
  const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
11257
12128
  if (!uid) return "Из какого письма создать событие? Укажите номер из списка или UID.";
@@ -11356,10 +12227,93 @@ async function buildYandexDirectAnswer(question, history = []) {
11356
12227
  return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
11357
12228
  }
11358
12229
 
11359
- if (/(календар|событи)/iu.test(normalized) && !/(создай|добавь|запланируй)/iu.test(normalized)) {
12230
+ if (/(документ|документы|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
12231
+ if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
12232
+ const result = await yandexDocsStatus();
12233
+ return `Яндекс Документы через Диск подключены. Папка: ${result.folder}.`;
12234
+ }
12235
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
12236
+ const text = extractShareMessage(question) || cleanupCloudSaveText(question);
12237
+ if (!text) return "Укажите текст документа. Пример: создай документ на Яндекс Диске текст: ...";
12238
+ const result = await yandexDocsCreateText({
12239
+ title: extractYandexDocTitle(question),
12240
+ text,
12241
+ format: /html/iu.test(normalized) ? "html" : /txt|текстов/iu.test(normalized) ? "txt" : "md",
12242
+ confirm: true,
12243
+ });
12244
+ return `Документ создан на Яндекс Диске: ${result.remote}.`;
12245
+ }
12246
+ if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
12247
+ const result = await yandexDocsRead(extractCloudPath(question) || cleanupYandexQuery(question), {});
12248
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12249
+ }
12250
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
12251
+ const result = await yandexDocsShare(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
12252
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12253
+ }
12254
+ if (/(переимен|rename)/iu.test(normalized)) {
12255
+ const result = await yandexDocsRename(extractCloudPath(question) || cleanupYandexQuery(question), extractCloudNewName(question), { confirm: true });
12256
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12257
+ }
12258
+ if (/(удали|удалить)/iu.test(normalized)) {
12259
+ const result = await yandexDocsDelete(extractCloudPath(question) || cleanupYandexQuery(question), { confirm: true });
12260
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12261
+ }
12262
+ const rows = /(найди|поиск)/iu.test(normalized)
12263
+ ? await yandexDocsFind(cleanupYandexQuery(question), { limit: 20 })
12264
+ : await yandexDocsList({ limit: 20 });
12265
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
12266
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
12267
+ }
12268
+
12269
+ if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
12270
+ if (/(статус|проверь|работает|доступ)/iu.test(normalized) && /(календар|телемост)/iu.test(normalized)) {
12271
+ const result = /телемост/iu.test(normalized) ? await yandexTelemostStatus() : await yandexCalendarStatus();
12272
+ return /телемост/iu.test(normalized)
12273
+ ? `${result.message} Календарь: ${result.calendar || "-"}.`
12274
+ : `Яндекс Календарь подключен: ${result.displayName || result.url}. Календарей: ${result.calendars || 1}.`;
12275
+ }
12276
+ if (/(какие|список).{0,30}(календар|календари)|календари/iu.test(normalized)) {
12277
+ const rows = await yandexCalendarCalendars({ limit: 20 });
12278
+ if (!rows.length) return "Календари Яндекса не найдены.";
12279
+ return ["Яндекс Календари:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.href}`)].join("\n");
12280
+ }
12281
+ if (/(напомин|уведом)/iu.test(normalized) && /(добав|постав|создай)/iu.test(normalized)) {
12282
+ const minutes = Number(question.match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 15);
12283
+ const result = await yandexCalendarAddReminder(cleanupCalendarEventQuery(question), { minutes, confirm: true });
12284
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12285
+ }
12286
+ if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
12287
+ const dateTime = extractDateTimeFromText(question);
12288
+ const title = extractCalendarTitle(question) || (/телемост/iu.test(normalized) ? "Телемост IOLA" : "Событие IOLA");
12289
+ const args = { ...dateTime, title, description: extractShareMessage(question) || "", confirm: true };
12290
+ const result = /телемост/iu.test(normalized)
12291
+ ? await yandexTelemostCreateEvent(args)
12292
+ : /кажд|еженед|ежеднев|ежемесяч|повтор/iu.test(normalized)
12293
+ ? await yandexCalendarCreateRecurringEvent({ ...args, ...extractCalendarRepeat(question), confirm: true })
12294
+ : await yandexCalendarCreateEvent(args);
12295
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12296
+ }
12297
+ if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) {
12298
+ const result = await yandexCalendarMove(cleanupCalendarEventQuery(question), { ...extractDateTimeFromText(question), confirm: true });
12299
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12300
+ }
12301
+ if (/(переимен|измени\s+назв|смени\s+назв)/iu.test(normalized)) {
12302
+ const result = await yandexCalendarUpdate(cleanupCalendarEventQuery(question), { title: extractCalendarTitle(question), confirm: true });
12303
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12304
+ }
12305
+ if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) {
12306
+ const result = await yandexCalendarDelete(cleanupCalendarEventQuery(question), { confirm: true });
12307
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12308
+ }
12309
+ if (/(найди|поиск|где|покажи).{0,40}(событи|встреч|телемост)/iu.test(normalized)) {
12310
+ const rows = await yandexCalendarSearch(cleanupCalendarEventQuery(question), { limit: 20 });
12311
+ if (!rows.length) return "События в Яндекс Календаре по запросу не найдены.";
12312
+ return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
12313
+ }
11360
12314
  const rows = await yandexCalendarList({ limit: 10 });
11361
12315
  if (!rows.length) return "В ближайшие дни событий в Яндекс Календаре не найдено.";
11362
- return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.start || "-"}`)].join("\n");
12316
+ return ["Яндекс Календарь:", ...rows.map((row, index) => `${index + 1}. ${row.title || "(без названия)"} — ${row.startIso || row.start || "-"}${row.location ? `, ${row.location}` : ""}`)].join("\n");
11363
12317
  }
11364
12318
 
11365
12319
  if (/(контакт|адресн)/iu.test(normalized)) {
@@ -11609,6 +12563,11 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
11609
12563
  const result = await yandexContactCreateDiskFolder({ query, confirm: true });
11610
12564
  return formatToolResult({ rows: [result], outputs: [] }, {});
11611
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
+ }
11612
12571
  if (/(отправь|пошли).{0,80}(ссылк|qr|qr-код|диск|яндекс.?диск)/iu.test(text)) {
11613
12572
  const remotePath = extractCloudPath(question);
11614
12573
  const contact = cleanupYandexContactActionQuery(question.replace(remotePath || "", " "));
@@ -11643,6 +12602,27 @@ async function buildYandexContactsDirectAnswer(question, normalized = "") {
11643
12602
  async function buildUserSkillDirectAnswer(question) {
11644
12603
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
11645
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
+ }
11646
12626
  if (/(создай|добавь|сделай|create|new)/iu.test(normalized)) {
11647
12627
  const name = extractUserSkillNameFromQuestion(question) || "user-skill";
11648
12628
  const result = await userSkillCreate({
@@ -11650,6 +12630,7 @@ async function buildUserSkillDirectAnswer(question) {
11650
12630
  description: extractUserSkillDescription(question, name),
11651
12631
  instructions: question,
11652
12632
  tools: inferUserSkillTools(question),
12633
+ template: normalizeUserSkillName(question.match(/(?:шаблон|template)\s+([a-z0-9а-яё_-]+)/iu)?.[1] || ""),
11653
12634
  enable: true,
11654
12635
  confirm: true,
11655
12636
  });
@@ -11682,7 +12663,7 @@ async function buildUserSkillDirectAnswer(question) {
11682
12663
  }
11683
12664
 
11684
12665
  function isYandexServiceQuestion(normalized) {
11685
- return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
12666
+ return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|документ|docs|360|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
11686
12667
  }
11687
12668
 
11688
12669
  function isYandexIdentityQuestion(normalized) {
@@ -11967,6 +12948,38 @@ async function buildCloudDirectAnswer(question) {
11967
12948
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
11968
12949
  try {
11969
12950
  const provider = await getCloudProvider();
12951
+ if (provider === "yandex-disk" && /(документ|docs|360)/iu.test(normalized)) {
12952
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) {
12953
+ const result = await yandexDocsShare(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
12954
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12955
+ }
12956
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) {
12957
+ const text = extractShareMessage(question) || cleanupCloudSaveText(question);
12958
+ if (!text) return "Укажите текст документа.";
12959
+ const result = await yandexDocsCreateText({ title: extractYandexDocTitle(question), text, confirm: true });
12960
+ return `Документ создан на Яндекс Диске: ${result.remote}.`;
12961
+ }
12962
+ if (/(прочитай|открой|покажи\s+текст)/iu.test(normalized)) {
12963
+ const result = await yandexDocsRead(extractCloudPath(question) || cleanupCloudQuery(question), {});
12964
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12965
+ }
12966
+ if (/(переимен|rename)/iu.test(normalized)) {
12967
+ const result = await yandexDocsRename(extractCloudPath(question) || cleanupCloudQuery(question), extractCloudNewName(question), { confirm: true });
12968
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12969
+ }
12970
+ if (/(удали|удалить)/iu.test(normalized)) {
12971
+ const result = await yandexDocsDelete(extractCloudPath(question) || cleanupCloudQuery(question), { confirm: true });
12972
+ return formatToolResult({ rows: [result], outputs: [] }, {});
12973
+ }
12974
+ if (/(найди|поиск)/iu.test(normalized)) {
12975
+ const rows = await yandexDocsFind(cleanupCloudQuery(question), { limit: 20 });
12976
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
12977
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
12978
+ }
12979
+ const rows = await yandexDocsList({ limit: 20 });
12980
+ if (!rows.length) return "Документы на Яндекс Диске не найдены.";
12981
+ return ["Документы на Яндекс Диске:", ...rows.map((row, index) => `${index + 1}. ${row.name} — ${row.path}`)].join("\n");
12982
+ }
11970
12983
  if (provider === "yandex-disk" && /(мест[оа]|сколько.*занято|сколько.*свобод|инфо|статус)/iu.test(normalized)) {
11971
12984
  const info = await yandexDiskInfo();
11972
12985
  return [
@@ -12200,6 +13213,7 @@ function extractCloudPath(question) {
12200
13213
  function cleanupCloudPathCandidate(value) {
12201
13214
  return String(value || "")
12202
13215
  .replace(/\s+(?:по\s+почт[еуы]|на\s+почт[уые]|контакту|получател[юя]|кому|с\s+темой|тема\s*:|текст\s*:).*$/iu, "")
13216
+ .replace(/\s+(?:на\s+яндекс.?диск(?:е)?|на\s+диск(?:е)?|в\s+облак(?:е|о)?).*/iu, "")
12203
13217
  .replace(/[.!?]+$/u, "")
12204
13218
  .trim();
12205
13219
  }
@@ -12249,7 +13263,50 @@ function extractMailSubject(question) {
12249
13263
  }
12250
13264
 
12251
13265
  function extractShareMessage(question) {
12252
- return String(question || "").match(/(?:текст|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
13266
+ return String(question || "").match(/(?:текст|text|сообщение|body)\s*:\s*(.*)$/iu)?.[1]?.trim() || "";
13267
+ }
13268
+
13269
+ function extractYandexDocTitle(question) {
13270
+ const text = String(question || "");
13271
+ const raw = text.match(/(?:названи(?:е|ем)|имя|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
13272
+ || text.match(/(?:документ|файл)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
13273
+ || "";
13274
+ return raw.replace(/\s+(?:текст|text|body|сообщение)\s*:?.*$/iu, "").trim();
13275
+ }
13276
+
13277
+ function extractCalendarTitle(question) {
13278
+ const text = String(question || "");
13279
+ const raw = text.match(/(?:тема|названи(?:е|ем)|title)\s*:?\s*["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
13280
+ || text.match(/(?:событи[ея]|встреч[ауи]|телемост)\s+["«]?([^"».,:;]+)["»]?/iu)?.[1]?.trim()
13281
+ || "";
13282
+ return raw
13283
+ .replace(/\s+(?:сегодня|завтра|послезавтра)(?:\s|$).*$/iu, "")
13284
+ .replace(/\s+(?:в\s+\d{1,2}(?::\d{2})?|на\s+\d{1,2}[.\-/]\d{1,2}[\d.\-/]*)(?:\s|$).*$/iu, "")
13285
+ .trim();
13286
+ }
13287
+
13288
+ function extractCalendarRepeat(question) {
13289
+ const text = String(question || "").toLocaleLowerCase("ru-RU");
13290
+ const count = Number(text.match(/(\d+)\s*(?:раз|повтор)/iu)?.[1] || 0);
13291
+ return {
13292
+ repeat: /ежеднев|каждый\s+день/iu.test(text) ? "daily"
13293
+ : /ежемесяч|каждый\s+месяц/iu.test(text) ? "monthly"
13294
+ : /ежегод|каждый\s+год/iu.test(text) ? "yearly"
13295
+ : "weekly",
13296
+ count: count || undefined,
13297
+ };
13298
+ }
13299
+
13300
+ function cleanupCalendarEventQuery(question) {
13301
+ return String(question || "")
13302
+ .replace(/(?:создай|добавь|запланируй|назначь|перенеси|перемести|измени|смени|удали|удалить|отмени|отменить|найди|поиск|покажи|добавь\s+напоминание|поставь\s+напоминание)/giu, " ")
13303
+ .replace(/(?:^|\s)(?:событи\p{L}*|встреч\p{L}*|телемост\p{L}*|календар\p{L}*|напомин\p{L}*|уведом\p{L}*|яндекс|на|к|ко|в|во|сегодня|завтра|послезавтра|час|часа|часов|минут|минуты)(?=\s|$)/giu, " ")
13304
+ .replace(/\d{1,2}[.\-/]\d{1,2}(?:[.\-/]\d{2,4})?/gu, " ")
13305
+ .replace(/\d{1,2}[:.]\d{2}/gu, " ")
13306
+ .replace(/(?:^|\s)\d{1,3}(?=\s|$)/gu, " ")
13307
+ .replace(/[,:;.!?«»"()]+/gu, " ")
13308
+ .replace(/\s+/g, " ")
13309
+ .trim();
12253
13310
  }
12254
13311
 
12255
13312
  function parseYandexDiskPackageRequest(question) {
@@ -12826,9 +13883,9 @@ async function buildLocalToolPlan(question, providerConfig, options) {
12826
13883
  `Доступные tools: ${availableToolNames(options).join(", ")}.`,
12827
13884
  "Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
12828
13885
  "Минимальные 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.",
12831
- "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.",
12832
13889
  "MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
12833
13890
  "Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
12834
13891
  `Вопрос: ${question}`,
@@ -12908,6 +13965,12 @@ function inferToolPlan(question, options = {}) {
12908
13965
  }],
12909
13966
  };
12910
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
+ }
12911
13974
  if (/(яндекс|yandex)/iu.test(normalized) && /(аккаунт|профил|логин|почт[аы]|email|e-mail|кто подключен)/iu.test(normalized)) {
12912
13975
  return { steps: [{ tool: "yandex_identity_me", args: {} }] };
12913
13976
  }
@@ -12938,12 +14001,31 @@ function inferToolPlan(question, options = {}) {
12938
14001
  if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
12939
14002
  if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
12940
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 } }] };
12941
14005
  if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
12942
14006
  if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
12943
14007
  return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
12944
14008
  }
14009
+ if (/(документ|docs|360)/iu.test(normalized) && /(яндекс|диск|облак|360|docs)/iu.test(normalized)) {
14010
+ const target = extractCloudPath(question) || cleanupYandexQuery(question);
14011
+ if (/(создай|сделай|запиши|сохрани)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_create_text", args: { title: extractYandexDocTitle(question), text: extractShareMessage(question) || cleanupCloudSaveText(question), confirm: true } }] };
14012
+ if (/(прочитай|открой|текст)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_read", args: { query: target } }] };
14013
+ if (/(ссылк|поделись|опубликуй|qr|qr-код)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_share", args: { query: target, confirm: true } }] };
14014
+ if (/(переимен|rename)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_rename", args: { query: target, name: extractCloudNewName(question), confirm: true } }] };
14015
+ if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_delete", args: { query: target, confirm: true } }] };
14016
+ if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_docs_find", args: { query: cleanupYandexQuery(question), limit: 20 } }] };
14017
+ return { steps: [{ tool: "yandex_docs_list", args: { limit: 20 } }] };
14018
+ }
12945
14019
  if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
12946
- return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
14020
+ if (/(напомин|уведом|следи|монитор)/iu.test(normalized) && /(проверь|tick|сейчас)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_reminders_tick", args: { force: true } }] };
14021
+ if (/(создай|добавь|запланируй|назначь)/iu.test(normalized)) {
14022
+ const dateTime = extractDateTimeFromText(question);
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 } }] };
14024
+ }
14025
+ if (/(перенеси|перемести|измени\s+время|смени\s+время)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_move", args: { query: cleanupCalendarEventQuery(question), ...extractDateTimeFromText(question), confirm: true } }] };
14026
+ if (/(удали|удалить|отмени|отменить)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_delete", args: { query: cleanupCalendarEventQuery(question), confirm: true } }] };
14027
+ if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_calendar_search", args: { query: cleanupCalendarEventQuery(question), limit: 20 } }] };
14028
+ return { steps: [{ tool: "yandex_calendar_list", args: { limit: 20 } }] };
12947
14029
  }
12948
14030
  if (/(контакт|адресн)/iu.test(normalized)) {
12949
14031
  if (/(дубликат|повтор)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_find_duplicates", args: { limit: 20 } }] };
@@ -12951,6 +14033,7 @@ function inferToolPlan(question, options = {}) {
12951
14033
  if (/(экспорт|выгруз)/iu.test(normalized)) return { steps: [{ tool: /csv/iu.test(normalized) ? "yandex_contacts_export_csv" : "yandex_contacts_export_vcard", args: {} }] };
12952
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 } }] };
12953
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 } }] };
12954
14037
  if (/(удали|удалить)/iu.test(normalized)) return { steps: [{ tool: "yandex_contacts_delete", args: { query: cleanupYandexContactActionQuery(question), confirm: true } }] };
12955
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 } }] };
12956
14039
  if (/(отправь|пошли|напиши).{0,40}(письм|сообщ)/iu.test(normalized)) {
@@ -13537,6 +14620,21 @@ function formatToolResult(result, options) {
13537
14620
  return `${name}: ${row.field} = ${row.value ?? "не указано"}`;
13538
14621
  }
13539
14622
  if (row.date && row.time) return `Сегодня ${row.date}, ${row.time}.`;
14623
+ if (row.status === "calendar-event-created" || row.status === "telemost-event-created" || row.status === "telemost-calendar-fallback-created") {
14624
+ 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 недоступна, создано событие календаря." : ""}`;
14625
+ }
14626
+ if (row.status === "calendar-event-updated") return `Событие обновлено: ${row.title || row.uid}`;
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;
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");
14632
+ if (row.status === "not-found" && row.kind === "calendar-event") return `Событие не найдено: ${row.query}`;
14633
+ if (row.status === "document-created") return `Документ создан на Яндекс Диске: ${row.remote}`;
14634
+ if (row.status === "binary-document") return `${row.name || row.remote}: ${row.message}`;
14635
+ if (row.status === "not-document") return `Это не документ: ${row.path}`;
14636
+ if (row.status === "ambiguous" && row.docs) return [`Нашел несколько документов. Уточните:`, ...row.docs.map((doc, index) => `${index + 1}. ${doc.name} — ${doc.path}`)].join("\n");
14637
+ if (row.status === "not-found" && row.docs !== undefined) return `Документ не найден: ${row.query}`;
13540
14638
  if (row.status === "contact-mail-sent") return `Письмо контакту отправлено: ${row.contact}. Тема: ${row.subject || "-"}`;
13541
14639
  if (row.status === "contact-disk-link-sent") return `Отправил контакту ${row.contact} ссылку на Яндекс Диск.\nСсылка: ${row.publicUrl}\nQR-код: ${row.qrPublicUrl}`;
13542
14640
  if (row.status === "contact-folder-created") return `Папка контакта создана: ${row.remote}\nКарточка: ${row.cardPath}`;
@@ -15094,12 +16192,12 @@ function parseOptions(args) {
15094
16192
 
15095
16193
  for (let index = 0; index < args.length; index += 1) {
15096
16194
  const arg = args[index];
15097
- 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") {
15098
16196
  result[arg.slice(2)] = true;
15099
16197
  } else if (arg === "--check" || arg === "--upgrade-node") {
15100
16198
  result.check = true;
15101
16199
  result[arg.slice(2)] = true;
15102
- } 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") {
15103
16201
  result[arg.slice(2)] = args[index + 1];
15104
16202
  index += 1;
15105
16203
  } else {
@@ -15323,9 +16421,13 @@ function printSkillsList(skills, config) {
15323
16421
  async function executeUserSkillTool(tool, args = {}) {
15324
16422
  if (tool === "user_skill_list") return listSkills(await loadConfig()).filter((skill) => skill.source === "user");
15325
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 });
15326
16425
  if (tool === "user_skill_enable") return userSkillSetEnabled(args.name, true);
15327
16426
  if (tool === "user_skill_disable") return userSkillSetEnabled(args.name, false);
15328
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) };
15329
16431
  throw new Error(`Неизвестный user skill tool: ${tool}`);
15330
16432
  }
15331
16433
 
@@ -15333,22 +16435,41 @@ async function userSkillCreate(args = {}) {
15333
16435
  if (!args.confirm) throw new Error("Для создания пользовательского skill нужен аргумент confirm=true.");
15334
16436
  const name = normalizeUserSkillName(args.name || args.skill || args.title);
15335
16437
  if (!name) throw new Error("Имя skill обязательно.");
15336
- const description = String(args.description || `Пользовательский skill: ${name}`).trim();
15337
- 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();
15338
16441
  if (!instructions) throw new Error("Инструкции skill обязательны.");
15339
16442
  const tools = parseCommaList(args.tools || args.allowedTools || args.allowed_tools || args.uses || "");
16443
+ const mergedTools = [...new Set([...(template?.tools || []), ...tools])];
15340
16444
  const dir = path.join(USER_SKILLS_DIR, name);
15341
16445
  const file = path.join(dir, "SKILL.md");
15342
16446
  if (existsSync(file) && !args.overwrite) throw new Error(`Skill уже существует: ${name}. Используйте overwrite=true или --force.`);
15343
16447
  await mkdir(dir, { recursive: true });
15344
- const body = buildUserSkillMarkdown({ name, description, instructions, tools });
16448
+ const body = buildUserSkillMarkdown({ name, description, instructions, tools: mergedTools });
15345
16449
  await writeFile(file, body, "utf8");
15346
16450
  let enabled = false;
15347
16451
  if (args.enable) {
15348
16452
  await userSkillSetEnabled(name, true);
15349
16453
  enabled = true;
15350
16454
  }
15351
- 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" };
15352
16473
  }
15353
16474
 
15354
16475
  async function userSkillSetEnabled(name, enabled) {
@@ -15442,9 +16563,23 @@ function inferUserSkillTools(question) {
15442
16563
  }
15443
16564
  if (/(календар|событи|встреч|телемост)/iu.test(text)) {
15444
16565
  tools.add("yandex_calendar_list");
16566
+ tools.add("yandex_calendar_search");
15445
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");
15446
16572
  tools.add("yandex_telemost_create_event");
15447
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
+ }
15448
16583
  if (/(контакт|адресн)/iu.test(text)) {
15449
16584
  tools.add("yandex_contacts_search");
15450
16585
  tools.add("yandex_contacts_get");
@@ -15485,6 +16620,69 @@ function inferUserSkillTools(question) {
15485
16620
  return [...tools];
15486
16621
  }
15487
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
+
15488
16686
  function buildUserSkillMarkdown({ name, description, instructions, tools = [] }) {
15489
16687
  const toolLines = tools.length
15490
16688
  ? ["", "Разрешенные/ожидаемые tools для этого skill:", "", ...tools.map((tool) => `- \`${tool}\``)]
@@ -15554,7 +16752,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
15554
16752
  if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
15555
16753
  const cloudQuestion = isCloudQuestion(normalized);
15556
16754
  if (enabled.has("personal-docs") && cloudQuestion) selected.add("personal-docs");
15557
- if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|облако)/iu.test(normalized)) selected.add("yandex-services");
16755
+ if (enabled.has("yandex-services") && /(яндекс|диск|почт|письм|календар|контакт|телемост|документ|docs|360|облако)/iu.test(normalized)) selected.add("yandex-services");
15558
16756
  if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
15559
16757
  if (enabled.has("local-files") && !cloudQuestion && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
15560
16758
  if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");