@iletai/nzb 1.4.7 → 1.4.9

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/config.js CHANGED
@@ -30,7 +30,7 @@ if (parsedUserId !== undefined && (Number.isNaN(parsedUserId) || parsedUserId <=
30
30
  if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
31
31
  throw new Error(`API_PORT must be 1-65535, got: "${raw.API_PORT}"`);
32
32
  }
33
- const DEFAULT_WORKER_TIMEOUT_MS = 600_000; // 10 minutes
33
+ const DEFAULT_WORKER_TIMEOUT_MS = 1_200_000; // 20 minutes
34
34
  const parsedWorkerTimeout = raw.WORKER_TIMEOUT ? Number(raw.WORKER_TIMEOUT) : DEFAULT_WORKER_TIMEOUT_MS;
35
35
  if (!Number.isInteger(parsedWorkerTimeout) || parsedWorkerTimeout <= 0) {
36
36
  throw new Error(`WORKER_TIMEOUT must be a positive integer (ms), got: "${raw.WORKER_TIMEOUT}"`);
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();
@@ -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", { reply_markup: mainMenu }));
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], { parse_mode: "HTML" });
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 = isFirst
575
- ? { parse_mode: "HTML", reply_parameters: replyParams }
576
- : { parse_mode: "HTML" };
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, isFirst ? { reply_parameters: replyParams } : {}));
607
+ .catch(() => ctx.reply(pageTag + fallback, fallbackOpts));
580
608
  if (index === 0 && sent)
581
609
  firstSentMsgId = sent.message_id;
582
610
  };
@@ -689,6 +717,14 @@ export async function startBot() {
689
717
  }
690
718
  bot
691
719
  .start({
720
+ allowed_updates: [
721
+ "message",
722
+ "edited_message",
723
+ "callback_query",
724
+ "inline_query",
725
+ "message_reaction",
726
+ "my_chat_member",
727
+ ],
692
728
  onStart: () => {
693
729
  console.log("[nzb] Telegram bot connected");
694
730
  void logInfo(`🚀 NZB v${process.env.npm_package_version || "?"} started (model: ${config.copilotModel})`);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "nzb": "dist/cli.js"
@@ -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
- 2. **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
+ 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
- 3. **Install** using the `learn_skill` tool:
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.