@iletai/nzb 1.4.6 → 1.4.8
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/dist/copilot/orchestrator.js +6 -12
- package/dist/store/db.js +8 -0
- package/dist/telegram/bot.js +35 -7
- package/dist/telegram/handlers/inline.js +83 -0
- package/dist/telegram/handlers/reactions.js +97 -0
- package/dist/telegram/handlers/suggestions.js +156 -0
- package/package.json +1 -1
- package/skills/find-skills/SKILL.md +5 -4
|
@@ -362,7 +362,8 @@ function isRecoverableError(err) {
|
|
|
362
362
|
const msg = err instanceof Error ? err.message : String(err);
|
|
363
363
|
return /timeout|disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
|
|
364
364
|
}
|
|
365
|
-
|
|
365
|
+
const MAX_AUTO_CONTINUE = 3;
|
|
366
|
+
export async function sendToOrchestrator(prompt, source, callback, onToolEvent, onUsage, _autoContinueCount = 0) {
|
|
366
367
|
const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
|
|
367
368
|
logMessage("in", sourceLabel, prompt);
|
|
368
369
|
// Tag the prompt with its source channel
|
|
@@ -434,18 +435,11 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
434
435
|
await callback(finalContent, true, { assistantLogId });
|
|
435
436
|
// Auto-continue: if the response was cut short by timeout, automatically
|
|
436
437
|
// send a follow-up "Continue" message so the user doesn't have to
|
|
437
|
-
if (finalContent.includes("⏱ Response was cut short (timeout)")
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (source.type === "telegram") {
|
|
441
|
-
try {
|
|
442
|
-
const { sendProactiveMessage } = await import("../telegram/bot.js");
|
|
443
|
-
await sendProactiveMessage("🔄 Auto-continuing...");
|
|
444
|
-
}
|
|
445
|
-
catch { }
|
|
446
|
-
}
|
|
438
|
+
if (finalContent.includes("⏱ Response was cut short (timeout)") &&
|
|
439
|
+
_autoContinueCount < MAX_AUTO_CONTINUE) {
|
|
440
|
+
console.log(`[nzb] Auto-continuing after timeout (${_autoContinueCount + 1}/${MAX_AUTO_CONTINUE})…`);
|
|
447
441
|
await sleep(1000);
|
|
448
|
-
void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage);
|
|
442
|
+
void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage, _autoContinueCount + 1);
|
|
449
443
|
}
|
|
450
444
|
return;
|
|
451
445
|
}
|
package/dist/store/db.js
CHANGED
|
@@ -164,6 +164,14 @@ export function setConversationTelegramMsgId(rowId, telegramMsgId) {
|
|
|
164
164
|
const db = getDb();
|
|
165
165
|
db.prepare(`UPDATE conversation_log SET telegram_msg_id = ? WHERE id = ?`).run(telegramMsgId, rowId);
|
|
166
166
|
}
|
|
167
|
+
/** Look up conversation content by Telegram message ID. Returns the message content or undefined. */
|
|
168
|
+
export function getConversationByTelegramMsgId(telegramMsgId) {
|
|
169
|
+
const db = getDb();
|
|
170
|
+
const row = db
|
|
171
|
+
.prepare(`SELECT content FROM conversation_log WHERE telegram_msg_id = ? LIMIT 1`)
|
|
172
|
+
.get(telegramMsgId);
|
|
173
|
+
return row?.content;
|
|
174
|
+
}
|
|
167
175
|
/** Get recent conversation history formatted for injection into system message. */
|
|
168
176
|
export function getRecentConversation(limit = 20) {
|
|
169
177
|
const db = getDb();
|
package/dist/telegram/bot.js
CHANGED
|
@@ -10,7 +10,10 @@ import { searchMemories } from "../store/db.js";
|
|
|
10
10
|
import { chunkMessage, escapeHtml, formatToolSummaryExpandable, toTelegramHTML } from "./formatter.js";
|
|
11
11
|
import { registerCallbackHandlers } from "./handlers/callbacks.js";
|
|
12
12
|
import { sendFormattedReply } from "./handlers/helpers.js";
|
|
13
|
+
import { registerInlineQueryHandler } from "./handlers/inline.js";
|
|
13
14
|
import { registerMediaHandlers } from "./handlers/media.js";
|
|
15
|
+
import { getReactionHelpText, registerReactionHandlers } from "./handlers/reactions.js";
|
|
16
|
+
import { createSmartSuggestionsWithContext, registerSmartSuggestionHandlers } from "./handlers/suggestions.js";
|
|
14
17
|
import { initLogChannel, logDebug, logError, logInfo } from "./log-channel.js";
|
|
15
18
|
let bot;
|
|
16
19
|
const startedAt = Date.now();
|
|
@@ -160,6 +163,12 @@ export function createBot() {
|
|
|
160
163
|
bot.use(mainMenu);
|
|
161
164
|
// Register callback + media handlers from extracted modules
|
|
162
165
|
registerCallbackHandlers(bot);
|
|
166
|
+
// 🚀 Breakthrough: Inline Query Mode — @bot in any chat
|
|
167
|
+
registerInlineQueryHandler(bot);
|
|
168
|
+
// 🚀 Breakthrough: Smart Suggestion button callbacks
|
|
169
|
+
registerSmartSuggestionHandlers(bot);
|
|
170
|
+
// 🚀 Breakthrough: Reaction-based AI actions
|
|
171
|
+
registerReactionHandlers(bot);
|
|
163
172
|
// Persistent reply keyboard — quick actions always visible below chat input
|
|
164
173
|
const replyKeyboard = new Keyboard()
|
|
165
174
|
.text("📊 Status")
|
|
@@ -186,7 +195,12 @@ export function createBot() {
|
|
|
186
195
|
"/status — Show system status\n" +
|
|
187
196
|
"/settings — Bot settings\n" +
|
|
188
197
|
"/restart — Restart NZB\n" +
|
|
189
|
-
"/help — Show this help"
|
|
198
|
+
"/help — Show this help\n\n" +
|
|
199
|
+
"⚡ Breakthrough Features:\n" +
|
|
200
|
+
"• @bot query — Use me inline in any chat!\n" +
|
|
201
|
+
"• React to any message to trigger AI:\n" +
|
|
202
|
+
getReactionHelpText() + "\n" +
|
|
203
|
+
"• Smart suggestions appear after each response", { reply_markup: mainMenu }));
|
|
190
204
|
bot.command("cancel", async (ctx) => {
|
|
191
205
|
const cancelled = await cancelCurrentMessage();
|
|
192
206
|
await ctx.reply(cancelled ? "Cancelled." : "Nothing to cancel.");
|
|
@@ -526,10 +540,15 @@ export function createBot() {
|
|
|
526
540
|
}
|
|
527
541
|
const chunks = chunkMessage(fullFormatted);
|
|
528
542
|
const fallbackChunks = chunkMessage(textWithMeta);
|
|
543
|
+
// 🚀 Breakthrough: Build smart suggestion buttons based on response content
|
|
544
|
+
const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
|
|
529
545
|
// Single chunk: edit placeholder in place
|
|
530
546
|
if (placeholderMsgId && chunks.length === 1) {
|
|
531
547
|
try {
|
|
532
|
-
await bot.api.editMessageText(chatId, placeholderMsgId, chunks[0], {
|
|
548
|
+
await bot.api.editMessageText(chatId, placeholderMsgId, chunks[0], {
|
|
549
|
+
parse_mode: "HTML",
|
|
550
|
+
reply_markup: smartKb,
|
|
551
|
+
});
|
|
533
552
|
try {
|
|
534
553
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
535
554
|
}
|
|
@@ -545,7 +564,9 @@ export function createBot() {
|
|
|
545
564
|
}
|
|
546
565
|
catch {
|
|
547
566
|
try {
|
|
548
|
-
await bot.api.editMessageText(chatId, placeholderMsgId, fallbackChunks[0]
|
|
567
|
+
await bot.api.editMessageText(chatId, placeholderMsgId, fallbackChunks[0], {
|
|
568
|
+
reply_markup: smartKb,
|
|
569
|
+
});
|
|
549
570
|
try {
|
|
550
571
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
551
572
|
}
|
|
@@ -569,14 +590,21 @@ export function createBot() {
|
|
|
569
590
|
let firstSentMsgId;
|
|
570
591
|
const sendChunk = async (chunk, fallback, index) => {
|
|
571
592
|
const isFirst = index === 0 && !placeholderMsgId;
|
|
593
|
+
const isLast = index === totalChunks - 1;
|
|
572
594
|
// Pagination header for multi-chunk messages
|
|
573
595
|
const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
|
|
574
|
-
const opts =
|
|
575
|
-
|
|
576
|
-
|
|
596
|
+
const opts = {
|
|
597
|
+
parse_mode: "HTML",
|
|
598
|
+
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
599
|
+
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
600
|
+
};
|
|
601
|
+
const fallbackOpts = {
|
|
602
|
+
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
603
|
+
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
604
|
+
};
|
|
577
605
|
const sent = await ctx
|
|
578
606
|
.reply(pageTag + chunk, opts)
|
|
579
|
-
.catch(() => ctx.reply(pageTag + fallback,
|
|
607
|
+
.catch(() => ctx.reply(pageTag + fallback, fallbackOpts));
|
|
580
608
|
if (index === 0 && sent)
|
|
581
609
|
firstSentMsgId = sent.message_id;
|
|
582
610
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { InlineKeyboard, InlineQueryResultBuilder } from "grammy";
|
|
2
|
+
import { sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
3
|
+
import { escapeHtml } from "../formatter.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register inline query handler — allows users to @mention the bot in ANY chat
|
|
6
|
+
* and get AI-generated responses as inline results.
|
|
7
|
+
*
|
|
8
|
+
* Uses grammY's InlineQueryResultBuilder for type-safe result construction.
|
|
9
|
+
*/
|
|
10
|
+
export function registerInlineQueryHandler(bot) {
|
|
11
|
+
bot.on("inline_query", async (ctx) => {
|
|
12
|
+
const query = ctx.inlineQuery.query.trim();
|
|
13
|
+
// Empty query → show usage hint
|
|
14
|
+
if (!query) {
|
|
15
|
+
const helpResult = InlineQueryResultBuilder.article("help", "💡 Type your question…", {
|
|
16
|
+
description: "Ask me anything — I'll respond with AI-powered answers",
|
|
17
|
+
}).text("💡 <b>NZB AI Assistant</b>\n\nType <code>@bot your question</code> in any chat to get instant AI answers.", { parse_mode: "HTML" });
|
|
18
|
+
await ctx.answerInlineQuery([helpResult], { cache_time: 10 });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Short query → wait for more input
|
|
22
|
+
if (query.length < 3) {
|
|
23
|
+
await ctx.answerInlineQuery([], { cache_time: 5 });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Generate AI response for the inline query — non-blocking with timeout
|
|
27
|
+
const responsePromise = new Promise((resolve) => {
|
|
28
|
+
const timeout = setTimeout(() => resolve(""), 8000);
|
|
29
|
+
sendToOrchestrator(`[inline query — respond concisely in 2-3 sentences max] ${query}`, { type: "background" }, (text, done) => {
|
|
30
|
+
if (done) {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
resolve(text);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const aiResponse = await responsePromise;
|
|
37
|
+
const results = [];
|
|
38
|
+
// AI answer (if we got one in time)
|
|
39
|
+
if (aiResponse && !aiResponse.startsWith("Error:")) {
|
|
40
|
+
const preview = aiResponse.length > 200 ? aiResponse.slice(0, 200) + "…" : aiResponse;
|
|
41
|
+
const fullText = aiResponse.length > 4000 ? aiResponse.slice(0, 4000) + "\n\n⋯ (truncated)" : aiResponse;
|
|
42
|
+
const moreDetailKb = new InlineKeyboard().text("🔄 More detail", `inline_detail:${query.slice(0, 50)}`);
|
|
43
|
+
results.push(InlineQueryResultBuilder.article(`ai-${Date.now()}`, "🤖 AI Answer", {
|
|
44
|
+
description: preview.replace(/\n/g, " "),
|
|
45
|
+
reply_markup: moreDetailKb,
|
|
46
|
+
}).text(`🤖 <b>NZB AI:</b>\n\n${escapeHtml(fullText)}`, { parse_mode: "HTML" }));
|
|
47
|
+
}
|
|
48
|
+
// Quick action templates — always available
|
|
49
|
+
results.push(InlineQueryResultBuilder.article(`ask-${Date.now()}`, `❓ Ask NZB: "${query.slice(0, 50)}"`, {
|
|
50
|
+
description: "Send this question to NZB for a detailed answer",
|
|
51
|
+
}).text(`❓ <b>Question for NZB:</b>\n\n${escapeHtml(query)}`, { parse_mode: "HTML" }), InlineQueryResultBuilder.article(`code-${Date.now()}`, `💻 Code: "${query.slice(0, 50)}"`, {
|
|
52
|
+
description: "Generate code for this request",
|
|
53
|
+
}).text(`💻 <b>Code Request:</b>\n\n<code>${escapeHtml(query)}</code>`, { parse_mode: "HTML" }), InlineQueryResultBuilder.article(`explain-${Date.now()}`, `📖 Explain: "${query.slice(0, 50)}"`, {
|
|
54
|
+
description: "Get a detailed explanation",
|
|
55
|
+
}).text(`📖 <b>Explanation Request:</b>\n\n${escapeHtml(query)}`, { parse_mode: "HTML" }));
|
|
56
|
+
await ctx.answerInlineQuery(results.slice(0, 50), {
|
|
57
|
+
cache_time: 30,
|
|
58
|
+
is_personal: true,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// Handle "More detail" button from inline results
|
|
62
|
+
bot.callbackQuery(/^inline_detail:(.+)$/, async (ctx) => {
|
|
63
|
+
const query = ctx.match[1];
|
|
64
|
+
await ctx.answerCallbackQuery({ text: "Generating detailed response…" });
|
|
65
|
+
sendToOrchestrator(`[Give a comprehensive, detailed answer] ${query}`, { type: "background" }, async (text, done) => {
|
|
66
|
+
if (done && !text.startsWith("Error:")) {
|
|
67
|
+
try {
|
|
68
|
+
const chatId = ctx.chat?.id;
|
|
69
|
+
if (chatId) {
|
|
70
|
+
const truncated = text.length > 4000 ? text.slice(0, 4000) + "\n\n⋯" : text;
|
|
71
|
+
await bot.api.sendMessage(chatId, `🔍 <b>Detailed Answer:</b>\n\n${escapeHtml(truncated)}`, {
|
|
72
|
+
parse_mode: "HTML",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Can't send to inline result chats we're not in
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=inline.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
2
|
+
import { logDebug, logInfo } from "../log-channel.js";
|
|
3
|
+
import { sendFormattedReply } from "./helpers.js";
|
|
4
|
+
const REACTION_ACTIONS = [
|
|
5
|
+
{
|
|
6
|
+
emoji: "🤔",
|
|
7
|
+
prompt: (text) => `Explain and analyze the following message in detail. Be thorough but concise:\n\n"${text}"`,
|
|
8
|
+
label: "Explaining",
|
|
9
|
+
ackEmoji: "👀",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
emoji: "✍",
|
|
13
|
+
prompt: (text) => `Rewrite and improve the following text for clarity, grammar, and impact. Provide the improved version only:\n\n"${text}"`,
|
|
14
|
+
label: "Rewriting",
|
|
15
|
+
ackEmoji: "✍",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
emoji: "👀",
|
|
19
|
+
prompt: (text) => `Translate the following text. If it's in English, translate to Vietnamese. If it's in any other language, translate to English. Provide only the translation:\n\n"${text}"`,
|
|
20
|
+
label: "Translating",
|
|
21
|
+
ackEmoji: "👀",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
emoji: "🤓",
|
|
25
|
+
prompt: (text) => `Summarize the following message in 2-3 concise bullet points:\n\n"${text}"`,
|
|
26
|
+
label: "Summarizing",
|
|
27
|
+
ackEmoji: "🤓",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
emoji: "👨💻",
|
|
31
|
+
prompt: (text) => `Analyze this code for bugs, issues, and potential improvements. Be specific about what's wrong and how to fix it:\n\n\`\`\`\n${text}\n\`\`\``,
|
|
32
|
+
label: "Debugging",
|
|
33
|
+
ackEmoji: "👨💻",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
emoji: "🤩",
|
|
37
|
+
prompt: (text) => `Suggest improvements and alternatives for the following. Be creative and practical:\n\n"${text}"`,
|
|
38
|
+
label: "Suggesting",
|
|
39
|
+
ackEmoji: "🤩",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
emoji: "⚡",
|
|
43
|
+
prompt: (text) => `Extract the key points and action items from the following message. Format as a numbered list:\n\n"${text}"`,
|
|
44
|
+
label: "Extracting",
|
|
45
|
+
ackEmoji: "⚡",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
// Build a lookup map for O(1) access
|
|
49
|
+
const REACTION_MAP = new Map(REACTION_ACTIONS.map((a) => [a.emoji, a]));
|
|
50
|
+
export function registerReactionHandlers(bot) {
|
|
51
|
+
// Use grammY's ctx.reactions() for cleaner reaction diff handling
|
|
52
|
+
bot.on("message_reaction", async (ctx) => {
|
|
53
|
+
const { emojiAdded } = ctx.reactions();
|
|
54
|
+
if (emojiAdded.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
// Find the first emoji that matches our action map
|
|
57
|
+
const matchedEmoji = emojiAdded.find((e) => REACTION_MAP.has(e));
|
|
58
|
+
if (!matchedEmoji)
|
|
59
|
+
return;
|
|
60
|
+
const action = REACTION_MAP.get(matchedEmoji);
|
|
61
|
+
const chatId = ctx.messageReaction.chat.id;
|
|
62
|
+
const messageId = ctx.messageReaction.message_id;
|
|
63
|
+
void logInfo(`⚡ Reaction action: ${action.emoji} ${action.label} on message ${messageId}`);
|
|
64
|
+
try {
|
|
65
|
+
await bot.api.sendChatAction(chatId, "typing");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* best-effort */
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const { getConversationByTelegramMsgId } = await import("../../store/db.js");
|
|
72
|
+
const msgContent = getConversationByTelegramMsgId(messageId);
|
|
73
|
+
if (msgContent) {
|
|
74
|
+
void logDebug(`📋 Found message content for reaction: ${msgContent.slice(0, 80)}`);
|
|
75
|
+
const prompt = action.prompt(msgContent);
|
|
76
|
+
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId }, (text, done) => {
|
|
77
|
+
if (done) {
|
|
78
|
+
void sendFormattedReply(bot, chatId, `${action.emoji} <b>${action.label}:</b>\n\n${text}`, {
|
|
79
|
+
replyTo: messageId,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await bot.api.sendMessage(chatId, `${action.emoji} I can see your reaction, but I need the message text. Reply to the message and I'll ${action.label.toLowerCase()} it for you.`, { reply_parameters: { message_id: messageId } });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
void logDebug(`Reaction handler error: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Get the list of supported reaction actions for help text. */
|
|
94
|
+
export function getReactionHelpText() {
|
|
95
|
+
return REACTION_ACTIONS.map((a) => `${a.emoji} — ${a.label}`).join("\n");
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=reactions.js.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
3
|
+
import { sendFormattedReply } from "./helpers.js";
|
|
4
|
+
const SUGGESTION_RULES = [
|
|
5
|
+
{
|
|
6
|
+
test: (resp) => /```[\s\S]*```/.test(resp),
|
|
7
|
+
label: "🧪 Test this code",
|
|
8
|
+
callbackPrefix: "smart_test",
|
|
9
|
+
promptTemplate: (_p, resp) => `Write comprehensive unit tests for the code you just provided. Include edge cases:\n\n${resp.slice(0, 2000)}`,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
test: (resp) => /```[\s\S]*```/.test(resp),
|
|
13
|
+
label: "📖 Explain code",
|
|
14
|
+
callbackPrefix: "smart_explain_code",
|
|
15
|
+
promptTemplate: (_p, resp) => `Explain the code you just provided step by step, as if teaching a junior developer:\n\n${resp.slice(0, 2000)}`,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
test: (resp) => /```[\s\S]*```/.test(resp),
|
|
19
|
+
label: "⚡ Optimize",
|
|
20
|
+
callbackPrefix: "smart_optimize",
|
|
21
|
+
promptTemplate: (_p, resp) => `Analyze the code you just provided for performance improvements and optimizations. Show the optimized version:\n\n${resp.slice(0, 2000)}`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
test: (resp) => /error|bug|issue|problem|fix|wrong/i.test(resp),
|
|
25
|
+
label: "🔧 Fix it",
|
|
26
|
+
callbackPrefix: "smart_fix",
|
|
27
|
+
promptTemplate: (p) => `Based on the error/issue you identified, provide the complete fix with corrected code. Original question: ${p.slice(0, 500)}`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
test: (resp) => /step|first|then|next|finally/i.test(resp) && resp.length > 500,
|
|
31
|
+
label: "📋 Checklist",
|
|
32
|
+
callbackPrefix: "smart_checklist",
|
|
33
|
+
promptTemplate: (_p, resp) => `Convert the steps you described into a clear, actionable checklist with checkboxes:\n\n${resp.slice(0, 2000)}`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
test: (_resp, prompt) => /how|what|why|explain|difference/i.test(prompt),
|
|
37
|
+
label: "🔬 Go deeper",
|
|
38
|
+
callbackPrefix: "smart_deeper",
|
|
39
|
+
promptTemplate: (p) => `Provide a much more detailed and in-depth explanation of: ${p.slice(0, 500)}. Include examples, edge cases, and advanced considerations.`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
test: (resp) => resp.length > 800,
|
|
43
|
+
label: "📝 TL;DR",
|
|
44
|
+
callbackPrefix: "smart_tldr",
|
|
45
|
+
promptTemplate: (_p, resp) => `Provide a very concise TL;DR summary (3-5 bullet points max) of your previous response:\n\n${resp.slice(0, 2000)}`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
test: (resp) => /alternative|option|approach|instead/i.test(resp) || resp.length > 500,
|
|
49
|
+
label: "🔀 Alternatives",
|
|
50
|
+
callbackPrefix: "smart_alt",
|
|
51
|
+
promptTemplate: (p) => `Suggest 3 alternative approaches or solutions for: ${p.slice(0, 500)}. Compare their pros and cons.`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
test: () => true, // Always available
|
|
55
|
+
label: "🔄 Continue",
|
|
56
|
+
callbackPrefix: "smart_continue",
|
|
57
|
+
promptTemplate: () => "Continue from where you left off. Provide more detail or the next steps.",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Build smart suggestion keyboard based on the AI response content.
|
|
62
|
+
* Returns an InlineKeyboard with up to 4 relevant action buttons.
|
|
63
|
+
*/
|
|
64
|
+
export function buildSmartSuggestions(response, prompt, maxButtons = 4) {
|
|
65
|
+
const matching = SUGGESTION_RULES.filter((rule) => rule.test(response, prompt));
|
|
66
|
+
if (matching.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
// Take top suggestions (most relevant first, since rules are ordered by specificity)
|
|
69
|
+
const selected = matching.slice(0, maxButtons);
|
|
70
|
+
const keyboard = new InlineKeyboard();
|
|
71
|
+
// Arrange buttons in 2-column layout
|
|
72
|
+
for (let i = 0; i < selected.length; i++) {
|
|
73
|
+
const rule = selected[i];
|
|
74
|
+
// Store a truncated version of the prompt as callback data (max 64 bytes for Telegram)
|
|
75
|
+
const callbackData = `${rule.callbackPrefix}:${Date.now().toString(36)}`;
|
|
76
|
+
keyboard.text(rule.label, callbackData);
|
|
77
|
+
if (i % 2 === 1 || i === selected.length - 1) {
|
|
78
|
+
keyboard.row();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return keyboard;
|
|
82
|
+
}
|
|
83
|
+
// In-memory store for pending smart suggestion prompts (TTL: 5 minutes)
|
|
84
|
+
const pendingPrompts = new Map();
|
|
85
|
+
/** Store the context for smart suggestion callbacks. */
|
|
86
|
+
export function storeSuggestionContext(callbackPrefix, timeKey, prompt, response) {
|
|
87
|
+
const key = `${callbackPrefix}:${timeKey}`;
|
|
88
|
+
pendingPrompts.set(key, { prompt, response, timestamp: Date.now() });
|
|
89
|
+
// Cleanup old entries (older than 5 minutes)
|
|
90
|
+
const cutoff = Date.now() - 5 * 60_000;
|
|
91
|
+
for (const [k, v] of pendingPrompts) {
|
|
92
|
+
if (v.timestamp < cutoff)
|
|
93
|
+
pendingPrompts.delete(k);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Register callback handlers for smart suggestion buttons. */
|
|
97
|
+
export function registerSmartSuggestionHandlers(bot) {
|
|
98
|
+
// Match all smart_ prefixed callbacks
|
|
99
|
+
bot.callbackQuery(/^smart_(\w+):(.+)$/, async (ctx) => {
|
|
100
|
+
const prefix = `smart_${ctx.match[1]}`;
|
|
101
|
+
const timeKey = ctx.match[2];
|
|
102
|
+
const fullKey = `${prefix}:${timeKey}`;
|
|
103
|
+
const context = pendingPrompts.get(fullKey);
|
|
104
|
+
if (!context) {
|
|
105
|
+
await ctx.answerCallbackQuery({ text: "Context expired — please ask again", show_alert: true });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Find the matching rule
|
|
109
|
+
const rule = SUGGESTION_RULES.find((r) => r.callbackPrefix === prefix);
|
|
110
|
+
if (!rule) {
|
|
111
|
+
await ctx.answerCallbackQuery({ text: "Unknown action" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await ctx.answerCallbackQuery({ text: `${rule.label}…` });
|
|
115
|
+
const chatId = ctx.chat.id;
|
|
116
|
+
const msgId = ctx.callbackQuery.message?.message_id;
|
|
117
|
+
// Send typing indicator
|
|
118
|
+
try {
|
|
119
|
+
await bot.api.sendChatAction(chatId, "typing");
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* best-effort */
|
|
123
|
+
}
|
|
124
|
+
const followUpPrompt = rule.promptTemplate(context.prompt, context.response);
|
|
125
|
+
sendToOrchestrator(followUpPrompt, { type: "telegram", chatId, messageId: msgId || 0 }, (text, done) => {
|
|
126
|
+
if (done) {
|
|
127
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: msgId });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Clean up used context
|
|
131
|
+
pendingPrompts.delete(fullKey);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build the full smart suggestion keyboard AND store the context for callbacks.
|
|
136
|
+
* Returns undefined if no suggestions are applicable.
|
|
137
|
+
*/
|
|
138
|
+
export function createSmartSuggestionsWithContext(response, prompt, maxButtons = 4) {
|
|
139
|
+
const matching = SUGGESTION_RULES.filter((rule) => rule.test(response, prompt));
|
|
140
|
+
if (matching.length === 0)
|
|
141
|
+
return undefined;
|
|
142
|
+
const selected = matching.slice(0, maxButtons);
|
|
143
|
+
const keyboard = new InlineKeyboard();
|
|
144
|
+
for (let i = 0; i < selected.length; i++) {
|
|
145
|
+
const rule = selected[i];
|
|
146
|
+
const timeKey = Date.now().toString(36) + i.toString(36);
|
|
147
|
+
const callbackData = `${rule.callbackPrefix}:${timeKey}`;
|
|
148
|
+
storeSuggestionContext(rule.callbackPrefix, timeKey, prompt, response);
|
|
149
|
+
keyboard.text(rule.label, callbackData);
|
|
150
|
+
if (i % 2 === 1 || i === selected.length - 1) {
|
|
151
|
+
keyboard.row();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return keyboard;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=suggestions.js.map
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ description: Helps users discover agent skills when they ask questions like "how
|
|
|
5
5
|
|
|
6
6
|
# Find Skills
|
|
7
7
|
|
|
8
|
-
Discover and install skills from the open agent skills ecosystem at https://skills.sh
|
|
8
|
+
Discover and install skills from the open agent skills ecosystem at <https://skills.sh/>.
|
|
9
9
|
|
|
10
10
|
## When to Use
|
|
11
11
|
|
|
@@ -51,7 +51,7 @@ Replace `QUERY` with a URL-encoded search term (e.g., `react`, `email`, `pr+revi
|
|
|
51
51
|
web_fetch url="https://skills.sh/audits"
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
If `web_fetch` fails or returns unexpected content, still present the search results but show "⚠️ Audit unavailable" for all security columns and include a link to https://skills.sh/audits so the user can check manually.
|
|
54
|
+
If `web_fetch` fails or returns unexpected content, still present the search results but show "⚠️ Audit unavailable" for all security columns and include a link to <https://skills.sh/audits> so the user can check manually.
|
|
55
55
|
|
|
56
56
|
This returns markdown where each skill has a heading (`### skill-name`) followed by its source, then three security scores:
|
|
57
57
|
|
|
@@ -76,6 +76,7 @@ Cross-reference the search results with the audit data and format as a numbered
|
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
**Formatting:**
|
|
79
|
+
|
|
79
80
|
- Sort by installs descending
|
|
80
81
|
- Format counts: 1000+ → "1.0K", 1000000+ → "1.0M"
|
|
81
82
|
- ✅ for Safe / Low Risk / 0 alerts, ⚠️ for Med Risk, 🔴 for High Risk / Critical / 1+ alerts
|
|
@@ -122,9 +123,9 @@ curl -fsSL "https://raw.githubusercontent.com/{source}/master/{skillId}/SKILL.md
|
|
|
122
123
|
|
|
123
124
|
If both fail, tell the user and link to `https://github.com/{source}`.
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
1. **Validate** the fetched content: it must not be empty and should contain meaningful instructions (more than just a title). If the content is empty, an HTML error page, or clearly not a SKILL.md, do NOT install — tell the user it couldn't be fetched properly.
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
2. **Install** using the `learn_skill` tool:
|
|
128
129
|
- `slug`: the `skillId` from the API
|
|
129
130
|
- `name`: from the SKILL.md frontmatter `name:` field (between `---` markers). If no frontmatter, use `skillId`.
|
|
130
131
|
- `description`: from the SKILL.md frontmatter `description:` field. If none, use the first sentence.
|