@iletai/nzb 1.4.4 → 1.4.6
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 +21 -6
- package/dist/store/db.js +4 -2
- package/dist/telegram/bot.js +24 -278
- package/dist/telegram/handlers/callbacks.js +43 -0
- package/dist/telegram/handlers/helpers.js +58 -0
- package/dist/telegram/handlers/media.js +185 -0
- package/package.json +68 -67
|
@@ -215,9 +215,11 @@ async function createOrResumeSession() {
|
|
|
215
215
|
const recentHistory = getRecentConversation(10);
|
|
216
216
|
if (recentHistory) {
|
|
217
217
|
console.log(`[nzb] Injecting recent conversation context into new session (non-blocking)`);
|
|
218
|
-
session
|
|
218
|
+
session
|
|
219
|
+
.sendAndWait({
|
|
219
220
|
prompt: `[System: Session recovered] Your previous session was lost. Here's the recent conversation for context — do NOT respond to these messages, just absorb the context silently:\n\n${recentHistory}\n\n(End of recovery context. Wait for the next real message.)`,
|
|
220
|
-
}, 20_000)
|
|
221
|
+
}, 20_000)
|
|
222
|
+
.catch((err) => {
|
|
221
223
|
console.log(`[nzb] Context recovery injection failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
222
224
|
});
|
|
223
225
|
}
|
|
@@ -262,7 +264,12 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
|
|
|
262
264
|
const unsubToolStart = session.on("tool.execution_start", (event) => {
|
|
263
265
|
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
264
266
|
const args = event?.data?.arguments;
|
|
265
|
-
const detail = args?.description ||
|
|
267
|
+
const detail = args?.description ||
|
|
268
|
+
args?.command?.slice(0, 80) ||
|
|
269
|
+
args?.intent ||
|
|
270
|
+
args?.pattern ||
|
|
271
|
+
args?.prompt?.slice(0, 80) ||
|
|
272
|
+
undefined;
|
|
266
273
|
onToolEvent?.({ type: "tool_start", toolName, detail });
|
|
267
274
|
});
|
|
268
275
|
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
@@ -377,7 +384,15 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
377
384
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
378
385
|
try {
|
|
379
386
|
const finalContent = await new Promise((resolve, reject) => {
|
|
380
|
-
const item = {
|
|
387
|
+
const item = {
|
|
388
|
+
prompt: taggedPrompt,
|
|
389
|
+
callback,
|
|
390
|
+
onToolEvent,
|
|
391
|
+
onUsage,
|
|
392
|
+
sourceChannel,
|
|
393
|
+
resolve,
|
|
394
|
+
reject,
|
|
395
|
+
};
|
|
381
396
|
if (source.type === "background") {
|
|
382
397
|
// Background results go to the back of the queue
|
|
383
398
|
messageQueue.push(item);
|
|
@@ -416,7 +431,7 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
416
431
|
catch {
|
|
417
432
|
/* best-effort */
|
|
418
433
|
}
|
|
419
|
-
callback(finalContent, true, { assistantLogId });
|
|
434
|
+
await callback(finalContent, true, { assistantLogId });
|
|
420
435
|
// Auto-continue: if the response was cut short by timeout, automatically
|
|
421
436
|
// send a follow-up "Continue" message so the user doesn't have to
|
|
422
437
|
if (finalContent.includes("⏱ Response was cut short (timeout)")) {
|
|
@@ -454,7 +469,7 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
454
469
|
continue;
|
|
455
470
|
}
|
|
456
471
|
console.error(`[nzb] Error processing message: ${msg}`);
|
|
457
|
-
callback(`Error: ${msg}`, true);
|
|
472
|
+
await callback(`Error: ${msg}`, true);
|
|
458
473
|
return;
|
|
459
474
|
}
|
|
460
475
|
}
|
package/dist/store/db.js
CHANGED
|
@@ -136,7 +136,8 @@ export function getConversationContext(telegramMsgId) {
|
|
|
136
136
|
if (!row)
|
|
137
137
|
return undefined;
|
|
138
138
|
// Fetch 4 rows before + the target + 4 rows after (handles ID gaps from pruning)
|
|
139
|
-
const rows = db
|
|
139
|
+
const rows = db
|
|
140
|
+
.prepare(`
|
|
140
141
|
SELECT role, content, source, ts FROM (
|
|
141
142
|
SELECT * FROM conversation_log WHERE id < ? ORDER BY id DESC LIMIT 4
|
|
142
143
|
)
|
|
@@ -146,7 +147,8 @@ export function getConversationContext(telegramMsgId) {
|
|
|
146
147
|
SELECT role, content, source, ts FROM (
|
|
147
148
|
SELECT * FROM conversation_log WHERE id > ? ORDER BY id ASC LIMIT 4
|
|
148
149
|
)
|
|
149
|
-
`)
|
|
150
|
+
`)
|
|
151
|
+
.all(row.id, row.id, row.id);
|
|
150
152
|
if (rows.length === 0)
|
|
151
153
|
return undefined;
|
|
152
154
|
return rows
|
package/dist/telegram/bot.js
CHANGED
|
@@ -2,13 +2,15 @@ import { autoRetry } from "@grammyjs/auto-retry";
|
|
|
2
2
|
import { Menu } from "@grammyjs/menu";
|
|
3
3
|
import { Bot, InlineKeyboard, Keyboard } from "grammy";
|
|
4
4
|
import { Agent as HttpsAgent } from "https";
|
|
5
|
-
import { rmSync } from "fs";
|
|
6
5
|
import { config, persistEnvVar, persistModel } from "../config.js";
|
|
7
6
|
import { cancelCurrentMessage, getQueueSize, getWorkers, sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
8
7
|
import { listSkills } from "../copilot/skills.js";
|
|
9
8
|
import { restartDaemon } from "../daemon.js";
|
|
10
9
|
import { searchMemories } from "../store/db.js";
|
|
11
10
|
import { chunkMessage, escapeHtml, formatToolSummaryExpandable, toTelegramHTML } from "./formatter.js";
|
|
11
|
+
import { registerCallbackHandlers } from "./handlers/callbacks.js";
|
|
12
|
+
import { sendFormattedReply } from "./handlers/helpers.js";
|
|
13
|
+
import { registerMediaHandlers } from "./handlers/media.js";
|
|
12
14
|
import { initLogChannel, logDebug, logError, logInfo } from "./log-channel.js";
|
|
13
15
|
let bot;
|
|
14
16
|
const startedAt = Date.now();
|
|
@@ -20,61 +22,6 @@ function getUptimeStr() {
|
|
|
20
22
|
const seconds = uptime % 60;
|
|
21
23
|
return hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
22
24
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Send a formatted HTML reply with multi-chunk support and fallback to plain text.
|
|
25
|
-
* Consolidates the repeated toTelegramHTML → chunkMessage → fallback → send pattern.
|
|
26
|
-
*/
|
|
27
|
-
async function sendFormattedReply(botInstance, chatId, text, opts) {
|
|
28
|
-
const formatted = toTelegramHTML(text);
|
|
29
|
-
const chunks = chunkMessage(formatted);
|
|
30
|
-
const fallbackChunks = chunkMessage(text);
|
|
31
|
-
let firstMsgId;
|
|
32
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
33
|
-
if (i > 0)
|
|
34
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
35
|
-
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
36
|
-
const replyParams = i === 0 && opts?.replyTo ? { message_id: opts.replyTo } : undefined;
|
|
37
|
-
try {
|
|
38
|
-
const sent = await botInstance.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
39
|
-
parse_mode: "HTML",
|
|
40
|
-
reply_parameters: replyParams,
|
|
41
|
-
});
|
|
42
|
-
if (i === 0)
|
|
43
|
-
firstMsgId = sent.message_id;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
try {
|
|
47
|
-
const sent = await botInstance.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
48
|
-
reply_parameters: replyParams,
|
|
49
|
-
});
|
|
50
|
-
if (i === 0)
|
|
51
|
-
firstMsgId = sent.message_id;
|
|
52
|
-
}
|
|
53
|
-
catch { }
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (opts?.assistantLogId && firstMsgId) {
|
|
57
|
-
try {
|
|
58
|
-
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
59
|
-
setConversationTelegramMsgId(opts.assistantLogId, firstMsgId);
|
|
60
|
-
}
|
|
61
|
-
catch { }
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
await botInstance.api.setMessageReaction(chatId, opts?.replyTo ?? 0, [{ type: "emoji", emoji: "👍" }]);
|
|
65
|
-
}
|
|
66
|
-
catch { }
|
|
67
|
-
return firstMsgId;
|
|
68
|
-
}
|
|
69
|
-
/** Remove a temp directory after a delay (gives orchestrator time to use the file). */
|
|
70
|
-
function scheduleTempCleanup(dirPath, delayMs = 5 * 60_000) {
|
|
71
|
-
setTimeout(() => {
|
|
72
|
-
try {
|
|
73
|
-
rmSync(dirPath, { recursive: true, force: true });
|
|
74
|
-
}
|
|
75
|
-
catch { }
|
|
76
|
-
}, delayMs);
|
|
77
|
-
}
|
|
78
25
|
// Settings sub-menu
|
|
79
26
|
const settingsMenu = new Menu("settings-menu")
|
|
80
27
|
.text((ctx) => `${config.showReasoning ? "✅" : "❌"} Show Reasoning`, async (ctx) => {
|
|
@@ -211,47 +158,15 @@ export function createBot() {
|
|
|
211
158
|
});
|
|
212
159
|
// Register interactive menu plugin
|
|
213
160
|
bot.use(mainMenu);
|
|
214
|
-
//
|
|
215
|
-
bot
|
|
216
|
-
await ctx.answerCallbackQuery({ text: "Retrying..." });
|
|
217
|
-
const originalMsg = ctx.callbackQuery.message;
|
|
218
|
-
if (originalMsg?.reply_to_message && "text" in originalMsg.reply_to_message && originalMsg.reply_to_message.text) {
|
|
219
|
-
const retryPrompt = originalMsg.reply_to_message.text;
|
|
220
|
-
const chatId = ctx.chat.id;
|
|
221
|
-
sendToOrchestrator(retryPrompt, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
222
|
-
if (done) {
|
|
223
|
-
void (async () => {
|
|
224
|
-
try {
|
|
225
|
-
const formatted = toTelegramHTML(text);
|
|
226
|
-
const chunks = chunkMessage(formatted);
|
|
227
|
-
await bot.api.editMessageText(chatId, originalMsg.message_id, chunks[0], { parse_mode: "HTML" });
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
try {
|
|
231
|
-
await ctx.reply(text);
|
|
232
|
-
}
|
|
233
|
-
catch { }
|
|
234
|
-
}
|
|
235
|
-
})();
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
bot.callbackQuery("explain_error", async (ctx) => {
|
|
241
|
-
await ctx.answerCallbackQuery({ text: "Explaining..." });
|
|
242
|
-
const originalMsg = ctx.callbackQuery.message;
|
|
243
|
-
if (originalMsg && "text" in originalMsg && originalMsg.text) {
|
|
244
|
-
const chatId = ctx.chat.id;
|
|
245
|
-
sendToOrchestrator(`Explain this error in simple terms and suggest a fix:\n${originalMsg.text}`, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
246
|
-
if (done)
|
|
247
|
-
void sendFormattedReply(bot, chatId, text);
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
});
|
|
161
|
+
// Register callback + media handlers from extracted modules
|
|
162
|
+
registerCallbackHandlers(bot);
|
|
251
163
|
// Persistent reply keyboard — quick actions always visible below chat input
|
|
252
164
|
const replyKeyboard = new Keyboard()
|
|
253
|
-
.text("📊 Status")
|
|
254
|
-
.text("
|
|
165
|
+
.text("📊 Status")
|
|
166
|
+
.text("❌ Cancel")
|
|
167
|
+
.row()
|
|
168
|
+
.text("🧠 Memory")
|
|
169
|
+
.text("🔄 Restart")
|
|
255
170
|
.resized()
|
|
256
171
|
.persistent();
|
|
257
172
|
// /start and /help — with inline menu + reply keyboard
|
|
@@ -393,7 +308,9 @@ export function createBot() {
|
|
|
393
308
|
});
|
|
394
309
|
bot.hears("🔄 Restart", async (ctx) => {
|
|
395
310
|
await ctx.reply("Restarting NZB...");
|
|
396
|
-
setTimeout(() => {
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
restartDaemon().catch(console.error);
|
|
313
|
+
}, 500);
|
|
397
314
|
});
|
|
398
315
|
// Handle all text messages — progressive streaming with tool event feedback
|
|
399
316
|
bot.on("message:text", async (ctx) => {
|
|
@@ -406,7 +323,9 @@ export function createBot() {
|
|
|
406
323
|
try {
|
|
407
324
|
await ctx.react("👀");
|
|
408
325
|
}
|
|
409
|
-
catch {
|
|
326
|
+
catch {
|
|
327
|
+
/* reactions may not be available */
|
|
328
|
+
}
|
|
410
329
|
// Typing indicator — keeps sending "typing" action every 4s until the final
|
|
411
330
|
// response is delivered. We use bot.api directly for reliability, and await the
|
|
412
331
|
// first call so the user sees typing immediately before any async work begins.
|
|
@@ -561,8 +480,8 @@ export function createBot() {
|
|
|
561
480
|
const assistantLogId = meta?.assistantLogId;
|
|
562
481
|
const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
|
|
563
482
|
void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
|
|
564
|
-
//
|
|
565
|
-
|
|
483
|
+
// Return the edit chain so callers can await final delivery
|
|
484
|
+
return editChain.then(async () => {
|
|
566
485
|
// Format error messages with a distinct visual
|
|
567
486
|
const isError = text.startsWith("Error:");
|
|
568
487
|
if (isError) {
|
|
@@ -588,7 +507,7 @@ export function createBot() {
|
|
|
588
507
|
}
|
|
589
508
|
let textWithMeta = text;
|
|
590
509
|
if (usageInfo) {
|
|
591
|
-
const fmtTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
510
|
+
const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
|
|
592
511
|
const parts = [];
|
|
593
512
|
if (usageInfo.model)
|
|
594
513
|
parts.push(usageInfo.model);
|
|
@@ -708,7 +627,9 @@ export function createBot() {
|
|
|
708
627
|
try {
|
|
709
628
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
710
629
|
}
|
|
711
|
-
catch {
|
|
630
|
+
catch {
|
|
631
|
+
/* reactions may not be available */
|
|
632
|
+
}
|
|
712
633
|
});
|
|
713
634
|
}
|
|
714
635
|
else {
|
|
@@ -731,183 +652,8 @@ export function createBot() {
|
|
|
731
652
|
}
|
|
732
653
|
}, onToolEvent, onUsage);
|
|
733
654
|
});
|
|
734
|
-
//
|
|
735
|
-
bot
|
|
736
|
-
const chatId = ctx.chat.id;
|
|
737
|
-
const userMessageId = ctx.message.message_id;
|
|
738
|
-
const caption = ctx.message.caption || "Describe this image and analyze what you see.";
|
|
739
|
-
void logInfo(`📸 Photo received: ${caption.slice(0, 80)}`);
|
|
740
|
-
try {
|
|
741
|
-
await ctx.react("👀");
|
|
742
|
-
}
|
|
743
|
-
catch { }
|
|
744
|
-
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
|
745
|
-
try {
|
|
746
|
-
const file = await ctx.api.getFile(photo.file_id);
|
|
747
|
-
const filePath = file.file_path;
|
|
748
|
-
if (!filePath) {
|
|
749
|
-
await ctx.reply("❌ Could not download photo.", { reply_parameters: { message_id: userMessageId } });
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
753
|
-
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
754
|
-
const { join } = await import("path");
|
|
755
|
-
const { tmpdir } = await import("os");
|
|
756
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-photo-"));
|
|
757
|
-
const ext = filePath.split(".").pop() || "jpg";
|
|
758
|
-
const localPath = join(tmpDir, `photo.${ext}`);
|
|
759
|
-
const response = await fetch(url);
|
|
760
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
761
|
-
writeFileSync(localPath, buffer);
|
|
762
|
-
scheduleTempCleanup(tmpDir);
|
|
763
|
-
const prompt = `[User sent a photo saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this image. The file is at ${localPath} — you can use bash to view it with tools if needed.`;
|
|
764
|
-
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
765
|
-
if (done)
|
|
766
|
-
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
catch (err) {
|
|
770
|
-
await ctx.reply(`❌ Error processing photo: ${err instanceof Error ? err.message : String(err)}`, {
|
|
771
|
-
reply_parameters: { message_id: userMessageId },
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
});
|
|
775
|
-
// Handle document/file messages — download and pass to AI
|
|
776
|
-
bot.on("message:document", async (ctx) => {
|
|
777
|
-
const chatId = ctx.chat.id;
|
|
778
|
-
const userMessageId = ctx.message.message_id;
|
|
779
|
-
const doc = ctx.message.document;
|
|
780
|
-
const caption = ctx.message.caption || `Analyze this file: ${doc.file_name || "unknown"}`;
|
|
781
|
-
void logInfo(`📄 Document received: ${doc.file_name || "unknown"} (${doc.file_size || 0} bytes)`);
|
|
782
|
-
try {
|
|
783
|
-
await ctx.react("👀");
|
|
784
|
-
}
|
|
785
|
-
catch { }
|
|
786
|
-
// Limit file size to 10MB
|
|
787
|
-
if (doc.file_size && doc.file_size > 10 * 1024 * 1024) {
|
|
788
|
-
await ctx.reply("❌ File too large (max 10MB).", { reply_parameters: { message_id: userMessageId } });
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
try {
|
|
792
|
-
const file = await ctx.api.getFile(doc.file_id);
|
|
793
|
-
const filePath = file.file_path;
|
|
794
|
-
if (!filePath) {
|
|
795
|
-
await ctx.reply("❌ Could not download file.", { reply_parameters: { message_id: userMessageId } });
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
799
|
-
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
800
|
-
const { join } = await import("path");
|
|
801
|
-
const { tmpdir } = await import("os");
|
|
802
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-doc-"));
|
|
803
|
-
const localPath = join(tmpDir, doc.file_name || "file");
|
|
804
|
-
const response = await fetch(url);
|
|
805
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
806
|
-
writeFileSync(localPath, buffer);
|
|
807
|
-
scheduleTempCleanup(tmpDir);
|
|
808
|
-
const prompt = `[User sent a file: ${doc.file_name || "unknown"} (${doc.file_size || 0} bytes), saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this file. You can read it with bash tools.`;
|
|
809
|
-
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
810
|
-
if (done)
|
|
811
|
-
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
catch (err) {
|
|
815
|
-
await ctx.reply(`❌ Error processing file: ${err instanceof Error ? err.message : String(err)}`, {
|
|
816
|
-
reply_parameters: { message_id: userMessageId },
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
// Handle voice messages — download, transcribe via Whisper, send to AI
|
|
821
|
-
bot.on("message:voice", async (ctx) => {
|
|
822
|
-
const chatId = ctx.chat.id;
|
|
823
|
-
const userMessageId = ctx.message.message_id;
|
|
824
|
-
const duration = ctx.message.voice.duration;
|
|
825
|
-
void logInfo(`🎤 Voice received: ${duration}s`);
|
|
826
|
-
try {
|
|
827
|
-
await ctx.react("👀");
|
|
828
|
-
}
|
|
829
|
-
catch { }
|
|
830
|
-
// Limit voice duration to 5 minutes
|
|
831
|
-
if (duration > 300) {
|
|
832
|
-
await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
// If voice is a reply, include context
|
|
836
|
-
let voiceReplyContext = "";
|
|
837
|
-
const voiceReplyMsg = ctx.message.reply_to_message;
|
|
838
|
-
if (voiceReplyMsg && "text" in voiceReplyMsg && voiceReplyMsg.text) {
|
|
839
|
-
const { getConversationContext } = await import("../store/db.js");
|
|
840
|
-
const context = getConversationContext(voiceReplyMsg.message_id);
|
|
841
|
-
if (context) {
|
|
842
|
-
voiceReplyContext = `[Continuing from earlier conversation:]\n---\n${context}\n---\n\n`;
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
const quoted = voiceReplyMsg.text.length > 500 ? voiceReplyMsg.text.slice(0, 500) + "…" : voiceReplyMsg.text;
|
|
846
|
-
voiceReplyContext = `[Replying to: "${quoted}"]\n\n`;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
try {
|
|
850
|
-
const file = await ctx.api.getFile(ctx.message.voice.file_id);
|
|
851
|
-
const filePath = file.file_path;
|
|
852
|
-
if (!filePath) {
|
|
853
|
-
await ctx.reply("❌ Could not download voice.", { reply_parameters: { message_id: userMessageId } });
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
857
|
-
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
858
|
-
const { join } = await import("path");
|
|
859
|
-
const { tmpdir } = await import("os");
|
|
860
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-voice-"));
|
|
861
|
-
const ext = filePath.split(".").pop() || "oga";
|
|
862
|
-
const localPath = join(tmpDir, `voice.${ext}`);
|
|
863
|
-
const response = await fetch(url);
|
|
864
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
865
|
-
writeFileSync(localPath, buffer);
|
|
866
|
-
scheduleTempCleanup(tmpDir);
|
|
867
|
-
let prompt;
|
|
868
|
-
if (config.openaiApiKey) {
|
|
869
|
-
// Transcribe using OpenAI Whisper API
|
|
870
|
-
try {
|
|
871
|
-
const formData = new FormData();
|
|
872
|
-
formData.append("file", new Blob([buffer], { type: "audio/ogg" }), `voice.${ext}`);
|
|
873
|
-
formData.append("model", "whisper-1");
|
|
874
|
-
const whisperResp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
875
|
-
method: "POST",
|
|
876
|
-
headers: { Authorization: `Bearer ${config.openaiApiKey}` },
|
|
877
|
-
body: formData,
|
|
878
|
-
});
|
|
879
|
-
if (!whisperResp.ok) {
|
|
880
|
-
const errText = await whisperResp.text();
|
|
881
|
-
throw new Error(`Whisper API ${whisperResp.status}: ${errText.slice(0, 200)}`);
|
|
882
|
-
}
|
|
883
|
-
const result = (await whisperResp.json());
|
|
884
|
-
const transcript = result.text?.trim();
|
|
885
|
-
if (!transcript) {
|
|
886
|
-
prompt = `[User sent a voice message (${duration}s) but transcription was empty. File saved at: ${localPath}]`;
|
|
887
|
-
}
|
|
888
|
-
else {
|
|
889
|
-
prompt = `[Voice message transcribed (${duration}s)]: ${transcript}`;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
catch (whisperErr) {
|
|
893
|
-
console.error("[nzb] Whisper transcription failed:", whisperErr instanceof Error ? whisperErr.message : whisperErr);
|
|
894
|
-
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. Transcription failed: ${whisperErr instanceof Error ? whisperErr.message : String(whisperErr)}]`;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
else {
|
|
898
|
-
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. No OPENAI_API_KEY configured for transcription. You can tell the user to set it up in ~/.nzb/.env]`;
|
|
899
|
-
}
|
|
900
|
-
sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
901
|
-
if (done)
|
|
902
|
-
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId, assistantLogId: meta?.assistantLogId });
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
catch (err) {
|
|
906
|
-
await ctx.reply(`❌ Error processing voice: ${err instanceof Error ? err.message : String(err)}`, {
|
|
907
|
-
reply_parameters: { message_id: userMessageId },
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
});
|
|
655
|
+
// Register media handlers (photo, document, voice) from extracted module
|
|
656
|
+
registerMediaHandlers(bot);
|
|
911
657
|
// Global error handler — prevents unhandled errors from crashing the bot
|
|
912
658
|
bot.catch((err) => {
|
|
913
659
|
const ctx = err.ctx;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
2
|
+
import { chunkMessage, toTelegramHTML } from "../formatter.js";
|
|
3
|
+
import { sendFormattedReply } from "./helpers.js";
|
|
4
|
+
/** Register inline keyboard callback handlers (Retry, Explain). */
|
|
5
|
+
export function registerCallbackHandlers(bot) {
|
|
6
|
+
bot.callbackQuery("retry", async (ctx) => {
|
|
7
|
+
await ctx.answerCallbackQuery({ text: "Retrying..." });
|
|
8
|
+
const originalMsg = ctx.callbackQuery.message;
|
|
9
|
+
if (originalMsg?.reply_to_message && "text" in originalMsg.reply_to_message && originalMsg.reply_to_message.text) {
|
|
10
|
+
const retryPrompt = originalMsg.reply_to_message.text;
|
|
11
|
+
const chatId = ctx.chat.id;
|
|
12
|
+
sendToOrchestrator(retryPrompt, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
13
|
+
if (done) {
|
|
14
|
+
void (async () => {
|
|
15
|
+
try {
|
|
16
|
+
const formatted = toTelegramHTML(text);
|
|
17
|
+
const chunks = chunkMessage(formatted);
|
|
18
|
+
await bot.api.editMessageText(chatId, originalMsg.message_id, chunks[0], { parse_mode: "HTML" });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
try {
|
|
22
|
+
await ctx.reply(text);
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
bot.callbackQuery("explain_error", async (ctx) => {
|
|
32
|
+
await ctx.answerCallbackQuery({ text: "Explaining..." });
|
|
33
|
+
const originalMsg = ctx.callbackQuery.message;
|
|
34
|
+
if (originalMsg && "text" in originalMsg && originalMsg.text) {
|
|
35
|
+
const chatId = ctx.chat.id;
|
|
36
|
+
sendToOrchestrator(`Explain this error in simple terms and suggest a fix:\n${originalMsg.text}`, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
37
|
+
if (done)
|
|
38
|
+
void sendFormattedReply(bot, chatId, text);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=callbacks.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { rmSync } from "fs";
|
|
2
|
+
import { chunkMessage, toTelegramHTML } from "../formatter.js";
|
|
3
|
+
/**
|
|
4
|
+
* Send a formatted HTML reply with multi-chunk support and fallback to plain text.
|
|
5
|
+
* Consolidates the repeated toTelegramHTML → chunkMessage → fallback → send pattern.
|
|
6
|
+
*/
|
|
7
|
+
export async function sendFormattedReply(botInstance, chatId, text, opts) {
|
|
8
|
+
const formatted = toTelegramHTML(text);
|
|
9
|
+
const chunks = chunkMessage(formatted);
|
|
10
|
+
const fallbackChunks = chunkMessage(text);
|
|
11
|
+
let firstMsgId;
|
|
12
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
13
|
+
if (i > 0)
|
|
14
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
15
|
+
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
16
|
+
const replyParams = i === 0 && opts?.replyTo ? { message_id: opts.replyTo } : undefined;
|
|
17
|
+
try {
|
|
18
|
+
const sent = await botInstance.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
19
|
+
parse_mode: "HTML",
|
|
20
|
+
reply_parameters: replyParams,
|
|
21
|
+
});
|
|
22
|
+
if (i === 0)
|
|
23
|
+
firstMsgId = sent.message_id;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
try {
|
|
27
|
+
const sent = await botInstance.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
28
|
+
reply_parameters: replyParams,
|
|
29
|
+
});
|
|
30
|
+
if (i === 0)
|
|
31
|
+
firstMsgId = sent.message_id;
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (opts?.assistantLogId && firstMsgId) {
|
|
37
|
+
try {
|
|
38
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
39
|
+
setConversationTelegramMsgId(opts.assistantLogId, firstMsgId);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await botInstance.api.setMessageReaction(chatId, opts?.replyTo ?? 0, [{ type: "emoji", emoji: "👍" }]);
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
return firstMsgId;
|
|
48
|
+
}
|
|
49
|
+
/** Remove a temp directory after a delay (gives orchestrator time to use the file). */
|
|
50
|
+
export function scheduleTempCleanup(dirPath, delayMs = 5 * 60_000) {
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
try {
|
|
53
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
}, delayMs);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { config } from "../../config.js";
|
|
2
|
+
import { sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
3
|
+
import { logInfo } from "../log-channel.js";
|
|
4
|
+
import { sendFormattedReply, scheduleTempCleanup } from "./helpers.js";
|
|
5
|
+
/** Register photo, document, and voice message handlers on the bot. */
|
|
6
|
+
export function registerMediaHandlers(bot) {
|
|
7
|
+
// Handle photo messages — download and pass to AI
|
|
8
|
+
bot.on("message:photo", async (ctx) => {
|
|
9
|
+
const chatId = ctx.chat.id;
|
|
10
|
+
const userMessageId = ctx.message.message_id;
|
|
11
|
+
const caption = ctx.message.caption || "Describe this image and analyze what you see.";
|
|
12
|
+
void logInfo(`📸 Photo received: ${caption.slice(0, 80)}`);
|
|
13
|
+
try {
|
|
14
|
+
await ctx.react("👀");
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
|
18
|
+
try {
|
|
19
|
+
const file = await ctx.api.getFile(photo.file_id);
|
|
20
|
+
const filePath = file.file_path;
|
|
21
|
+
if (!filePath) {
|
|
22
|
+
await ctx.reply("❌ Could not download photo.", { reply_parameters: { message_id: userMessageId } });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
26
|
+
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
27
|
+
const { join } = await import("path");
|
|
28
|
+
const { tmpdir } = await import("os");
|
|
29
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-photo-"));
|
|
30
|
+
const ext = filePath.split(".").pop() || "jpg";
|
|
31
|
+
const localPath = join(tmpDir, `photo.${ext}`);
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
34
|
+
writeFileSync(localPath, buffer);
|
|
35
|
+
scheduleTempCleanup(tmpDir);
|
|
36
|
+
const prompt = `[User sent a photo saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this image. The file is at ${localPath} — you can use bash to view it with tools if needed.`;
|
|
37
|
+
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
38
|
+
if (done)
|
|
39
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
await ctx.reply(`❌ Error processing photo: ${err instanceof Error ? err.message : String(err)}`, {
|
|
44
|
+
reply_parameters: { message_id: userMessageId },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Handle document/file messages — download and pass to AI
|
|
49
|
+
bot.on("message:document", async (ctx) => {
|
|
50
|
+
const chatId = ctx.chat.id;
|
|
51
|
+
const userMessageId = ctx.message.message_id;
|
|
52
|
+
const doc = ctx.message.document;
|
|
53
|
+
const caption = ctx.message.caption || `Analyze this file: ${doc.file_name || "unknown"}`;
|
|
54
|
+
void logInfo(`📄 Document received: ${doc.file_name || "unknown"} (${doc.file_size || 0} bytes)`);
|
|
55
|
+
try {
|
|
56
|
+
await ctx.react("👀");
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
if (doc.file_size && doc.file_size > 10 * 1024 * 1024) {
|
|
60
|
+
await ctx.reply("❌ File too large (max 10MB).", { reply_parameters: { message_id: userMessageId } });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const file = await ctx.api.getFile(doc.file_id);
|
|
65
|
+
const filePath = file.file_path;
|
|
66
|
+
if (!filePath) {
|
|
67
|
+
await ctx.reply("❌ Could not download file.", { reply_parameters: { message_id: userMessageId } });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
71
|
+
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
72
|
+
const { join } = await import("path");
|
|
73
|
+
const { tmpdir } = await import("os");
|
|
74
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-doc-"));
|
|
75
|
+
const localPath = join(tmpDir, doc.file_name || "file");
|
|
76
|
+
const response = await fetch(url);
|
|
77
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
78
|
+
writeFileSync(localPath, buffer);
|
|
79
|
+
scheduleTempCleanup(tmpDir);
|
|
80
|
+
const prompt = `[User sent a file: ${doc.file_name || "unknown"} (${doc.file_size || 0} bytes), saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this file. You can read it with bash tools.`;
|
|
81
|
+
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
82
|
+
if (done)
|
|
83
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
await ctx.reply(`❌ Error processing file: ${err instanceof Error ? err.message : String(err)}`, {
|
|
88
|
+
reply_parameters: { message_id: userMessageId },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Handle voice messages — download, transcribe via Whisper, send to AI
|
|
93
|
+
bot.on("message:voice", async (ctx) => {
|
|
94
|
+
const chatId = ctx.chat.id;
|
|
95
|
+
const userMessageId = ctx.message.message_id;
|
|
96
|
+
const duration = ctx.message.voice.duration;
|
|
97
|
+
void logInfo(`🎤 Voice received: ${duration}s`);
|
|
98
|
+
try {
|
|
99
|
+
await ctx.react("👀");
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
if (duration > 300) {
|
|
103
|
+
await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// If voice is a reply, include context
|
|
107
|
+
let voiceReplyContext = "";
|
|
108
|
+
const voiceReplyMsg = ctx.message.reply_to_message;
|
|
109
|
+
if (voiceReplyMsg && "text" in voiceReplyMsg && voiceReplyMsg.text) {
|
|
110
|
+
const { getConversationContext } = await import("../../store/db.js");
|
|
111
|
+
const context = getConversationContext(voiceReplyMsg.message_id);
|
|
112
|
+
if (context) {
|
|
113
|
+
voiceReplyContext = `[Continuing from earlier conversation:]\n---\n${context}\n---\n\n`;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const quoted = voiceReplyMsg.text.length > 500 ? voiceReplyMsg.text.slice(0, 500) + "…" : voiceReplyMsg.text;
|
|
117
|
+
voiceReplyContext = `[Replying to: "${quoted}"]\n\n`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const file = await ctx.api.getFile(ctx.message.voice.file_id);
|
|
122
|
+
const filePath = file.file_path;
|
|
123
|
+
if (!filePath) {
|
|
124
|
+
await ctx.reply("❌ Could not download voice.", { reply_parameters: { message_id: userMessageId } });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
128
|
+
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
129
|
+
const { join } = await import("path");
|
|
130
|
+
const { tmpdir } = await import("os");
|
|
131
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-voice-"));
|
|
132
|
+
const ext = filePath.split(".").pop() || "oga";
|
|
133
|
+
const localPath = join(tmpDir, `voice.${ext}`);
|
|
134
|
+
const response = await fetch(url);
|
|
135
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
136
|
+
writeFileSync(localPath, buffer);
|
|
137
|
+
scheduleTempCleanup(tmpDir);
|
|
138
|
+
let prompt;
|
|
139
|
+
if (config.openaiApiKey) {
|
|
140
|
+
try {
|
|
141
|
+
const formData = new FormData();
|
|
142
|
+
formData.append("file", new Blob([buffer], { type: "audio/ogg" }), `voice.${ext}`);
|
|
143
|
+
formData.append("model", "whisper-1");
|
|
144
|
+
const whisperResp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { Authorization: `Bearer ${config.openaiApiKey}` },
|
|
147
|
+
body: formData,
|
|
148
|
+
});
|
|
149
|
+
if (!whisperResp.ok) {
|
|
150
|
+
const errText = await whisperResp.text();
|
|
151
|
+
throw new Error(`Whisper API ${whisperResp.status}: ${errText.slice(0, 200)}`);
|
|
152
|
+
}
|
|
153
|
+
const result = (await whisperResp.json());
|
|
154
|
+
const transcript = result.text?.trim();
|
|
155
|
+
if (!transcript) {
|
|
156
|
+
prompt = `[User sent a voice message (${duration}s) but transcription was empty. File saved at: ${localPath}]`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
prompt = `[Voice message transcribed (${duration}s)]: ${transcript}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (whisperErr) {
|
|
163
|
+
console.error("[nzb] Whisper transcription failed:", whisperErr instanceof Error ? whisperErr.message : whisperErr);
|
|
164
|
+
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. Transcription failed: ${whisperErr instanceof Error ? whisperErr.message : String(whisperErr)}]`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. No OPENAI_API_KEY configured for transcription. You can tell the user to set it up in ~/.nzb/.env]`;
|
|
169
|
+
}
|
|
170
|
+
sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
171
|
+
if (done)
|
|
172
|
+
void sendFormattedReply(bot, chatId, text, {
|
|
173
|
+
replyTo: userMessageId,
|
|
174
|
+
assistantLogId: meta?.assistantLogId,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
await ctx.reply(`❌ Error processing voice: ${err instanceof Error ? err.message : String(err)}`, {
|
|
180
|
+
reply_parameters: { message_id: userMessageId },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=media.js.map
|
package/package.json
CHANGED
|
@@ -1,69 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
2
|
+
"name": "@iletai/nzb",
|
|
3
|
+
"version": "1.4.6",
|
|
4
|
+
"description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nzb": "dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*.js",
|
|
13
|
+
"scripts/fix-esm-imports.cjs",
|
|
14
|
+
"skills/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"postinstall": "node scripts/fix-esm-imports.cjs",
|
|
22
|
+
"daemon": "tsx src/daemon.ts",
|
|
23
|
+
"tui": "tsx src/tui/index.ts",
|
|
24
|
+
"dev": "tsx --watch src/cli.ts start",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"format:check": "prettier --check .",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"copilot",
|
|
34
|
+
"telegram",
|
|
35
|
+
"orchestrator",
|
|
36
|
+
"ai",
|
|
37
|
+
"cli"
|
|
38
|
+
],
|
|
39
|
+
"author": "iletai",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/iletai/AI-Agent-Assistant.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/iletai/AI-Agent-Assistant#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/iletai/AI-Agent-Assistant/issues"
|
|
48
|
+
},
|
|
49
|
+
"type": "module",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@github/copilot-sdk": "^0.1.26",
|
|
52
|
+
"@grammyjs/auto-retry": "^2.0.2",
|
|
53
|
+
"@grammyjs/menu": "^1.3.1",
|
|
54
|
+
"better-sqlite3": "^12.6.2",
|
|
55
|
+
"dotenv": "^17.3.1",
|
|
56
|
+
"express": "^5.2.1",
|
|
57
|
+
"grammy": "^1.40.0",
|
|
58
|
+
"zod": "^4.3.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
62
|
+
"@types/express": "^5.0.6",
|
|
63
|
+
"@types/node": "^25.3.0",
|
|
64
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
65
|
+
"prettier": "^3.8.1",
|
|
66
|
+
"tsx": "^4.21.0",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"vitest": "^4.1.0"
|
|
69
|
+
}
|
|
69
70
|
}
|