@iletai/nzb 1.3.3 → 1.3.4

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
@@ -14,6 +14,7 @@ const configSchema = z.object({
14
14
  SHOW_REASONING: z.string().optional(),
15
15
  LOG_CHANNEL_ID: z.string().optional(),
16
16
  NODE_EXTRA_CA_CERTS: z.string().optional(),
17
+ OPENAI_API_KEY: z.string().optional(),
17
18
  });
18
19
  const raw = configSchema.parse(process.env);
19
20
  // Apply NODE_EXTRA_CA_CERTS from .env if not already set via environment.
@@ -43,6 +44,7 @@ export const config = {
43
44
  apiPort: parsedPort,
44
45
  logChannelId: parsedLogChannelId,
45
46
  workerTimeoutMs: parsedWorkerTimeout,
47
+ openaiApiKey: raw.OPENAI_API_KEY,
46
48
  get copilotModel() {
47
49
  return _copilotModel;
48
50
  },
package/dist/setup.js CHANGED
@@ -215,6 +215,20 @@ ${BOLD}╔═══════════════════════
215
215
  else {
216
216
  console.log(`\n${DIM} Skipping Google. You can always set it up later with: nzb setup${RESET}\n`);
217
217
  }
218
+ // ── Voice / Whisper Setup ────────────────────────────────
219
+ console.log(`${BOLD}━━━ Voice Message Setup (optional) ━━━${RESET}\n`);
220
+ console.log(`NZB can transcribe voice messages using OpenAI's Whisper API.`);
221
+ console.log(`You need an OpenAI API key from ${CYAN}https://platform.openai.com/api-keys${RESET}`);
222
+ console.log();
223
+ const existingOpenaiKey = existing.OPENAI_API_KEY;
224
+ const openaiKey = await ask(rl, ` OpenAI API Key ${existingOpenaiKey ? `${DIM}(Enter to keep existing)${RESET}` : `${DIM}(Enter to skip)${RESET}`}: `);
225
+ const finalOpenaiKey = openaiKey.trim() || existingOpenaiKey || "";
226
+ if (finalOpenaiKey) {
227
+ console.log(`\n${GREEN} ✓ Whisper transcription enabled${RESET}\n`);
228
+ }
229
+ else {
230
+ console.log(`\n${DIM} Skipping voice. Voice messages will be saved but not transcribed.${RESET}\n`);
231
+ }
218
232
  // ── Model picker ─────────────────────────────────────────
219
233
  console.log(`\n${BOLD}━━━ Default Model ━━━${RESET}\n`);
220
234
  console.log(`${DIM}Fetching available models from Copilot...${RESET}`);
@@ -241,6 +255,8 @@ ${BOLD}╔═══════════════════════
241
255
  lines.push(`AUTHORIZED_USER_ID=${userId}`);
242
256
  lines.push(`API_PORT=${apiPort}`);
243
257
  lines.push(`COPILOT_MODEL=${model}`);
258
+ if (finalOpenaiKey)
259
+ lines.push(`OPENAI_API_KEY=${finalOpenaiKey}`);
244
260
  writeFileSync(ENV_PATH, lines.join("\n") + "\n");
245
261
  // ── Done ─────────────────────────────────────────────────
246
262
  console.log(`
@@ -736,6 +736,110 @@ export function createBot() {
736
736
  });
737
737
  }
738
738
  });
739
+ // Handle voice messages — download, transcribe via Whisper, send to AI
740
+ bot.on("message:voice", async (ctx) => {
741
+ const chatId = ctx.chat.id;
742
+ const userMessageId = ctx.message.message_id;
743
+ const duration = ctx.message.voice.duration;
744
+ void logInfo(`🎤 Voice received: ${duration}s`);
745
+ try {
746
+ await ctx.react("👀");
747
+ }
748
+ catch { }
749
+ // Limit voice duration to 5 minutes
750
+ if (duration > 300) {
751
+ await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
752
+ return;
753
+ }
754
+ try {
755
+ const file = await ctx.api.getFile(ctx.message.voice.file_id);
756
+ const filePath = file.file_path;
757
+ if (!filePath) {
758
+ await ctx.reply("❌ Could not download voice.", { reply_parameters: { message_id: userMessageId } });
759
+ return;
760
+ }
761
+ const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
762
+ const { mkdtempSync, writeFileSync } = await import("fs");
763
+ const { join } = await import("path");
764
+ const { tmpdir } = await import("os");
765
+ const tmpDir = mkdtempSync(join(tmpdir(), "nzb-voice-"));
766
+ const ext = filePath.split(".").pop() || "oga";
767
+ const localPath = join(tmpDir, `voice.${ext}`);
768
+ const response = await fetch(url);
769
+ const buffer = Buffer.from(await response.arrayBuffer());
770
+ writeFileSync(localPath, buffer);
771
+ let prompt;
772
+ if (config.openaiApiKey) {
773
+ // Transcribe using OpenAI Whisper API
774
+ try {
775
+ const formData = new FormData();
776
+ formData.append("file", new Blob([buffer], { type: "audio/ogg" }), `voice.${ext}`);
777
+ formData.append("model", "whisper-1");
778
+ const whisperResp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
779
+ method: "POST",
780
+ headers: { Authorization: `Bearer ${config.openaiApiKey}` },
781
+ body: formData,
782
+ });
783
+ if (!whisperResp.ok) {
784
+ const errText = await whisperResp.text();
785
+ throw new Error(`Whisper API ${whisperResp.status}: ${errText.slice(0, 200)}`);
786
+ }
787
+ const result = (await whisperResp.json());
788
+ const transcript = result.text?.trim();
789
+ if (!transcript) {
790
+ prompt = `[User sent a voice message (${duration}s) but transcription was empty. File saved at: ${localPath}]`;
791
+ }
792
+ else {
793
+ prompt = `[Voice message transcribed (${duration}s)]: ${transcript}`;
794
+ }
795
+ }
796
+ catch (whisperErr) {
797
+ console.error("[nzb] Whisper transcription failed:", whisperErr instanceof Error ? whisperErr.message : whisperErr);
798
+ prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. Transcription failed: ${whisperErr instanceof Error ? whisperErr.message : String(whisperErr)}]`;
799
+ }
800
+ }
801
+ else {
802
+ 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]`;
803
+ }
804
+ sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
805
+ if (done) {
806
+ const formatted = toTelegramMarkdown(text);
807
+ const chunks = chunkMessage(formatted);
808
+ const fallbackChunks = chunkMessage(text);
809
+ void (async () => {
810
+ for (let i = 0; i < chunks.length; i++) {
811
+ if (i > 0)
812
+ await new Promise((r) => setTimeout(r, 300));
813
+ const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
814
+ try {
815
+ await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
816
+ parse_mode: "MarkdownV2",
817
+ reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
818
+ });
819
+ }
820
+ catch {
821
+ try {
822
+ await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
823
+ reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
824
+ });
825
+ }
826
+ catch { }
827
+ }
828
+ }
829
+ try {
830
+ await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
831
+ }
832
+ catch { }
833
+ })();
834
+ }
835
+ });
836
+ }
837
+ catch (err) {
838
+ await ctx.reply(`❌ Error processing voice: ${err instanceof Error ? err.message : String(err)}`, {
839
+ reply_parameters: { message_id: userMessageId },
840
+ });
841
+ }
842
+ });
739
843
  return bot;
740
844
  }
741
845
  export async function startBot() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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"