@iletai/nzb 1.1.7 → 1.2.0

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
@@ -12,6 +12,7 @@ const configSchema = z.object({
12
12
  COPILOT_MODEL: z.string().optional(),
13
13
  WORKER_TIMEOUT: z.string().optional(),
14
14
  SHOW_REASONING: z.string().optional(),
15
+ LOG_CHANNEL_ID: z.string().optional(),
15
16
  NODE_EXTRA_CA_CERTS: z.string().optional(),
16
17
  });
17
18
  const raw = configSchema.parse(process.env);
@@ -33,12 +34,14 @@ const parsedWorkerTimeout = raw.WORKER_TIMEOUT ? Number(raw.WORKER_TIMEOUT) : DE
33
34
  if (!Number.isInteger(parsedWorkerTimeout) || parsedWorkerTimeout <= 0) {
34
35
  throw new Error(`WORKER_TIMEOUT must be a positive integer (ms), got: "${raw.WORKER_TIMEOUT}"`);
35
36
  }
37
+ const parsedLogChannelId = raw.LOG_CHANNEL_ID ? raw.LOG_CHANNEL_ID.trim() : undefined;
36
38
  export const DEFAULT_MODEL = "claude-sonnet-4.6";
37
39
  let _copilotModel = raw.COPILOT_MODEL || DEFAULT_MODEL;
38
40
  export const config = {
39
41
  telegramBotToken: raw.TELEGRAM_BOT_TOKEN,
40
42
  authorizedUserId: parsedUserId,
41
43
  apiPort: parsedPort,
44
+ logChannelId: parsedLogChannelId,
42
45
  workerTimeoutMs: parsedWorkerTimeout,
43
46
  get copilotModel() {
44
47
  return _copilotModel;
@@ -1,4 +1,3 @@
1
- import { appendFileSync } from "fs";
2
1
  import { Bot, InlineKeyboard } from "grammy";
3
2
  import { Agent as HttpsAgent } from "https";
4
3
  import { config, persistEnvVar, persistModel } from "../config.js";
@@ -7,6 +6,7 @@ import { listSkills } from "../copilot/skills.js";
7
6
  import { restartDaemon } from "../daemon.js";
8
7
  import { searchMemories } from "../store/db.js";
9
8
  import { chunkMessage, formatToolSummaryExpandable, toTelegramMarkdown } from "./formatter.js";
9
+ import { initLogChannel, logDebug, logError, logInfo } from "./log-channel.js";
10
10
  let bot;
11
11
  const startedAt = Date.now();
12
12
  // Inline keyboard menu for quick actions
@@ -41,6 +41,7 @@ export function createBot() {
41
41
  },
42
42
  });
43
43
  console.log("[nzb] Telegram bot using direct HTTPS agent (proxy bypass)");
44
+ initLogChannel(bot);
44
45
  // Auth middleware — only allow the authorized user
45
46
  bot.use(async (ctx, next) => {
46
47
  if (config.authorizedUserId !== undefined && ctx.from?.id !== config.authorizedUserId) {
@@ -244,6 +245,8 @@ export function createBot() {
244
245
  const chatId = ctx.chat.id;
245
246
  const userMessageId = ctx.message.message_id;
246
247
  const replyParams = { message_id: userMessageId };
248
+ const msgPreview = ctx.message.text.length > 80 ? ctx.message.text.slice(0, 80) + "…" : ctx.message.text;
249
+ void logInfo(`📩 Message: ${msgPreview}`);
247
250
  // Typing indicator — keeps sending "typing" action every 4s until the final
248
251
  // response is delivered. We use bot.api directly for reliability, and await the
249
252
  // first call so the user sees typing immediately before any async work begins.
@@ -325,12 +328,9 @@ export function createBot() {
325
328
  .catch(() => { });
326
329
  };
327
330
  const onToolEvent = (event) => {
328
- try {
329
- appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} BOT ${event.type} ${event.toolName}\n`);
330
- }
331
- catch { }
332
331
  console.log(`[nzb] Bot received tool event: ${event.type} ${event.toolName}`);
333
332
  if (event.type === "tool_start") {
333
+ void logDebug(`🔧 Tool start: ${event.toolName}`);
334
334
  currentToolName = event.toolName;
335
335
  toolHistory.push({ name: event.toolName, startTime: Date.now() });
336
336
  const existingText = lastEditedText.replace(/^🔧 .*\n\n/, "");
@@ -374,11 +374,14 @@ export function createBot() {
374
374
  if (done) {
375
375
  finalized = true;
376
376
  stopTyping();
377
+ const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
378
+ void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
377
379
  // Wait for in-flight edits to finish before sending the final response
378
380
  void editChain.then(async () => {
379
381
  // Format error messages with a distinct visual
380
382
  const isError = text.startsWith("Error:");
381
383
  if (isError) {
384
+ void logError(`Response error: ${text.slice(0, 200)}`);
382
385
  const errorText = `⚠️ ${text}`;
383
386
  if (placeholderMsgId) {
384
387
  try {
@@ -412,21 +415,9 @@ export function createBot() {
412
415
  }
413
416
  const formatted = toTelegramMarkdown(textWithMeta);
414
417
  let fullFormatted = formatted;
415
- try {
416
- appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} FINAL showReasoning=${config.showReasoning} toolHistory=${toolHistory.length}\n`);
417
- }
418
- catch { }
419
418
  if (config.showReasoning && toolHistory.length > 0) {
420
419
  const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs })));
