@iola_adm/iola-cli 0.2.23 → 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 +188 -25
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -527,10 +527,7 @@ const SLASH_COMMANDS = [
|
|
|
527
527
|
{ command: "/search лицей --limit 3", description: "поиск" },
|
|
528
528
|
{ command: "/mcp-info", description: "публичный MCP" },
|
|
529
529
|
{ command: "/profiles", description: "AI-профили" },
|
|
530
|
-
{ command: "/model", description: "
|
|
531
|
-
{ command: "/model codex", description: "выбрать модель Codex" },
|
|
532
|
-
{ command: "/model api", description: "выбрать API-модель" },
|
|
533
|
-
{ command: "/models openrouter --search qwen", description: "модели" },
|
|
530
|
+
{ command: "/model", description: "выбрать AI-подключение и модель" },
|
|
534
531
|
{ command: "/ai doctor", description: "AI diagnostics" },
|
|
535
532
|
{ command: "/ai setup ollama", description: "настройка Ollama" },
|
|
536
533
|
{ command: "/use codex", description: "выбрать Codex CLI" },
|
|
@@ -1609,8 +1606,7 @@ function getSlashVisibleLimit() {
|
|
|
1609
1606
|
function renderAgentInput(state) {
|
|
1610
1607
|
clearAgentInputArea(state);
|
|
1611
1608
|
const prompt = "> ";
|
|
1612
|
-
const
|
|
1613
|
-
const inputLines = [`${prompt}${lines[0] || ""}`, ...lines.slice(1)];
|
|
1609
|
+
const inputLines = buildAgentInputDisplayLines(state.buffer, prompt);
|
|
1614
1610
|
const menuLines = [];
|
|
1615
1611
|
if (state.slashOpen) {
|
|
1616
1612
|
const matches = currentSlashMatches(state);
|
|
@@ -1636,13 +1632,41 @@ function renderAgentInput(state) {
|
|
|
1636
1632
|
const renderedLines = [...menuLines, ...inputLines];
|
|
1637
1633
|
output.write(renderedLines.join("\n"));
|
|
1638
1634
|
if (output.isTTY) {
|
|
1639
|
-
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);
|
|
1640
1636
|
output.write(`\x1b[${cursorColumn + 1}G`);
|
|
1641
1637
|
}
|
|
1642
1638
|
state.renderedInputLines = inputLines.length;
|
|
1643
1639
|
state.renderedLines = renderedLines.length;
|
|
1644
1640
|
}
|
|
1645
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
|
+
|
|
1646
1670
|
function clearAgentInputArea(state = null) {
|
|
1647
1671
|
if (!output.isTTY) return;
|
|
1648
1672
|
const renderedLines = Math.max(1, Number(state?.renderedLines || state?.renderedInputLines || 1));
|
|
@@ -3971,8 +3995,8 @@ async function yandexMailList(options = {}) {
|
|
|
3971
3995
|
const search = await imapCommand(session, `UID SEARCH ${criterion}`);
|
|
3972
3996
|
const uids = parseImapSearchUids(search).slice(-Number(options.limit || 10));
|
|
3973
3997
|
if (!uids.length) return [];
|
|
3974
|
-
const fetch = await imapCommand(session, `UID FETCH ${uids.join(",")} (UID FLAGS
|
|
3975
|
-
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));
|
|
3976
4000
|
} finally {
|
|
3977
4001
|
await imapClose(session);
|
|
3978
4002
|
}
|
|
@@ -3992,7 +4016,7 @@ async function yandexMailRead(uid, options = {}) {
|
|
|
3992
4016
|
try {
|
|
3993
4017
|
await imapAuthenticate(session, email, token);
|
|
3994
4018
|
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
3995
|
-
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 });
|
|
3996
4020
|
return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
|
|
3997
4021
|
} finally {
|
|
3998
4022
|
await imapClose(session);
|
|
@@ -4145,26 +4169,93 @@ function parseImapFetchSummaries(text, options = {}) {
|
|
|
4145
4169
|
for (const chunk of chunks) {
|
|
4146
4170
|
const uid = Number(chunk.match(/UID (\d+)/iu)?.[1] || 0);
|
|
4147
4171
|
if (!uid) continue;
|
|
4148
|
-
const
|
|
4149
|
-
const
|
|
4150
|
-
const
|
|
4172
|
+
const headers = parseMailHeaders(chunk);
|
|
4173
|
+
const subject = headers.subject || "";
|
|
4174
|
+
const from = headers.from || "";
|
|
4175
|
+
const date = headers.date || "";
|
|
4151
4176
|
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 800);
|
|
4152
4177
|
rows.push({ uid, date, from, subject, snippet: body.replace(/\s+/g, " ").trim().slice(0, options.full ? 12000 : 500) });
|
|
4153
4178
|
}
|
|
4154
4179
|
return rows;
|
|
4155
4180
|
}
|
|
4156
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
|
+
|
|
4157
4195
|
function decodeMimeHeader(value) {
|
|
4158
|
-
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"));
|
|
4159
4200
|
}
|
|
4160
4201
|
|
|
4161
4202
|
function stripMailBody(value) {
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
.
|
|
4165
|
-
.replace(/^[\s\
|
|
4166
|
-
.replace(
|
|
4167
|
-
|
|
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");
|
|
4168
4259
|
}
|
|
4169
4260
|
|
|
4170
4261
|
async function yandexDavRequest(url, token, options = {}) {
|
|
@@ -9355,7 +9446,7 @@ async function aiAsk(args, context = {}) {
|
|
|
9355
9446
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
9356
9447
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
9357
9448
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
9358
|
-
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
9449
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, context.history || history);
|
|
9359
9450
|
if (yandexAnswer) {
|
|
9360
9451
|
if (historyEnabled) {
|
|
9361
9452
|
recordAskHistory({ question, answer: yandexAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
@@ -9483,10 +9574,19 @@ async function buildDirectDataAnswer(question, dataContext) {
|
|
|
9483
9574
|
].join("\n");
|
|
9484
9575
|
}
|
|
9485
9576
|
|
|
9486
|
-
async function buildYandexDirectAnswer(question) {
|
|
9577
|
+
async function buildYandexDirectAnswer(question, history = []) {
|
|
9487
9578
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
9488
|
-
|
|
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 "";
|
|
9489
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
|
+
|
|
9490
9590
|
if (/(аккаунт|профил|логин|кто подключен)/iu.test(normalized) && /(яндекс|yandex)/iu.test(normalized)) {
|
|
9491
9591
|
const profile = await getYandexIdentityProfile();
|
|
9492
9592
|
return [
|
|
@@ -9502,11 +9602,26 @@ async function buildYandexDirectAnswer(question) {
|
|
|
9502
9602
|
const result = await yandexMailStatus();
|
|
9503
9603
|
return `Яндекс Почта подключена: ${result.email}. Входящие: ${result.inbox?.exists ?? "-"}.`;
|
|
9504
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
|
+
}
|
|
9505
9620
|
const rows = /(найди|поиск)/iu.test(normalized)
|
|
9506
9621
|
? await yandexMailSearch(cleanupYandexQuery(question), { limit: 10 })
|
|
9507
9622
|
: await yandexMailList({ limit: 10, unread: /непрочитан/iu.test(normalized) });
|
|
9508
9623
|
if (!rows.length) return "Писем по запросу не найдено.";
|
|
9509
|
-
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}.
|
|
9624
|
+
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
9510
9625
|
}
|
|
9511
9626
|
|
|
9512
9627
|
if (/(календар|событи)/iu.test(normalized) && !/(создай|добавь|запланируй)/iu.test(normalized)) {
|
|
@@ -9543,6 +9658,54 @@ function cleanupYandexQuery(question) {
|
|
|
9543
9658
|
.trim();
|
|
9544
9659
|
}
|
|
9545
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
|
+
|
|
9546
9709
|
async function buildCloudDirectAnswer(question) {
|
|
9547
9710
|
if (!isCloudQuestion(question)) return "";
|
|
9548
9711
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
@@ -9939,7 +10102,7 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
9939
10102
|
if (!options.quiet) console.log(casualAnswer);
|
|
9940
10103
|
return casualAnswer;
|
|
9941
10104
|
}
|
|
9942
|
-
const yandexAnswer = await buildYandexDirectAnswer(question);
|
|
10105
|
+
const yandexAnswer = await buildYandexDirectAnswer(question, []);
|
|
9943
10106
|
if (yandexAnswer) {
|
|
9944
10107
|
if (!options.quiet) console.log(yandexAnswer);
|
|
9945
10108
|
return yandexAnswer;
|