@iola_adm/iola-cli 0.2.24 → 0.2.25
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 +1 -1
- package/src/cli.js +187 -21
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1606,8 +1606,7 @@ function getSlashVisibleLimit() {
|
|
|
1606
1606
|
function renderAgentInput(state) {
|
|
1607
1607
|
clearAgentInputArea(state);
|
|
1608
1608
|
const prompt = "> ";
|
|
1609
|
-
const
|
|
1610
|
-
const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1)];
|
|
1609
|
+
const inputLines = buildAgentInputDisplayLines(state.buffer, prompt);
|
|
1611
1610
|
const menuLines = [];
|
|
1612
1611
|
if (state.slashOpen) {
|
|
1613
1612
|
const matches = currentSlashMatches(state);
|
|
@@ -1633,13 +1632,41 @@ function renderAgentInput(state) {
|
|
|
1633
1632
|
const renderedLines = [...menuLines, ...inputLines];
|
|
1634
1633
|
output.write(renderedLines.join("\n"));
|
|
1635
1634
|
if (output.isTTY) {
|
|
1636
|
-
const cursorColumn = visibleLength(inputLines[inputLines.length - 1]);
|
|
1635
|
+
const cursorColumn = Math.min(visibleLength(inputLines[inputLines.length - 1]), Math.max(1, Number(output.columns || 100)) - 1);
|
|
1637
1636
|
output.write(`\x1b[${cursorColumn + 1}G`);
|
|
1638
1637
|
}
|
|
1639
1638
|
state.renderedInputLines = inputLines.length;
|
|
1640
1639
|
state.renderedLines = renderedLines.length;
|
|
1641
1640
|
}
|
|
1642
1641
|
|
|
1642
|
+
function buildAgentInputDisplayLines(buffer, prompt = "> ") {
|
|
1643
|
+
const columns = Math.max(20, Number(output.columns || 100));
|
|
1644
|
+
const logicalLines = String(buffer || "").split("\n");
|
|
1645
|
+
const result = [];
|
|
1646
|
+
for (let index = 0; index < logicalLines.length; index += 1) {
|
|
1647
|
+
const prefix = index === 0 ? prompt : "";
|
|
1648
|
+
const width = Math.max(1, columns - visibleLength(prefix));
|
|
1649
|
+
const chunks = wrapTerminalText(logicalLines[index] || "", width);
|
|
1650
|
+
if (chunks.length === 0) {
|
|
1651
|
+
result.push(prefix);
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
result.push(`${prefix}${chunks[0]}`);
|
|
1655
|
+
for (const chunk of chunks.slice(1)) result.push(chunk);
|
|
1656
|
+
}
|
|
1657
|
+
return result.length ? result : [prompt];
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function wrapTerminalText(value, width) {
|
|
1661
|
+
const chars = [...String(value || "")];
|
|
1662
|
+
if (!chars.length) return [];
|
|
1663
|
+
const rows = [];
|
|
1664
|
+
for (let index = 0; index < chars.length; index += width) {
|
|
1665
|
+
rows.push(chars.slice(index, index + width).join(""));
|
|
1666
|
+
}
|
|
1667
|
+
return rows;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1643
1670
|
function clearAgentInputArea(state = null) {
|
|
1644
1671
|
if (!output.isTTY) return;
|
|
1645
1672
|
const renderedLines = Math.max(1, Number(state?.renderedLines || state?.renderedInputLines || 1));
|
|
@@ -3968,8 +3995,8 @@ async function yandexMailList(options = {}) {
|
|
|
3968
3995
|
const search = await imapCommand(session, `UID SEARCH ${criterion}`);
|
|
3969
3996
|
const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
|
|
3970
3997
|
if (!uids.length) return [];
|
|
3971
|
-
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS
|
|
3972
|
-
return parseImapFetchSummaries(fetch);
|
|
3998
|
+
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] BODY.PEEK[TEXT]<0.800>)`, { timeout: 45000 });
|
|
3999
|
+
return parseImapFetchSummaries(fetch).sort((left, right) => Number(right.uid || 0) - Number(left.uid || 0));
|
|
3973
4000
|
} finally {
|
|
3974
4001
|
await imapClose(session);
|
|
3975
4002
|
}
|
|
@@ -3989,7 +4016,7 @@ async function yandexMailRead(uid, options = {}) {
|
|
|
3989
4016
|
try {
|
|
3990
4017
|
await imapAuthenticate(session, email, token);
|
|
3991
4018
|
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
3992
|
-
const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS
|
|
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 });
|
|
3993
4020
|
return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
|
|
3994
4021
|
} finally {
|
|
3995
4022
|
await imapClose(session);
|
|
@@ -4142,26 +4169,93 @@ function parseImapFetchSummaries(text, options = {}) {
|
|
|
4142
4169
|
for (const chunk of chunks) {
|
|
4143
4170
|
const uid = Number(chunk.match(/UID (\d+)/iu)?.[1] || 0);
|
|
4144
4171
|
if (!uid) continue;
|
|
4145
|
-
const
|
|
4146
|
-
const
|
|
4147
|
-
const
|
|
4172
|
+
const headers = parseMailHeaders(chunk);
|
|
4173
|
+
const subject = headers.subject || "";
|
|
4174
|
+
const from = headers.from || "";
|
|
4175
|
+
const date = headers.date || "";
|
|
4148
4176
|
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 800);
|
|
4149
4177
|
rows.push({ uid, date, from, subject, snippet: body.replace(/\s+/g, " ").trim().slice(0, options.full ? 12000 : 500) });
|
|
4150
4178
|
}
|
|
4151
4179
|
return rows;
|
|
4152
4180
|
}
|
|
4153
4181
|
|
|
4182
|
+
function parseMailHeaders(value) {
|
|
4183
|
+
const normalized = String(value || "").replace(/\r/g, "").replace(/\n BODY\[[\s\S]*$/u, "");
|
|
4184
|
+
const headerValue = (name) => {
|
|
4185
|
+
const match = normalized.match(new RegExp(`^${name}:\\s*([^\\n]*(?:\\n[\\t ][^\\n]*)*)`, "imu"));
|
|
4186
|
+
return match ? decodeMimeHeader(match[1].replace(/\n[\t ]+/g, " ").trim()) : "";
|
|
4187
|
+
};
|
|
4188
|
+
return {
|
|
4189
|
+
date: headerValue("Date"),
|
|
4190
|
+
from: headerValue("From"),
|
|
4191
|
+
subject: headerValue("Subject"),
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4154
4195
|
function decodeMimeHeader(value) {
|
|
4155
|
-
return String(value || "")
|
|
4196
|
+
return String(value || "")
|
|
4197
|
+
.replace(/\?=\s+=\?/gu, "?==?")
|
|
4198
|
+
.replace(/=\?UTF-8\?B\?([^?]+)\?=/giu, (_, data) => Buffer.from(data, "base64").toString("utf8"))
|
|
4199
|
+
.replace(/=\?UTF-8\?Q\?([^?]+)\?=/giu, (_, data) => Buffer.from(data.replace(/_/g, " ").replace(/=([A-F0-9]{2})/giu, (_, hex) => String.fromCharCode(parseInt(hex, 16))), "binary").toString("utf8"));
|
|
4156
4200
|
}
|
|
4157
4201
|
|
|
4158
4202
|
function stripMailBody(value) {
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
.
|
|
4162
|
-
.replace(/^[\s\
|
|
4163
|
-
.replace(
|
|
4164
|
-
|
|
4203
|
+
const raw = String(value || "").replace(/\r/g, "");
|
|
4204
|
+
const withoutFetch = raw
|
|
4205
|
+
.replace(/^\* \d+ FETCH[^\n]*\n?/u, "")
|
|
4206
|
+
.replace(/^BODY\[[^\n]*\]\s*\{\d+\}\n?/imu, "")
|
|
4207
|
+
.replace(/\n\)\s*$/u, "");
|
|
4208
|
+
const bodyMatch = withoutFetch.match(/BODY\[TEXT\]\s*\{\d+\}\s*([\s\S]*)/iu);
|
|
4209
|
+
if (bodyMatch?.[1]) return extractMimeText(bodyMatch[1]);
|
|
4210
|
+
const headerEnd = withoutFetch.indexOf("\n\n");
|
|
4211
|
+
if (headerEnd < 0) return withoutFetch.replace(/[^\S\n]+/g, " ").trim();
|
|
4212
|
+
const headers = withoutFetch.slice(0, headerEnd);
|
|
4213
|
+
const body = withoutFetch.slice(headerEnd + 2).replace(/^BODY\[[^\n]*\]\s*\{\d+\}\n?/imu, "");
|
|
4214
|
+
return extractMimeText(`${headers}\n\n${body}`);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
function extractMimeText(source, depth = 0) {
|
|
4218
|
+
if (depth > 5) return decodeMailPart(source);
|
|
4219
|
+
const boundary = String(source || "").match(/boundary="?([^"\n;]+)"?/iu)?.[1];
|
|
4220
|
+
if (!boundary || !source.includes(`--${boundary}`)) return decodeMailPart(source);
|
|
4221
|
+
const parts = source.slice(source.indexOf(`--${boundary}`)).split(`--${boundary}`).filter((part) => part.trim() && part.trim() !== "--");
|
|
4222
|
+
const plain = parts.find((part) => /^Content-Type:\s*text\/plain/im.test(part));
|
|
4223
|
+
if (plain) return decodeMailPart(plain);
|
|
4224
|
+
for (const part of parts) {
|
|
4225
|
+
if (/^Content-Type:\s*multipart\//im.test(part) || /boundary="?([^"\n;]+)"?/iu.test(part)) {
|
|
4226
|
+
const nested = extractMimeText(part, depth + 1);
|
|
4227
|
+
if (nested) return nested;
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
const html = parts.find((part) => /^Content-Type:\s*text\/html/im.test(part));
|
|
4231
|
+
if (html) return decodeMailPart(html);
|
|
4232
|
+
return decodeMailPart(parts[0] || source);
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
function decodeMailPart(part) {
|
|
4236
|
+
const text = String(part || "").replace(/\r/g, "");
|
|
4237
|
+
let headerEnd = text.indexOf("\n\n");
|
|
4238
|
+
let headers = headerEnd >= 0 ? text.slice(0, headerEnd) : "";
|
|
4239
|
+
let body = headerEnd >= 0 ? text.slice(headerEnd + 2) : text;
|
|
4240
|
+
if (headerEnd < 0) {
|
|
4241
|
+
const transferMatch = text.match(/^(.*Content-Transfer-Encoding:\s*(?:base64|quoted-printable)[^\n]*\n)([\s\S]*)$/imu);
|
|
4242
|
+
if (transferMatch) {
|
|
4243
|
+
headers = transferMatch[1];
|
|
4244
|
+
body = transferMatch[2];
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
const encoding = headers.match(/^Content-Transfer-Encoding:\s*([^\n]+)/imu)?.[1]?.trim().toLocaleLowerCase("en-US") || "";
|
|
4248
|
+
let decoded = body;
|
|
4249
|
+
if (encoding === "base64") {
|
|
4250
|
+
decoded = Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
4251
|
+
} else if (encoding === "quoted-printable") {
|
|
4252
|
+
decoded = decodeQuotedPrintable(body);
|
|
4253
|
+
}
|
|
4254
|
+
return decoded.replace(/<[^>]+>/g, " ").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
function decodeQuotedPrintable(value) {
|
|
4258
|
+
return Buffer.from(String(value || "").replace(/=\n/gu, "").replace(/=([A-F0-9]{2})/giu, (_, hex) => String.fromCharCode(parseInt(hex, 16))), "binary").toString("utf8");
|
|
4165
4259
|
}
|
|
4166
4260
|
|
|
4167
4261
|
async function yandexDavRequest(url, token, options = {}) {
|
|
@@ -9352,7 +9446,7 @@ async function aiAsk(args, context = {}) {
|
|
|
9352
9446
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
9353
9447
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
9354
9448
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
9355
|
-
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
9449
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
9356
9450
|
if (yandexAnswer) {
|
|
9357
9451
|
if (historyEnabled) {
|
|
9358
9452
|
recordAskHistory({ question, answer: yandexAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
@@ -9480,10 +9574,19 @@ async function buildDirectDataAnswer(question, dataContext) {
|
|
|
9480
9574
|
].join("\n");
|
|
9481
9575
|
}
|
|
9482
9576
|
|
|
9483
|
-
async function buildYandexDirectAnswer(question) {
|
|
9577
|
+
async function buildYandexDirectAnswer(question, history = []) {
|
|
9484
9578
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
9485
|
-
|
|
9579
|
+
const previousAssistantText = [...(history || [])].reverse().find((item) => item.role === "assistant")?.content || "";
|
|
9580
|
+
const mailContext = /Яндекс Почта|Письмо #|\bUID\b|#\d{3,}/iu.test(previousAssistantText);
|
|
9581
|
+
if (!/(яндекс|yandex|почт|письм|календар|контакт|телемост)/iu.test(normalized) && !(/^\s*\d{3,}\s*$/u.test(question) && mailContext)) return "";
|
|
9486
9582
|
try {
|
|
9583
|
+
if (/^\s*\d{3,}\s*$/u.test(question) && mailContext) {
|
|
9584
|
+
const uid = extractYandexMailUid(question);
|
|
9585
|
+
const row = await yandexMailRead(uid);
|
|
9586
|
+
if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
|
|
9587
|
+
return formatYandexMailRead(row);
|
|
9588
|
+
}
|
|
9589
|
+
|
|
9487
9590
|
if (/(аккаунт|профил|логин|кто подключен)/iu.test(normalized) && /(яндекс|yandex)/iu.test(normalized)) {
|
|
9488
9591
|
const profile = await getYandexIdentityProfile();
|
|
9489
9592
|
return [
|
|
@@ -9499,11 +9602,26 @@ async function buildYandexDirectAnswer(question) {
|
|
|
9499
9602
|
const result = await yandexMailStatus();
|
|
9500
9603
|
return `Яндекс Почта подключена: ${result.email}. Входящие: ${result.inbox?.exists ?? "-"}.`;
|
|
9501
9604
|
}
|
|
9605
|
+
if (/(отправ|напиши|пошли)/iu.test(normalized)) {
|
|
9606
|
+
const draft = parseYandexMailSendRequest(question);
|
|
9607
|
+
if (!draft.to.length || !draft.text) {
|
|
9608
|
+
return "Для отправки письма укажите получателя и текст. Пример: отправь письмо user@example.com тема: Привет текст: Проверка.";
|
|
9609
|
+
}
|
|
9610
|
+
const result = await yandexMailSend({ ...draft, confirm: true });
|
|
9611
|
+
return `Письмо отправлено: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
9612
|
+
}
|
|
9613
|
+
if (isYandexMailReadRequest(normalized)) {
|
|
9614
|
+
const uid = extractYandexMailUid(question) || await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
|
|
9615
|
+
if (!uid) return "Писем для чтения не найдено.";
|
|
9616
|
+
const row = await yandexMailRead(uid);
|
|
9617
|
+
if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
|
|
9618
|
+
return formatYandexMailRead(row);
|
|
9619
|
+
}
|
|
9502
9620
|
const rows = /(найди|поиск)/iu.test(normalized)
|
|
9503
9621
|
? await yandexMailSearch(cleanupYandexQuery(question), { limit: 10 })
|
|
9504
9622
|
: await yandexMailList({ limit: 10, unread: /непрочитан/iu.test(normalized) });
|
|
9505
9623
|
if (!rows.length) return "Писем по запросу не найдено.";
|
|
9506
|
-
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}.
|
|
9624
|
+
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
9507
9625
|
}
|
|
9508
9626
|
|
|
9509
9627
|
if (/(календар|событи)/iu.test(normalized) && !/(создай|добавь|запланируй)/iu.test(normalized)) {
|
|
@@ -9540,6 +9658,54 @@ function cleanupYandexQuery(question) {
|
|
|
9540
9658
|
.trim();
|
|
9541
9659
|
}
|
|
9542
9660
|
|
|
9661
|
+
function extractYandexMailUid(question) {
|
|
9662
|
+
const text = String(question || "");
|
|
9663
|
+
const explicit = text.match(/(?:#|uid\s*|письм[оа]?\s*)?(\d{3,})/iu)?.[1];
|
|
9664
|
+
return explicit ? Number(explicit) : 0;
|
|
9665
|
+
}
|
|
9666
|
+
|
|
9667
|
+
function isYandexMailReadRequest(normalizedQuestion) {
|
|
9668
|
+
return /(^|\s)(прочитай|прочти|открой|раскрой)(\s|$)/iu.test(normalizedQuestion)
|
|
9669
|
+
|| /(покажи\s+содерж|о чем|о чём)/iu.test(normalizedQuestion);
|
|
9670
|
+
}
|
|
9671
|
+
|
|
9672
|
+
async function getLatestYandexMailUid(options = {}) {
|
|
9673
|
+
const rows = await yandexMailList({ limit: 1, unread: Boolean(options.unread) });
|
|
9674
|
+
return rows[0]?.uid || 0;
|
|
9675
|
+
}
|
|
9676
|
+
|
|
9677
|
+
function parseYandexMailSendRequest(question) {
|
|
9678
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
9679
|
+
const emails = [...text.matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu)].map((match) => match[0]);
|
|
9680
|
+
const subjectMatch = text.match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|body|сообщение)\s*:|$)/iu);
|
|
9681
|
+
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu);
|
|
9682
|
+
const withoutCommand = text
|
|
9683
|
+
.replace(/^(?:отправь|отправить|напиши|пошли)\s+(?:письмо\s+)?/iu, "")
|
|
9684
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")
|
|
9685
|
+
.trim();
|
|
9686
|
+
return {
|
|
9687
|
+
to: emails,
|
|
9688
|
+
subject: (subjectMatch?.[1] || "Сообщение от IOLA CLI").trim(),
|
|
9689
|
+
text: (bodyMatch?.[1] || (!subjectMatch ? withoutCommand : "")).trim(),
|
|
9690
|
+
};
|
|
9691
|
+
}
|
|
9692
|
+
|
|
9693
|
+
function formatYandexMailSummary(row) {
|
|
9694
|
+
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
9695
|
+
}
|
|
9696
|
+
|
|
9697
|
+
function formatYandexMailRead(row) {
|
|
9698
|
+
const body = String(row.snippet || "").trim();
|
|
9699
|
+
return [
|
|
9700
|
+
`Письмо #${row.uid}`,
|
|
9701
|
+
`От: ${row.from || "-"}`,
|
|
9702
|
+
`Тема: ${row.subject || "(без темы)"}`,
|
|
9703
|
+
row.date ? `Дата: ${row.date}` : "",
|
|
9704
|
+
"",
|
|
9705
|
+
body ? `Текст: ${body.slice(0, 2000)}` : "Текст письма пустой или не распознан.",
|
|
9706
|
+
].filter((line) => line !== "").join("\n");
|
|
9707
|
+
}
|
|
9708
|
+
|
|
9543
9709
|
async function buildCloudDirectAnswer(question) {
|
|
9544
9710
|
if (!isCloudQuestion(question)) return "";
|
|
9545
9711
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
@@ -9936,7 +10102,7 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
9936
10102
|
if (!options.quiet) console.log(casualAnswer);
|
|
9937
10103
|
return casualAnswer;
|
|
9938
10104
|
}
|
|
9939
|
-
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
10105
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, []);
|
|
9940
10106
|
if (yandexAnswer) {
|
|
9941
10107
|
if (!options.quiet) console.log(yandexAnswer);
|
|
9942
10108
|
return yandexAnswer;
|