@iletai/nzb 1.5.2 → 1.5.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/daemon.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { spawn } from "child_process";
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
3
  import { broadcastToSSE, startApiServer } from "./api/server.js";
3
4
  import { config } from "./config.js";
4
5
  import { getClient, stopClient } from "./copilot/client.js";
5
6
  import { getWorkers, initOrchestrator, setMessageLogger, setProactiveNotify, setWorkerNotify, stopHealthCheck, } from "./copilot/orchestrator.js";
7
+ import { PID_FILE_PATH } from "./paths.js";
6
8
  import { closeDb, getDb } from "./store/db.js";
7
9
  import { createBot, sendProactiveMessage, sendWorkerNotification, startBot, stopBot } from "./telegram/bot.js";
8
10
  import { checkForUpdate } from "./update.js";
@@ -14,8 +16,64 @@ function truncate(text, max = 200) {
14
16
  const oneLine = text.replace(/\n/g, " ").trim();
15
17
  return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
16
18
  }
19
+ /**
20
+ * Check if a process with the given PID is alive.
21
+ * Sends signal 0 which doesn't kill but checks existence.
22
+ */
23
+ function isProcessAlive(pid) {
24
+ try {
25
+ process.kill(pid, 0);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Acquire a PID lock file. Prevents multiple daemon instances.
34
+ * Returns true if lock acquired, false if another instance is running.
35
+ */
36
+ function acquirePidLock() {
37
+ if (existsSync(PID_FILE_PATH)) {
38
+ try {
39
+ const existingPid = parseInt(readFileSync(PID_FILE_PATH, "utf-8").trim(), 10);
40
+ if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
41
+ console.error(`[nzb] Another NZB instance is already running (PID ${existingPid}).`);
42
+ console.error(`[nzb] Stop it first, or remove ${PID_FILE_PATH} if the process is stale.`);
43
+ return false;
44
+ }
45
+ // Stale PID file — process is dead, remove it
46
+ console.log(`[nzb] Removed stale PID file (old PID ${existingPid} is no longer running).`);
47
+ }
48
+ catch {
49
+ // Corrupt PID file — remove it
50
+ }
51
+ unlinkSync(PID_FILE_PATH);
52
+ }
53
+ writeFileSync(PID_FILE_PATH, String(process.pid), { mode: 0o644 });
54
+ return true;
55
+ }
56
+ /** Release the PID lock file. */
57
+ function releasePidLock() {
58
+ try {
59
+ if (existsSync(PID_FILE_PATH)) {
60
+ const pid = parseInt(readFileSync(PID_FILE_PATH, "utf-8").trim(), 10);
61
+ // Only remove if it's our PID (in case a new instance took over)
62
+ if (pid === process.pid) {
63
+ unlinkSync(PID_FILE_PATH);
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ /* best effort */
69
+ }
70
+ }
17
71
  async function main() {
18
72
  console.log("[nzb] Starting NZB daemon...");
73
+ // Single-instance guard
74
+ if (!acquirePidLock()) {
75
+ process.exit(1);
76
+ }
19
77
  if (config.selfEditEnabled) {
20
78
  console.log("[nzb] Warning: Self-edit mode enabled — NZB can modify his own source code");
21
79
  }
@@ -153,6 +211,7 @@ async function shutdown() {
153
211
  /* best effort */
154
212
  }
155
213
  closeDb();
214
+ releasePidLock();
156
215
  console.log("[nzb] Goodbye.");
157
216
  process.exit(0);
158
217
  }
@@ -184,6 +243,7 @@ export async function restartDaemon() {
184
243
  /* best effort */
185
244
  }
186
245
  closeDb();
246
+ releasePidLock();
187
247
  // Spawn a detached replacement process with the same args (include execArgv for tsx/loaders)
188
248
  const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
189
249
  detached: true,
package/dist/paths.js CHANGED
@@ -17,6 +17,8 @@ export const HISTORY_PATH = join(NZB_HOME, "tui_history");
17
17
  export const TUI_DEBUG_LOG_PATH = join(NZB_HOME, "tui-debug.log");
18
18
  /** Path to the API bearer token file */
19
19
  export const API_TOKEN_PATH = join(NZB_HOME, "api-token");
20
+ /** Path to the PID lock file for single-instance enforcement */
21
+ export const PID_FILE_PATH = join(NZB_HOME, "nzb.pid");
20
22
  /** Ensure ~/.nzb/ exists */
21
23
  export function ensureNZBHome() {
22
24
  mkdirSync(NZB_HOME, { recursive: true });
@@ -33,11 +33,29 @@ const TIMEOUT_PRESETS = [
33
33
  { ms: 3_600_000, label: "60min" },
34
34
  { ms: 7_200_000, label: "120min" },
35
35
  ];
36
- const MODEL_PRESETS = [
37
- "claude-sonnet-4-20250514",
38
- "claude-haiku-4-20250414",
39
- "claude-opus-4-20250115",
40
- ];
36
+ // Dynamic model list — fetched from Copilot SDK, cached for 5 minutes
37
+ let cachedModels;
38
+ let cachedModelsAt = 0;
39
+ const MODEL_CACHE_TTL = 5 * 60_000;
40
+ async function getAvailableModels() {
41
+ if (cachedModels && Date.now() - cachedModelsAt < MODEL_CACHE_TTL) {
42
+ return cachedModels;
43
+ }
44
+ try {
45
+ const { getClient } = await import("../copilot/client.js");
46
+ const client = await getClient();
47
+ const models = await client.listModels();
48
+ if (models.length > 0) {
49
+ cachedModels = models.map((m) => m.id);
50
+ cachedModelsAt = Date.now();
51
+ return cachedModels;
52
+ }
53
+ }
54
+ catch {
55
+ /* fall through to fallback */
56
+ }
57
+ return cachedModels ?? [config.copilotModel];
58
+ }
41
59
  function getTimeoutLabel() {
42
60
  const preset = TIMEOUT_PRESETS.find((p) => p.ms === config.workerTimeoutMs);
43
61
  return preset ? preset.label : `${Math.round(config.workerTimeoutMs / 60_000)}min`;
@@ -62,8 +80,13 @@ const settingsMenu = new Menu("settings-menu")
62
80
  })
63
81
  .row()
64
82
  .text(() => `🤖 ${config.copilotModel}`, async (ctx) => {
65
- const idx = MODEL_PRESETS.indexOf(config.copilotModel);
66
- const next = MODEL_PRESETS[(idx + 1) % MODEL_PRESETS.length];
83
+ const models = await getAvailableModels();
84
+ if (models.length === 0) {
85
+ await ctx.answerCallbackQuery("No models available");
86
+ return;
87
+ }
88
+ const idx = models.indexOf(config.copilotModel);
89
+ const next = models[(idx + 1) % models.length];
67
90
  config.copilotModel = next;
68
91
  persistModel(next);
69
92
  ctx.menu.update();
@@ -244,7 +267,8 @@ export function createBot() {
244
267
  "⚡ Breakthrough Features:\n" +
245
268
  "• @bot query — Use me inline in any chat!\n" +
246
269
  "• React to any message to trigger AI:\n" +
247
- getReactionHelpText() + "\n" +
270
+ getReactionHelpText() +
271
+ "\n" +
248
272
  "• Smart suggestions appear after each response", { reply_markup: mainMenu }));
249
273
  bot.command("cancel", async (ctx) => {
250
274
  const cancelled = await cancelCurrentMessage();
@@ -771,15 +795,22 @@ export async function startBot() {
771
795
  void logInfo(`🚀 NZB v${process.env.npm_package_version || "?"} started (model: ${config.copilotModel})`);
772
796
  },
773
797
  })
774
- .catch((err) => {
798
+ .catch(async (err) => {
775
799
  if (err?.error_code === 401) {
776
800
  console.error("[nzb] Warning: Telegram bot token is invalid or expired. Run 'nzb setup' and re-enter your bot token from @BotFather.");
801
+ return; // Unrecoverable — don't retry
777
802
  }
778
- else if (err?.error_code === 409) {
779
- console.error("[nzb] Warning: Another bot instance is already running with this token. Stop the other instance first.");
803
+ if (err?.error_code === 409) {
804
+ console.error("[nzb] Warning: Telegram polling conflict (409). Restarting polling in 5 seconds...");
780
805
  }
781
806
  else {
782
- console.error("[nzb] Error: Telegram bot failed to start:", err?.message || err);
807
+ console.error("[nzb] Error: Telegram polling stopped:", err?.message || err, "— restarting in 5 seconds...");
808
+ }
809
+ // Auto-restart polling after a delay
810
+ await new Promise((r) => setTimeout(r, 5000));
811
+ if (bot) {
812
+ console.log("[nzb] Re-starting Telegram polling...");
813
+ startBot().catch((e) => console.error("[nzb] Failed to re-start Telegram polling:", e));
783
814
  }
784
815
  });
785
816
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.5.2",
3
+ "version": "1.5.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"
@@ -67,4 +67,4 @@
67
67
  "typescript": "^5.9.3",
68
68
  "vitest": "^4.1.0"
69
69
  }
70
- }
70
+ }