@iola_adm/iola-cli 0.2.32 → 0.2.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iola_adm/iola-cli",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "CLI и AI-агент городского округа Йошкар-Ола.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/adm-iola/iola-cli#readme",
package/src/cli.js CHANGED
@@ -4148,8 +4148,13 @@ async function yandexMailCount(options = {}) {
4148
4148
  async function yandexMailSearch(query, options = {}) {
4149
4149
  const normalized = normalizeGeoText(query);
4150
4150
  if (!normalized) return yandexMailList(options);
4151
+ const tokens = normalized.split(/\s+/u).filter((token) => token.length > 2 && !/^(про|обо?|где|что|кто|как)$/iu.test(token));
4151
4152
  const rows = await yandexMailList({ ...options, limit: Math.max(50, Number(options.limit || 20) * 3) });
4152
- return rows.filter((row) => normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`).includes(normalized)).slice(0, Number(options.limit || 20));
4153
+ return rows.filter((row) => {
4154
+ const haystack = normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`);
4155
+ if (haystack.includes(normalized)) return true;
4156
+ return tokens.length > 0 && tokens.every((token) => haystack.includes(token));
4157
+ }).slice(0, Number(options.limit || 20));
4153
4158
  }
4154
4159
 
4155
4160
  async function yandexMailWatchTick(options = {}) {
@@ -4205,7 +4210,7 @@ async function yandexMailRead(uid, options = {}) {
4205
4210
  try {
4206
4211
  await imapAuthenticate(session, email, token);
4207
4212
  await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
4208
- const bodyAccessor = options.markSeen === false ? "BODY.PEEK[TEXT]" : "BODY[TEXT]";
4213
+ const bodyAccessor = options.markSeen === false ? "BODY.PEEK[]" : "BODY[]";
4209
4214
  const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT MESSAGE-ID REFERENCES)] ${bodyAccessor})`, { timeout: 60000 });
4210
4215
  return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
4211
4216
  } finally {
@@ -4646,14 +4651,14 @@ function stripMailBody(value) {
4646
4651
  const raw = String(value || "").replace(/\r/g, "");
4647
4652
  const withoutFetch = raw
4648
4653
  .replace(/^\* \d+ FETCH[^\n]*\n?/u, "")
4649
- .replace(/^BODY\[[^\n]*\]\s*\{\d+\}\n?/imu, "")
4654
+ .replace(/^BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\n?/imu, "")
4650
4655
  .replace(/\n\)\s*$/u, "");
4651
- const bodyMatch = withoutFetch.match(/BODY\[TEXT\]\s*\{\d+\}\s*([\s\S]*)/iu);
4656
+ const bodyMatch = withoutFetch.match(/BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\s*([\s\S]*)/iu);
4652
4657
  if (bodyMatch?.[1]) return extractMimeText(bodyMatch[1]);
4653
4658
  const headerEnd = withoutFetch.indexOf("\n\n");
4654
4659
  if (headerEnd < 0) return withoutFetch.replace(/[^\S\n]+/g, " ").trim();
4655
4660
  const headers = withoutFetch.slice(0, headerEnd);
4656
- const body = withoutFetch.slice(headerEnd + 2).replace(/^BODY\[[^\n]*\]\s*\{\d+\}\n?/imu, "");
4661
+ const body = withoutFetch.slice(headerEnd + 2).replace(/^BODY(?:\.PEEK)?\[[^\n]*\]\s*\{\d+\}\n?/imu, "");
4657
4662
  return extractMimeText(`${headers}\n\n${body}`);
4658
4663
  }
4659
4664
 
@@ -4690,17 +4695,52 @@ function decodeMailPart(part) {
4690
4695
  const encoding = headers.match(/^Content-Transfer-Encoding:\s*([^\n]+)/imu)?.[1]?.trim().toLocaleLowerCase("en-US") || "";
4691
4696
  let decoded = body;
4692
4697
  if (encoding === "base64") {
4693
- decoded = Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
4698
+ const clean = body.replace(/\s+/g, "");
4699
+ decoded = /^[A-Z0-9+/]+={0,2}$/iu.test(clean) && clean.length >= 8
4700
+ ? Buffer.from(clean, "base64").toString("utf8")
4701
+ : body;
4694
4702
  } else if (encoding === "quoted-printable") {
4695
4703
  decoded = decodeQuotedPrintable(body);
4704
+ } else {
4705
+ const clean = body.replace(/\s+/g, "");
4706
+ if (/^[A-Z0-9+/]+={0,2}$/iu.test(clean) && clean.length >= 80) {
4707
+ const candidate = Buffer.from(clean, "base64").toString("utf8");
4708
+ if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) decoded = candidate;
4709
+ }
4710
+ }
4711
+ decoded = decodeEmbeddedBase64MailBody(decoded);
4712
+ const cleaned = decoded
4713
+ .replace(/<style\b[\s\S]*?<\/style>/giu, " ")
4714
+ .replace(/<script\b[\s\S]*?<\/script>/giu, " ")
4715
+ .replace(/<[^>]+>/g, " ")
4716
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]+/gu, " ")
4717
+ .replace(/[^\S\n]+/g, " ")
4718
+ .replace(/\n{3,}/g, "\n\n")
4719
+ .trim();
4720
+ const firstCyrillic = cleaned.search(/[А-Яа-яЁё]{3,}/u);
4721
+ if (firstCyrillic > 0 && firstCyrillic < 120 && cleaned.slice(0, firstCyrillic).includes("�")) {
4722
+ return cleaned.slice(firstCyrillic).trim();
4696
4723
  }
4697
- return decoded.replace(/<[^>]+>/g, " ").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
4724
+ return cleaned;
4698
4725
  }
4699
4726
 
4700
4727
  function decodeQuotedPrintable(value) {
4701
4728
  return Buffer.from(String(value || "").replace(/=\n/gu, "").replace(/=([A-F0-9]{2})/giu, (_, hex) => String.fromCharCode(parseInt(hex, 16))), "binary").toString("utf8");
4702
4729
  }
4703
4730
 
4731
+ function decodeEmbeddedBase64MailBody(value) {
4732
+ const text = String(value || "");
4733
+ if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(text) && !/[A-Z0-9+/]{120,}/u.test(text)) return text;
4734
+ const matches = text.match(/[A-Z0-9+/=\s]{160,}/giu) || [];
4735
+ for (const match of matches) {
4736
+ const clean = match.replace(/\s+/g, "");
4737
+ if (!/^[A-Z0-9+/]+={0,2}$/iu.test(clean) || clean.length < 160) continue;
4738
+ const candidate = Buffer.from(clean, "base64").toString("utf8");
4739
+ if (/(<!doctype|<html|<body|[А-Яа-яЁё]{3,})/u.test(candidate)) return candidate;
4740
+ }
4741
+ return text;
4742
+ }
4743
+
4704
4744
  async function yandexDavRequest(url, token, options = {}) {
4705
4745
  const response = await fetch(url, {
4706
4746
  method: options.method || "GET",
@@ -10236,7 +10276,7 @@ async function buildYandexDirectAnswer(question, history = []) {
10236
10276
  if (!rows.length) return "В письме не нашел совпадений со слоями школ и детских садов.";
10237
10277
  return ["Нашел в городских слоях:", ...rows.map((row, index) => `${index + 1}. ${row.name}${row.inn ? `, ИНН ${row.inn}` : ""}${row.address ? `, ${row.address}` : ""}`)].join("\n");
10238
10278
  }
10239
- if (/(адрес|карт|гео|место|где)/iu.test(normalized) && /(письм|письма|письме)/iu.test(normalized)) {
10279
+ if (/(адрес|карт|место|где)/iu.test(normalized) && /(письм|письма|письме)/iu.test(normalized)) {
10240
10280
  const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
10241
10281
  if (!uid) return "Из какого письма взять адрес? Укажите номер из списка или UID.";
10242
10282
  const rows = await yandexMailMapAddresses(uid, { mailbox: extractYandexMailboxName(question) || "INBOX" });
@@ -10366,7 +10406,7 @@ function isYandexServiceQuestion(normalized) {
10366
10406
 
10367
10407
  function isYandexIdentityQuestion(normalized) {
10368
10408
  const text = String(normalized || "");
10369
- return /(аккаунт|профил|логин|кто подключен|какой.*подключен|email|e-mail)/iu.test(text)
10409
+ return /(аккаунт|аккант|акант|акаунт|акк?аунт|профил|логин|кто подключен|какой.*подключ|email|e-mail)/iu.test(text)
10370
10410
  && /(яндекс|яндес|язндекс|язндекс|яндкс|yandex)/iu.test(text);
10371
10411
  }
10372
10412
 
@@ -10378,7 +10418,7 @@ function isYandexMailFollowupQuestion(normalized, question) {
10378
10418
  }
10379
10419
 
10380
10420
  function cleanupYandexQuery(question) {
10381
- const stop = /^(?:в|на|у|из|для|по|яндекс|yandex|найди|поиск|покажи|посмотри|проверь|почт\p{L}*|письм\p{L}*|календар\p{L}*|контакт\p{L}*)$/iu;
10421
+ const stop = /^(?:в|на|у|из|для|по|про|о|об|обо|яндекс|yandex|найди|поиск|покажи|посмотри|проверь|почт\p{L}*|письм\p{L}*|календар\p{L}*|контакт\p{L}*)$/iu;
10382
10422
  return String(question || "")
10383
10423
  .replace(/[?.!]+$/u, "")
10384
10424
  .split(/[^\p{L}\p{N}@._+-]+/gu)
@@ -81,7 +81,7 @@ assertIncludes(cliSource, "язндекс", "Yandex direct router should tolerat
81
81
  assertIncludes(cliSource, "yandexMailCount", "Yandex mail should answer unread count questions directly");
82
82
  assertIncludes(cliSource, "resolveYandexMailUidFromQuestion", "Yandex mail follow-ups should resolve selected message UID");
83
83
  assertIncludes(cliSource, "extractYandexMailUidByOrdinal", "Yandex mail follow-ups should support numbered selections");
84
- assertIncludes(cliSource, "BODY[TEXT]", "Yandex mail read should mark opened messages as seen");
84
+ assertIncludes(cliSource, "BODY[]", "Yandex mail read should fetch full MIME messages and mark opened messages as seen");
85
85
  assertIncludes(cliSource, "markSeen === false", "Yandex mail read should keep an explicit no-mark fallback");
86
86
  assertIncludes(cliSource, "yandex_mail_folders", "Yandex mail should expose folders as a tool");
87
87
  assertIncludes(cliSource, "yandex_mail_reply", "Yandex mail should expose reply as a tool");