@iletai/nzb 1.7.0 → 1.7.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.
@@ -6,7 +6,7 @@ import { cancelCurrentMessage, getWorkers, sendToOrchestrator } from "../copilot
6
6
  import { listSkills, removeSkill } from "../copilot/skills.js";
7
7
  import { restartDaemon } from "../daemon.js";
8
8
  import { API_TOKEN_PATH, ensureNZBHome } from "../paths.js";
9
- import { searchMemories } from "../store/db.js";
9
+ import { searchMemories } from "../store/memory.js";
10
10
  import { sendPhoto } from "../telegram/bot.js";
11
11
  // Ensure token file exists (generate on first run)
12
12
  let apiToken = null;
@@ -26,9 +26,9 @@ catch (err) {
26
26
  }
27
27
  const app = express();
28
28
  app.use(express.json());
29
- // Bearer token authentication middleware (skip /status health check)
29
+ // Bearer token authentication middleware (skip /ping health check only)
30
30
  app.use((req, res, next) => {
31
- if (!apiToken || req.path === "/status")
31
+ if (!apiToken || req.path === "/ping")
32
32
  return next();
33
33
  const auth = req.headers.authorization;
34
34
  if (!auth || auth !== `Bearer ${apiToken}`) {
@@ -40,7 +40,11 @@ app.use((req, res, next) => {
40
40
  // Active SSE connections
41
41
  const sseClients = new Map();
42
42
  let connectionCounter = 0;
43
- // Health check
43
+ // Minimal unauthenticated health check — no internal details
44
+ app.get("/ping", (_req, res) => {
45
+ res.json({ status: "ok" });
46
+ });
47
+ // Authenticated status with worker details
44
48
  app.get("/status", (_req, res) => {
45
49
  res.json({
46
50
  status: "ok",
@@ -73,8 +77,23 @@ app.get("/stream", (req, res) => {
73
77
  sseClients.set(connectionId, res);
74
78
  // Heartbeat to keep connection alive
75
79
  const heartbeat = setInterval(() => {
76
- res.write(`:ping\n\n`);
80
+ if (res.writableEnded || res.closed) {
81
+ clearInterval(heartbeat);
82
+ sseClients.delete(connectionId);
83
+ return;
84
+ }
85
+ try {
86
+ res.write(`:ping\n\n`);
87
+ }
88
+ catch {
89
+ clearInterval(heartbeat);
90
+ sseClients.delete(connectionId);
91
+ }
77
92
  }, 20_000);
93
+ res.on("error", () => {
94
+ clearInterval(heartbeat);
95
+ sseClients.delete(connectionId);
96
+ });
78
97
  req.on("close", () => {
79
98
  clearInterval(heartbeat);
80
99
  sseClients.delete(connectionId);
@@ -175,7 +194,12 @@ app.post("/restart", (_req, res) => {
175
194
  app.post("/send-photo", async (req, res) => {
176
195
  const { photo, caption } = req.body;
177
196
  if (!photo || typeof photo !== "string") {
178
- res.status(400).json({ error: "Missing 'photo' (file path or URL) in request body" });
197
+ res.status(400).json({ error: "Missing 'photo' (file path or HTTPS URL) in request body" });
198
+ return;
199
+ }
200
+ // Basic input validation before passing to sendPhoto
201
+ if (photo.startsWith("http://")) {
202
+ res.status(400).json({ error: "Only HTTPS URLs are allowed for photos" });
179
203
  return;
180
204
  }
181
205
  try {
@@ -187,6 +211,13 @@ app.post("/send-photo", async (req, res) => {
187
211
  res.status(500).json({ error: msg });
188
212
  }
189
213
  });
214
+ // Global error handler — catch unhandled Express errors
215
+ app.use((err, _req, res, _next) => {
216
+ console.error("[nzb] Express error:", err.message);
217
+ if (!res.headersSent) {
218
+ res.status(500).json({ error: "Internal server error" });
219
+ }
220
+ });
190
221
  export function startApiServer() {
191
222
  return new Promise((resolve, reject) => {
192
223
  const server = app.listen(config.apiPort, "127.0.0.1", () => {
package/dist/cli.js CHANGED
@@ -28,6 +28,7 @@ function getVersion() {
28
28
  return pkg.version || "0.0.0";
29
29
  }
30
30
  catch {
31
+ // Expected: package.json may not be found in dev/bundled environments
31
32
  return "0.0.0";
32
33
  }
33
34
  }
package/dist/config.js CHANGED
@@ -38,6 +38,14 @@ if (!Number.isInteger(parsedWorkerTimeout) || parsedWorkerTimeout <= 0) {
38
38
  }
39
39
  const parsedLogChannelId = raw.LOG_CHANNEL_ID ? raw.LOG_CHANNEL_ID.trim() : undefined;
40
40
  export const DEFAULT_MODEL = "claude-sonnet-4.6";
41
+ function validateEnum(value, validValues, defaultValue, name) {
42
+ if (!value)
43
+ return defaultValue;
44
+ if (validValues.includes(value))
45
+ return value;
46
+ console.log(`[nzb] Invalid ${name} value "${value}", using default "${defaultValue}"`);
47
+ return defaultValue;
48
+ }
41
49
  let _copilotModel = raw.COPILOT_MODEL || DEFAULT_MODEL;
42
50
  export const config = {
43
51
  telegramBotToken: raw.TELEGRAM_BOT_TOKEN,
@@ -65,15 +73,15 @@ export const config = {
65
73
  process.env.SHOW_REASONING = value ? "true" : "false";
66
74
  },
67
75
  /** Usage display mode: off | tokens | full */
68
- usageMode: (process.env.USAGE_MODE || "off"),
76
+ usageMode: validateEnum(process.env.USAGE_MODE, ["off", "tokens", "full"], "off", "USAGE_MODE"),
69
77
  /** Verbose mode: when on, instructs the AI to be more detailed */
70
78
  verboseMode: process.env.VERBOSE_MODE === "true",
71
79
  /** Thinking level: off | low | medium | high */
72
- thinkingLevel: (process.env.THINKING_LEVEL || "off"),
80
+ thinkingLevel: validateEnum(process.env.THINKING_LEVEL, ["off", "low", "medium", "high"], "off", "THINKING_LEVEL"),
73
81
  /** Group chat: when true, bot only responds when mentioned in groups */
74
82
  groupMentionOnly: process.env.GROUP_MENTION_ONLY !== "false",
75
83
  /** Reasoning effort: low | medium | high */
76
- reasoningEffort: (process.env.REASONING_EFFORT || "medium"),
84
+ reasoningEffort: validateEnum(process.env.REASONING_EFFORT, ["low", "medium", "high"], "medium", "REASONING_EFFORT"),
77
85
  };
78
86
  /** Persist an env variable to ~/.nzb/.env */
79
87
  export function persistEnvVar(key, value) {
@@ -94,6 +102,7 @@ export function persistEnvVar(key, value) {
94
102
  writeFileSync(ENV_PATH, updated.join("\n"));
95
103
  }
96
104
  catch {
105
+ // Expected: .env file may not exist yet on first run
97
106
  writeFileSync(ENV_PATH, `${key}=${value}\n`);
98
107
  }
99
108
  }
@@ -1,4 +1,5 @@
1
1
  import { CopilotClient } from "@github/copilot-sdk";
2
+ import { withTimeout } from "../utils.js";
2
3
  let client;
3
4
  /** Coalesces concurrent resetClient() calls into a single reset operation. */
4
5
  let pendingResetPromise;
@@ -7,7 +8,7 @@ export async function getClient() {
7
8
  client = new CopilotClient({
8
9
  autoStart: true,
9
10
  });
10
- await client.start();
11
+ await withTimeout(client.start(), 30_000, "client.start()");
11
12
  }
12
13
  return client;
13
14
  }
@@ -16,27 +17,27 @@ export async function resetClient() {
16
17
  if (pendingResetPromise)
17
18
  return pendingResetPromise;
18
19
  pendingResetPromise = (async () => {
19
- if (client) {
20
- try {
21
- await client.stop();
20
+ try {
21
+ if (client) {
22
+ try {
23
+ await withTimeout(client.stop(), 10_000, "client.stop()");
24
+ }
25
+ catch (err) {
26
+ console.error("[nzb] Error stopping client during reset:", err);
27
+ }
28
+ client = undefined;
22
29
  }
23
- catch {
24
- /* best-effort */
25
- }
26
- client = undefined;
30
+ return await getClient();
31
+ }
32
+ finally {
33
+ pendingResetPromise = undefined;
27
34
  }
28
- return getClient();
29
35
  })();
30
- try {
31
- return await pendingResetPromise;
32
- }
33
- finally {
34
- pendingResetPromise = undefined;
35
- }
36
+ return pendingResetPromise;
36
37
  }
37
38
  export async function stopClient() {
38
39
  if (client) {
39
- await client.stop();
40
+ await withTimeout(client.stop(), 10_000, "client.stop()");
40
41
  client = undefined;
41
42
  }
42
43
  }
@@ -10,6 +10,7 @@ const isWSL = (() => {
10
10
  return readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
11
11
  }
12
12
  catch {
13
+ // Expected: /proc/version may not exist on non-Linux systems
13
14
  return false;
14
15
  }
15
16
  })();
@@ -86,6 +87,7 @@ export function loadMcpConfig() {
86
87
  return cachedConfig;
87
88
  }
88
89
  catch {
90
+ // Expected: config file may not exist or be malformed
89
91
  cachedConfig = {};
90
92
  return cachedConfig;
91
93
  }