@iletai/nzb 1.9.0 → 1.9.1

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.
@@ -262,7 +262,8 @@ async function generateAndSendVocabAudio(word) {
262
262
  }
263
263
  catch { /* ignore */ }
264
264
  // Generate TTS with macOS say
265
- execSync(`say -v Samantha -o "${aiffPath}" "${word.replace(/"/g, '\\"')}"`, {
265
+ const safeWord = word.replace(/["`$\\]/g, "");
266
+ execSync(`say -v Samantha -o "${aiffPath}" "${safeWord}"`, {
266
267
  timeout: 10_000,
267
268
  });
268
269
  if (!existsSync(aiffPath)) {
@@ -275,16 +276,21 @@ async function generateAndSendVocabAudio(word) {
275
276
  if (!existsSync(m4aPath)) {
276
277
  throw new Error("Audio conversion failed: m4a file not created");
277
278
  }
279
+ // Resolve symlink path (macOS /tmp → /private/tmp) for grammy InputFile
280
+ const { realpathSync: realpath } = await import("fs");
281
+ const resolvedPath = realpath(m4aPath);
282
+ console.log(`[nzb] Vocab TTS: sending voice for "${word}" (${resolvedPath}, ${statSync(resolvedPath).size} bytes)`);
278
283
  // Send voice via Telegram
279
284
  const { sendVoice } = await import("../telegram/bot.js");
280
- await sendVoice(m4aPath, `🔊 ${word}`);
285
+ await sendVoice(resolvedPath, `🔊 ${word}`);
286
+ console.log(`[nzb] Vocab TTS: voice sent successfully for "${word}"`);
281
287
  // Clean up temp files
282
288
  try {
283
289
  unlinkSync(aiffPath);
284
290
  }
285
291
  catch { /* ignore */ }
286
292
  try {
287
- unlinkSync(m4aPath);
293
+ unlinkSync(resolvedPath);
288
294
  }
289
295
  catch { /* ignore */ }
290
296
  }
@@ -13,6 +13,13 @@ const SCHEDULE_PRESETS = [
13
13
  { label: "Every 6 hours", cron: "0 */6 * * *" },
14
14
  { label: "Every day at 8AM", cron: "0 8 * * *" },
15
15
  ];
16
+ // ── Model presets ────────────────────────────────────────────────────
17
+ const MODEL_PRESETS = [
18
+ { label: "⚡ Haiku (fast & cheap)", id: "claude-haiku-4.5", short: "haiku" },
19
+ { label: "⚖️ Sonnet (balanced)", id: "claude-sonnet-4", short: "sonnet" },
20
+ { label: "🧠 Opus (most capable)", id: "claude-opus-4.6", short: "opus" },
21
+ { label: "⚡ GPT-4.1 (fast alt)", id: "gpt-4.1", short: "gpt-4.1" },
22
+ ];
16
23
  // In-memory state for users awaiting custom cron expression input
17
24
  const pendingCustomSchedule = new Map();
18
25
  /** Check if a user is awaiting custom cron input and return the job ID. */
@@ -39,6 +46,7 @@ function buildJobListKeyboard(jobs, page, totalPages) {
39
46
  const toggleLabel = job.enabled ? "⏸" : "▶️";
40
47
  kb.text(`${toggleLabel} ${job.name}`, `cron:toggle:${job.id}`)
41
48
  .text("⏱", `cron:schedule:${job.id}`)
49
+ .text("🤖", `cron:model:${job.id}`)
42
50
  .text("▶ Run", `cron:trigger:${job.id}`)
43
51
  .text("📊", `cron:history:${job.id}`)
44
52
  .text("🗑", `cron:delete:${job.id}`)
@@ -64,6 +72,18 @@ function buildScheduleKeyboard(jobId) {
64
72
  kb.text("🔙 Back to list", "cron:list");
65
73
  return kb;
66
74
  }
75
+ function buildModelKeyboard(jobId, currentModel) {
76
+ const kb = new InlineKeyboard();
77
+ for (let i = 0; i < MODEL_PRESETS.length; i++) {
78
+ const preset = MODEL_PRESETS[i];
79
+ const current = currentModel === preset.id ? " ✓" : "";
80
+ kb.text(`${preset.label}${current}`, `cron:setmodel:${jobId}:${i}`).row();
81
+ }
82
+ const noOverride = !currentModel ? " ✓" : "";
83
+ kb.text(`🚫 No model override (use default)${noOverride}`, `cron:setmodel:${jobId}:none`).row();
84
+ kb.text("🔙 Back to list", "cron:list");
85
+ return kb;
86
+ }
67
87
  /** Apply a schedule change: update DB and reschedule. */
68
88
  function applyScheduleChange(jobId, cronExpression) {
69
89
  const updated = updateCronJob(jobId, { cronExpression });
@@ -88,8 +108,11 @@ function formatJobLine(job, index) {
88
108
  minute: "2-digit",
89
109
  })
90
110
  : "—";
111
+ const modelTag = job.model
112
+ ? ` | 🤖 ${MODEL_PRESETS.find((p) => p.id === job.model)?.short ?? job.model}`
113
+ : "";
91
114
  return (`${index}. ${status} ${job.name}\n` +
92
- ` ⏰ ${job.cronExpression} | 🏷 ${job.taskType}\n` +
115
+ ` ⏰ ${job.cronExpression} | 🏷 ${job.taskType}${modelTag}\n` +
93
116
  ` 📅 Next: ${nextRun}`);
94
117
  }
95
118
  function buildJobListText(jobs, page, totalPages) {
@@ -437,6 +460,74 @@ export function registerCronHandlers(bot) {
437
460
  }
438
461
  }
439
462
  });
463
+ // Model selection — show model presets for a job
464
+ bot.callbackQuery(/^cron:model:(.+)$/, async (ctx) => {
465
+ try {
466
+ const jobId = ctx.match[1];
467
+ const job = getCronJob(jobId);
468
+ if (!job) {
469
+ await ctx.answerCallbackQuery({ text: "Job not found", show_alert: true });
470
+ return;
471
+ }
472
+ const currentLabel = job.model
473
+ ? MODEL_PRESETS.find((p) => p.id === job.model)?.label ?? job.model
474
+ : "Default (no override)";
475
+ const text = `🤖 Change Model: ${job.name}\n\n` +
476
+ `Current: ${currentLabel}\n\n` +
477
+ "Select a model for this job:";
478
+ await ctx.answerCallbackQuery();
479
+ await ctx.editMessageText(text, {
480
+ reply_markup: buildModelKeyboard(job.id, job.model),
481
+ });
482
+ }
483
+ catch (err) {
484
+ if (!isMessageNotModifiedError(err)) {
485
+ console.error("[nzb] Cron model menu error:", err instanceof Error ? err.message : err);
486
+ await ctx
487
+ .answerCallbackQuery({ text: "Error loading model options", show_alert: true })
488
+ .catch(() => { });
489
+ }
490
+ }
491
+ });
492
+ // Set model from preset or clear override
493
+ bot.callbackQuery(/^cron:setmodel:(.+):(none|\d+)$/, async (ctx) => {
494
+ try {
495
+ const jobId = ctx.match[1];
496
+ const selection = ctx.match[2];
497
+ const job = getCronJob(jobId);
498
+ if (!job) {
499
+ await ctx.answerCallbackQuery({ text: "Job not found", show_alert: true });
500
+ return;
501
+ }
502
+ let newModel;
503
+ let label;
504
+ if (selection === "none") {
505
+ newModel = null;
506
+ label = "default";
507
+ }
508
+ else {
509
+ const presetIndex = parseInt(selection, 10);
510
+ const preset = MODEL_PRESETS[presetIndex];
511
+ if (!preset) {
512
+ await ctx.answerCallbackQuery({ text: "Invalid model", show_alert: true });
513
+ return;
514
+ }
515
+ newModel = preset.id;
516
+ label = preset.short;
517
+ }
518
+ updateCronJob(jobId, { model: newModel });
519
+ await ctx.answerCallbackQuery(`Model → ${label}`);
520
+ await showCronList(ctx, 0, true);
521
+ }
522
+ catch (err) {
523
+ if (!isMessageNotModifiedError(err)) {
524
+ console.error("[nzb] Cron set model error:", err instanceof Error ? err.message : err);
525
+ await ctx
526
+ .answerCallbackQuery({ text: "Error updating model", show_alert: true })
527
+ .catch(() => { });
528
+ }
529
+ }
530
+ });
440
531
  // Custom schedule — prompt user to type cron expression
441
532
  bot.callbackQuery(/^cron:customsched:(.+)$/, async (ctx) => {
442
533
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
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"