@iola_adm/iola-cli 0.2.27 → 0.2.29

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.27",
3
+ "version": "0.2.29",
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
@@ -4002,6 +4002,20 @@ async function yandexMailList(options = {}) {
4002
4002
  }
4003
4003
  }
4004
4004
 
4005
+ async function yandexMailCount(options = {}) {
4006
+ const { token, email } = await yandexMailCredentials();
4007
+ const session = await imapConnect();
4008
+ try {
4009
+ await imapAuthenticate(session, email, token);
4010
+ await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
4011
+ const criterion = options.unread ? "UNSEEN" : "ALL";
4012
+ const search = await imapCommand(session, `UID SEARCH ${criterion}`);
4013
+ return parseImapSearchUids(search).length;
4014
+ } finally {
4015
+ await imapClose(session);
4016
+ }
4017
+ }
4018
+
4005
4019
  async function yandexMailSearch(query, options = {}) {
4006
4020
  const normalized = normalizeGeoText(query);
4007
4021
  if (!normalized) return yandexMailList(options);
@@ -4016,7 +4030,8 @@ async function yandexMailRead(uid, options = {}) {
4016
4030
  try {
4017
4031
  await imapAuthenticate(session, email, token);
4018
4032
  await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
4019
- const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT])`, { timeout: 60000 });
4033
+ const bodyAccessor = options.markSeen === false ? "BODY.PEEK[TEXT]" : "BODY[TEXT]";
4034
+ const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] ${bodyAccessor})`, { timeout: 60000 });
4020
4035
  return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