421
420
  fullFormatted += expandable;
422
- try {
423
- appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} EXPANDABLE=${JSON.stringify(expandable)}\n`);
424
- }
425
- catch { }
426
- try {
427
- appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} FULL_LAST200=${JSON.stringify(fullFormatted.slice(-200))}\n`);
428
- }
429
- catch { }
430
421
  }
431
422
  const chunks = chunkMessage(fullFormatted);
432
423
  const fallbackChunks = chunkMessage(textWithMeta);
@@ -446,18 +437,10 @@ export function createBot() {
446
437
  }
447
438
  }
448
439
  }
449
- // Multi-chunk or no placeholder: delete placeholder and send chunks
450
- if (placeholderMsgId) {
451
- try {
452
- await bot.api.deleteMessage(chatId, placeholderMsgId);
453
- }
454
- catch {
455
- /* ignore */
456
- }
457
- }
440
+ // Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
458
441
  const totalChunks = chunks.length;
459
442
  const sendChunk = async (chunk, fallback, index) => {
460
- const isFirst = index === 0;
443
+ const isFirst = index === 0 && !placeholderMsgId;
461
444
  // Pagination header for multi-chunk messages
462
445
  const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
463
446
  const opts = isFirst
@@ -467,12 +450,14 @@ export function createBot() {
467
450
  .reply(pageTag + chunk, opts)
468
451
  .catch(() => ctx.reply(pageTag + fallback, isFirst ? { reply_parameters: replyParams } : {}));
469
452
  };
453
+ let sendSucceeded = false;
470
454
  try {
471
455
  for (let i = 0; i < chunks.length; i++) {
472
456
  if (i > 0)
473
457
  await new Promise((r) => setTimeout(r, 300));
474
458
  await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
475
459
  }
460
+ sendSucceeded = true;
476
461
  }
477
462
  catch {
478
463
  try {
@@ -482,11 +467,21 @@ export function createBot() {
482
467
  const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
483
468
  await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
484
469
  }
470
+ sendSucceeded = true;
485
471
  }
486
472
  catch {
487
473
  /* nothing more we can do */
488
474
  }
489
475
  }
476
+ // Only delete placeholder AFTER new messages sent successfully
477
+ if (placeholderMsgId && sendSucceeded) {
478
+ try {
479
+ await bot.api.deleteMessage(chatId, placeholderMsgId);
480
+ }
481
+ catch {
482
+ /* ignore — placeholder stays but user has the real message */
483
+ }
484
+ }
490
485
  });
491
486
  }
492
487
  else {
@@ -536,7 +531,10 @@ export async function startBot() {
536
531
  }
537
532
  bot
538
533
  .start({
539
- onStart: () => console.log("[nzb] Telegram bot connected"),
534
+ onStart: () => {
535
+ console.log("[nzb] Telegram bot connected");
536
+ void logInfo(`🚀 NZB v${process.env.npm_package_version || "?"} started (model: ${config.copilotModel})`);
537
+ },
540
538
  })
541
539
  .catch((err) => {
542
540
  if (err?.error_code === 401) {
@@ -0,0 +1,35 @@
1
+ import { config } from "../config.js";
2
+ let botRef;
3
+ /** Initialize the log channel with a bot reference */
4
+ export function initLogChannel(bot) {
5
+ botRef = bot;
6
+ }
7
+ const ICONS = {
8
+ info: "ℹ️",
9
+ warn: "⚠️",
10
+ error: "🔴",
11
+ debug: "🔍",
12
+ };
13
+ /** Send a log message to the configured Telegram channel */
14
+ export async function sendLog(level, message) {
15
+ if (!botRef || !config.logChannelId)
16
+ return;
17
+ const icon = ICONS[level];
18
+ const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
19
+ const text = `${icon} <b>[${level.toUpperCase()}]</b> <code>${timestamp}</code>\n${escapeHtml(message)}`;
20
+ try {
21
+ await botRef.api.sendMessage(config.logChannelId, text, { parse_mode: "HTML" });
22
+ }
23
+ catch {
24
+ // best-effort — don't crash if log channel is unreachable
25
+ }
26
+ }
27
+ /** Convenience wrappers */
28
+ export const logInfo = (msg) => sendLog("info", msg);
29
+ export const logWarn = (msg) => sendLog("warn", msg);
30
+ export const logError = (msg) => sendLog("error", msg);
31
+ export const logDebug = (msg) => sendLog("debug", msg);
32
+ function escapeHtml(text) {
33
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
34
+ }
35
+ //# sourceMappingURL=log-channel.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
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"
@@ -61,4 +61,4 @@
61
61
  "tsx": "^4.21.0",
62
62
  "typescript": "^5.9.3"
63
63
  }
64
- }
64
+ }