4021
4036
  } finally {
4022
4037
  await imapClose(session);
@@ -9456,6 +9471,20 @@ async function aiAsk(args, context = {}) {
9456
9471
  const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
9457
9472
  const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
9458
9473
  const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
9474
+ const casualAnswer = buildCasualDirectAnswer(question);
9475
+ if (casualAnswer) {
9476
+ if (historyEnabled) {
9477
+ recordAskHistory({ question, answer: casualAnswer, providerConfig, dataContext, error: "", sessionId });
9478
+ appendSessionExchange(sessionId, question, casualAnswer, dataContext, "");
9479
+ }
9480
+ emitEvent(options, "answer", { length: casualAnswer.length, sessionId, direct: true });
9481
+ if (options.output) {
9482
+ await assertPermission("writeFiles");
9483
+ await writeFile(options.output, casualAnswer, "utf8");
9484
+ }
9485
+ if (!options.quiet) console.log(casualAnswer);
9486
+ return casualAnswer;
9487
+ }
9459
9488
  const yandexAnswer = await buildYandexDirectAnswer(question, context.history || history);
9460
9489
  if (yandexAnswer) {
9461
9490
  if (historyEnabled) {
@@ -9588,10 +9617,12 @@ async function buildYandexDirectAnswer(question, history = []) {
9588
9617
  const normalized = String(question || "").toLocaleLowerCase("ru-RU");
9589
9618
  const previousAssistantText = [...(history || [])].reverse().find((item) => item.role === "assistant")?.content || "";
9590
9619
  const mailContext = /Яндекс Почта|Письмо #|\bUID\b|#\d{3,}/iu.test(previousAssistantText);
9591
- if (!isYandexServiceQuestion(normalized) && !(/^\s*\d{3,}\s*$/u.test(question) && mailContext)) return "";
9620
+ const mailFollowup = mailContext && isYandexMailFollowupQuestion(normalized, question);
9621
+ if (!isYandexServiceQuestion(normalized) && !mailFollowup) return "";
9592
9622
  try {
9593
- if (/^\s*\d{3,}\s*$/u.test(question) && mailContext) {
9594
- const uid = extractYandexMailUid(question);
9623
+ if (mailFollowup && (isYandexMailReadRequest(normalized) || isYandexMailSelectionQuestion(question))) {
9624
+ const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
9625
+ || await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
9595
9626
  const row = await yandexMailRead(uid);
9596
9627
  if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
9597
9628
  return formatYandexMailRead(row);
@@ -9620,8 +9651,18 @@ async function buildYandexDirectAnswer(question, history = []) {
9620
9651
  const result = await yandexMailSend({ ...draft, confirm: true });
9621
9652
  return `Письмо отправлено: ${result.to.join(", ")}. Тема: ${result.subject}.`;
9622
9653
  }
9654
+ if (/(сколько|количеств|есть\s+ли)/iu.test(normalized) && /непрочитан/iu.test(normalized)) {
9655
+ const count = await yandexMailCount({ unread: true });
9656
+ if (!count) return "Непрочитанных писем нет.";
9657
+ const latest = await yandexMailList({ limit: 1, unread: true });
9658
+ return [
9659
+ `Непрочитанных писем: ${count}.`,
9660
+ latest[0] ? `Самое свежее: ${formatYandexMailSummary(latest[0])}` : "",
9661
+ ].filter(Boolean).join("\n");
9662
+ }
9623
9663
  if (isYandexMailReadRequest(normalized)) {
9624
- const uid = extractYandexMailUid(question) || await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
9664
+ const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
9665
+ || await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
9625
9666
  if (!uid) return "Писем для чтения не найдено.";
9626
9667
  const row = await yandexMailRead(uid);
9627
9668
  if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
@@ -9668,6 +9709,12 @@ function isYandexIdentityQuestion(normalized) {
9668
9709
  && /(яндекс|яндес|язндекс|язндекс|яндкс|yandex)/iu.test(text);
9669
9710
  }
9670
9711
 
9712
+ function isYandexMailFollowupQuestion(normalized, question) {
9713
+ return isYandexMailReadRequest(normalized)
9714
+ || isYandexMailSelectionQuestion(question)
9715
+ || /(самое\s+свеж|последн|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(String(normalized || ""));
9716
+ }
9717
+
9671
9718
  function cleanupYandexQuery(question) {
9672
9719
  const stop = /^(?:в|на|у|из|для|по|яндекс|yandex|найди|поиск|покажи|посмотри|проверь|почт\p{L}*|письм\p{L}*|календар\p{L}*|контакт\p{L}*)$/iu;
9673
9720
  return String(question || "")
@@ -9684,9 +9731,41 @@ function extractYandexMailUid(question) {
9684
9731
  return explicit ? Number(explicit) : 0;
9685
9732
  }
9686
9733
 
9734
+ function isYandexMailSelectionQuestion(question) {
9735
+ return /^\s*\d{1,3}\.?\s*$/u.test(String(question || ""));
9736
+ }
9737
+
9738
+ function resolveYandexMailUidFromQuestion(question, previousAssistantText = "") {
9739
+ const explicitUid = extractYandexMailUid(question);
9740
+ if (explicitUid) return explicitUid;
9741
+ const ordinal = String(question || "").match(/^\s*(\d{1,3})\.?\s*$/u)?.[1];
9742
+ if (ordinal) {
9743
+ const uid = extractYandexMailUidByOrdinal(previousAssistantText, Number(ordinal));
9744
+ if (uid) return uid;
9745
+ }
9746
+ if (/(самое\s+свеж|последн|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(String(question || ""))) {
9747
+ return extractFirstYandexMailUid(previousAssistantText);
9748
+ }
9749
+ return 0;
9750
+ }
9751
+
9752
+ function extractYandexMailUidByOrdinal(text, ordinal) {
9753
+ if (!ordinal || ordinal < 1) return 0;
9754
+ const rows = String(text || "").split(/\r?\n/u);
9755
+ for (const row of rows) {
9756
+ const match = row.match(/^\s*(\d{1,3})\.\s+#(\d{3,})/u);
9757
+ if (match && Number(match[1]) === ordinal) return Number(match[2]);
9758
+ }
9759
+ return 0;
9760
+ }
9761
+
9762
+ function extractFirstYandexMailUid(text) {
9763
+ return Number(String(text || "").match(/#(\d{3,})/u)?.[1] || 0);
9764
+ }
9765
+
9687
9766
  function isYandexMailReadRequest(normalizedQuestion) {
9688
- return /(^|\s)(прочитай|прочти|открой|раскрой)(\s|$)/iu.test(normalizedQuestion)
9689
- || /(покажи\s+содерж|о чем|о чём)/iu.test(normalizedQuestion);
9767
+ return /(^|\s)(прочитай|прочти|открой|раскрой|прочитаем)(\s|$)/iu.test(normalizedQuestion)
9768
+ || /(покажи\s+содерж|о чем|о чём|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(normalizedQuestion);
9690
9769
  }
9691
9770
 
9692
9771
  async function getLatestYandexMailUid(options = {}) {
@@ -78,6 +78,12 @@ assertIncludes(cliSource, "isYandexConnectorFullyConnected", "Yandex master stat
78
78
  assertIncludes(cliSource, "--app", "Yandex token command should persist tokens by OAuth app group");
79
79
  assertIncludes(cliSource, "isYandexIdentityQuestion", "Yandex ID questions should be handled directly");
80
80
  assertIncludes(cliSource, "язндекс", "Yandex direct router should tolerate common typos");
81
+ assertIncludes(cliSource, "yandexMailCount", "Yandex mail should answer unread count questions directly");
82
+ assertIncludes(cliSource, "resolveYandexMailUidFromQuestion", "Yandex mail follow-ups should resolve selected message UID");
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");
85
+ assertIncludes(cliSource, "markSeen === false", "Yandex mail read should keep an explicit no-mark fallback");
86
+ assertIncludes(cliSource, "buildCasualDirectAnswer(question)", "Casual greetings should bypass external AI providers");
81
87
  assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
82
88
  if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
83
89
  throw new Error("package files should include the Yandex OAuth icon");