@iletai/nzb 1.6.0 → 1.6.2

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
@@ -63,6 +63,14 @@ export const config = {
63
63
  set showReasoning(value) {
64
64
  process.env.SHOW_REASONING = value ? "true" : "false";
65
65
  },
66
+ /** Usage display mode: off | tokens | full */
67
+ usageMode: (process.env.USAGE_MODE || "off"),
68
+ /** Verbose mode: when on, instructs the AI to be more detailed */
69
+ verboseMode: process.env.VERBOSE_MODE === "true",
70
+ /** Thinking level: off | low | medium | high */
71
+ thinkingLevel: (process.env.THINKING_LEVEL || "off"),
72
+ /** Group chat: when true, bot only responds when mentioned in groups */
73
+ groupMentionOnly: process.env.GROUP_MENTION_ONLY !== "false",
66
74
  };
67
75
  /** Persist an env variable to ~/.nzb/.env */
68
76
  export function persistEnvVar(key, value) {
@@ -31,6 +31,8 @@ let healthCheckTimer;
31
31
  let orchestratorSession;
32
32
  // Coalesces concurrent ensureOrchestratorSession calls
33
33
  let sessionCreatePromise;
34
+ // Tracks in-flight context recovery injection so we don't race with real messages
35
+ let recoveryInjectionPromise;
34
36
  const messageQueue = [];
35
37
  let processing = false;
36
38
  let currentCallback;
@@ -244,16 +246,22 @@ async function createOrResumeSession() {
244
246
  setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
245
247
  console.log(`[nzb] Created orchestrator session ${session.sessionId.slice(0, 8)}…`);
246
248
  // Recover conversation context if available (session was lost, not first run)
247
- // Fire-and-forget: don't block the first real message behind recovery injection
249
+ // Runs concurrently but is awaited before any real message is sent on the session
248
250
  const recentHistory = getRecentConversation(10);
249
251
  if (recentHistory) {
250
- console.log(`[nzb] Injecting recent conversation context into new session (non-blocking)`);
251
- session
252
+ console.log(`[nzb] Injecting recent conversation context into new session`);
253
+ recoveryInjectionPromise = session
252
254
  .sendAndWait({
253
255
  prompt: `[System: Session recovered] Your previous session was lost. Here's the recent conversation for context — do NOT respond to these messages, just absorb the context silently:\n\n${recentHistory}\n\n(End of recovery context. Wait for the next real message.)`,
254
256
  }, 20_000)
257
+ .then(() => {
258
+ console.log(`[nzb] Context recovery injection completed`);
259
+ })
255
260
  .catch((err) => {
256
261
  console.log(`[nzb] Context recovery injection failed (non-fatal): ${err instanceof Error ? err.message : err}`);
262
+ })
263
+ .finally(() => {
264
+ recoveryInjectionPromise = undefined;
257
265
  });
258
266
  }
259
267
  return session;
@@ -291,6 +299,11 @@ export async function initOrchestrator(client) {
291
299
  /** Send a prompt on the persistent session, return the response. */
292
300
  async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
293
301
  const session = await ensureOrchestratorSession();
302
+ // Wait for any in-flight context recovery injection to finish before sending
303
+ if (recoveryInjectionPromise) {
304
+ console.log(`[nzb] Waiting for context recovery injection to complete before sending…`);
305
+ await recoveryInjectionPromise;
306
+ }
294
307
  currentCallback = callback;
295
308
  let accumulated = "";
296
309
  let toolCallExecuted = false;
@@ -408,6 +421,17 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
408
421
  if (freshMemory) {
409
422
  taggedPrompt = `<reminder>\n${freshMemory}\n</reminder>\n\n${taggedPrompt}`;
410
423
  }
424
+ // Inject thinking level and verbose mode hints
425
+ const hints = [];
426
+ if (config.thinkingLevel !== "off") {
427
+ hints.push(`[Thinking level: ${config.thinkingLevel} — reason through this at ${config.thinkingLevel} depth]`);
428
+ }
429
+ if (config.verboseMode) {
430
+ hints.push("[Verbose mode: ON — provide detailed, thorough explanations with examples]");
431
+ }
432
+ if (hints.length > 0) {
433
+ taggedPrompt = `${hints.join("\n")}\n\n${taggedPrompt}`;
434
+ }
411
435
  }
412
436
  // Log role: background events are "system", user messages are "user"
413
437
  const logRole = source.type === "background" ? "system" : "user";
@@ -468,8 +492,7 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
468
492
  await callback(finalContent, true, { assistantLogId });
469
493
  // Auto-continue: if the response was cut short by timeout, automatically
470
494
  // send a follow-up "Continue" message so the user doesn't have to
471
- if (finalContent.includes("⏱ Response was cut short (timeout)") &&
472
- _autoContinueCount < MAX_AUTO_CONTINUE) {
495
+ if (finalContent.includes("⏱ Response was cut short (timeout)") && _autoContinueCount < MAX_AUTO_CONTINUE) {
473
496
  console.log(`[nzb] Auto-continuing after timeout (${_autoContinueCount + 1}/${MAX_AUTO_CONTINUE})…`);
474
497
  await sleep(1000);
475
498
  void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage, _autoContinueCount + 1);
@@ -532,4 +555,45 @@ export function getWorkers() {
532
555
  export function getQueueSize() {
533
556
  return messageQueue.length;
534
557
  }
558
+ /** Reset the orchestrator session — destroys current session and creates a fresh one. */
559
+ export async function resetSession() {
560
+ // Drain any queued messages first
561
+ while (messageQueue.length > 0) {
562
+ const item = messageQueue.shift();
563
+ item.reject(new Error("Session reset"));
564
+ }
565
+ // Abort in-flight request
566
+ if (orchestratorSession && currentCallback) {
567
+ try {
568
+ await orchestratorSession.abort();
569
+ }
570
+ catch { }
571
+ }
572
+ // Destroy the existing session
573
+ if (orchestratorSession) {
574
+ try {
575
+ await orchestratorSession.destroy();
576
+ }
577
+ catch { }
578
+ orchestratorSession = undefined;
579
+ }
580
+ // Clear persisted session ID so a fresh session is created
581
+ deleteState(ORCHESTRATOR_SESSION_KEY);
582
+ console.log("[nzb] Session reset — will create fresh session on next message");
583
+ }
584
+ /** Compact the session by sending a compaction prompt (summarize context). */
585
+ export async function compactSession() {
586
+ const session = await ensureOrchestratorSession();
587
+ try {
588
+ const result = await session.sendAndWait({
589
+ prompt: "[System: Context compaction requested] Summarize everything important from our conversation so far into a concise internal note. " +
590
+ "Include: key decisions, pending tasks, user preferences, and any context you'd need to continue helping. " +
591
+ "This summary will be used to maintain continuity. Be thorough but concise.",
592
+ }, 30_000);
593
+ return result?.data?.content || "(Compaction completed)";
594
+ }
595
+ catch (err) {
596
+ return `Compaction failed: ${err instanceof Error ? err.message : String(err)}`;
597
+ }
598
+ }
535
599
  //# sourceMappingURL=orchestrator.js.map
@@ -133,8 +133,13 @@ export async function startBot() {
133
133
  await bot.api.setMyCommands([
134
134
  { command: "start", description: "Start the bot" },
135
135
  { command: "help", description: "Show help text" },
136
+ { command: "new", description: "Reset session (fresh context)" },
137
+ { command: "compact", description: "Compact session context" },
136
138
  { command: "cancel", description: "Cancel current message" },
137
139
  { command: "model", description: "Show/switch AI model" },
140
+ { command: "think", description: "Set thinking level (off/low/medium/high)" },
141
+ { command: "verbose", description: "Toggle verbose responses" },
142
+ { command: "usage", description: "Set usage display (off/tokens/full)" },
138
143
  { command: "status", description: "Show system status" },
139
144
  { command: "workers", description: "List active workers" },
140
145
  { command: "skills", description: "List installed skills" },
@@ -1,10 +1,10 @@
1
- import { config, persistModel } from "../../config.js";
2
- import { cancelCurrentMessage, getQueueSize, getWorkers } from "../../copilot/orchestrator.js";
1
+ import { config, persistEnvVar, persistModel } from "../../config.js";
2
+ import { cancelCurrentMessage, compactSession, getQueueSize, getWorkers, resetSession } from "../../copilot/orchestrator.js";
3
3
  import { listSkills } from "../../copilot/skills.js";
4
4
  import { restartDaemon } from "../../daemon.js";
5
5
  import { searchMemories } from "../../store/db.js";
6
- import { getReactionHelpText } from "./reactions.js";
7
6
  import { buildSettingsText, formatMemoryList } from "../menus.js";
7
+ import { getReactionHelpText } from "./reactions.js";
8
8
  export function registerCommandHandlers(bot, deps) {
9
9
  const { replyKeyboard, mainMenu, settingsMenu, getUptimeStr } = deps;
10
10
  // /start and /help — with inline menu + reply keyboard
@@ -14,27 +14,89 @@ export function registerCommandHandlers(bot, deps) {
14
14
  });
15
15
  bot.command("help", (ctx) => ctx.reply("I'm NZB, your AI daemon.\n\n" +
16
16
  "Just send me a message and I'll handle it.\n\n" +
17
- "Commands:\n" +
18
- "/cancelCancel the current message\n" +
19
- "/modelShow current model\n" +
20
- "/model <name> Switch model\n" +
21
- "/memory — Show stored memories\n" +
22
- "/skillsList installed skills\n" +
23
- "/workersList active worker sessions\n" +
24
- "/statusShow system status\n" +
25
- "/settingsBot settings\n" +
26
- "/restartRestart NZB\n" +
27
- "/help — Show this help\n\n" +
17
+ "Session:\n" +
18
+ "/newReset session (fresh context)\n" +
19
+ "/compactCompact session context\n" +
20
+ "/cancelCancel the current message\n\n" +
21
+ "Config:\n" +
22
+ "/modelShow/switch AI model\n" +
23
+ "/think <level> off|low|medium|high\n" +
24
+ "/verboseToggle verbose responses\n" +
25
+ "/usageoff|tokens|full\n" +
26
+ "/settingsBot settings\n\n" +
27
+ "Info:\n" +
28
+ "/status — System status\n" +
29
+ "/memory — Stored memories\n" +
30
+ "/skills — Installed skills\n" +
31
+ "/workers — Active worker sessions\n" +
32
+ "/restart — Restart NZB\n\n" +
28
33
  "⚡ Breakthrough Features:\n" +
29
34
  "• @bot query — Use me inline in any chat!\n" +
30
35
  "• React to any message to trigger AI:\n" +
31
36
  getReactionHelpText() +
32
37
  "\n" +
33
- "• Smart suggestions appear after each response", { reply_markup: mainMenu }));
38
+ "• Smart suggestions appear after each response\n" +
39
+ "• Works in groups — mention me to activate!", { reply_markup: mainMenu }));
34
40
  bot.command("cancel", async (ctx) => {
35
41
  const cancelled = await cancelCurrentMessage();
36
42
  await ctx.reply(cancelled ? "Cancelled." : "Nothing to cancel.");
37
43
  });
44
+ bot.command("new", async (ctx) => {
45
+ await ctx.reply("🔄 Resetting session…");
46
+ try {
47
+ await resetSession();
48
+ await ctx.reply("✅ Session reset. Fresh context — send me a message to start.");
49
+ }
50
+ catch (err) {
51
+ await ctx.reply(`❌ Reset failed: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ });
54
+ bot.command("compact", async (ctx) => {
55
+ await ctx.reply("📦 Compacting session context…");
56
+ try {
57
+ const summary = await compactSession();
58
+ const preview = summary.length > 500 ? summary.slice(0, 500) + "…" : summary;
59
+ await ctx.reply(`✅ Context compacted.\n\n${preview}`);
60
+ }
61
+ catch (err) {
62
+ await ctx.reply(`❌ Compaction failed: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ });
65
+ bot.command("think", async (ctx) => {
66
+ const THINK_LEVELS = ["off", "low", "medium", "high"];
67
+ const arg = ctx.match?.trim().toLowerCase();
68
+ if (arg && THINK_LEVELS.includes(arg)) {
69
+ config.thinkingLevel = arg;
70
+ persistEnvVar("THINKING_LEVEL", arg);
71
+ await ctx.reply(`🧠 Thinking level → ${arg}`);
72
+ }
73
+ else if (arg) {
74
+ await ctx.reply(`Invalid level: ${arg}\nValid: ${THINK_LEVELS.join(", ")}`);
75
+ }
76
+ else {
77
+ await ctx.reply(`🧠 Thinking level: ${config.thinkingLevel}\nSet with: /think off|low|medium|high`);
78
+ }
79
+ });
80
+ bot.command("usage", async (ctx) => {
81
+ const USAGE_MODES = ["off", "tokens", "full"];
82
+ const arg = ctx.match?.trim().toLowerCase();
83
+ if (arg && USAGE_MODES.includes(arg)) {
84
+ config.usageMode = arg;
85
+ persistEnvVar("USAGE_MODE", arg);
86
+ await ctx.reply(`📊 Usage display → ${arg}`);
87
+ }
88
+ else if (arg) {
89
+ await ctx.reply(`Invalid mode: ${arg}\nValid: ${USAGE_MODES.join(", ")}`);
90
+ }
91
+ else {
92
+ await ctx.reply(`📊 Usage display: ${config.usageMode}\nSet with: /usage off|tokens|full`);
93
+ }
94
+ });
95
+ bot.command("verbose", async (ctx) => {
96
+ config.verboseMode = !config.verboseMode;
97
+ persistEnvVar("VERBOSE_MODE", config.verboseMode ? "true" : "false");
98
+ await ctx.reply(`📝 Verbose mode ${config.verboseMode ? "ON — detailed responses" : "OFF — concise responses"}`);
99
+ });
38
100
  bot.command("model", async (ctx) => {
39
101
  const arg = ctx.match?.trim();
40
102
  if (arg) {
@@ -99,6 +161,9 @@ export function registerCommandHandlers(bot, deps) {
99
161
  const lines = [
100
162
  "📊 NZB Status",
101
163
  `Model: ${config.copilotModel}`,
164
+ `Thinking: ${config.thinkingLevel}`,
165
+ `Verbose: ${config.verboseMode ? "on" : "off"}`,
166
+ `Usage: ${config.usageMode}`,
102
167
  `Uptime: ${getUptimeStr()}`,
103
168
  `Workers: ${workers.length} active`,
104
169
  `Queue: ${getQueueSize()} pending`,
@@ -122,6 +187,9 @@ export function registerCommandHandlers(bot, deps) {
122
187
  const lines = [
123
188
  "📊 NZB Status",
124
189
  `Model: ${config.copilotModel}`,
190
+ `Thinking: ${config.thinkingLevel}`,
191
+ `Verbose: ${config.verboseMode ? "on" : "off"}`,
192
+ `Usage: ${config.usageMode}`,
125
193
  `Uptime: ${getUptimeStr()}`,
126
194
  `Workers: ${workers.length} active`,
127
195
  `Queue: ${getQueueSize()} pending`,
@@ -11,8 +11,17 @@ export function registerMessageHandler(bot, getBot) {
11
11
  const chatId = ctx.chat.id;
12
12
  const userMessageId = ctx.message.message_id;
13
13
  const replyParams = { message_id: userMessageId };
14
+ // Group chat support — only respond when mentioned or replied to
15
+ const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
16
+ if (isGroup && config.groupMentionOnly) {
17
+ const botUsername = ctx.me.username;
18
+ const isMentioned = botUsername && ctx.message.text.includes(`@${botUsername}`);
19
+ const isReplyToBot = ctx.message.reply_to_message?.from?.id === ctx.me.id;
20
+ if (!isMentioned && !isReplyToBot)
21
+ return;
22
+ }
14
23
  const msgPreview = ctx.message.text.length > 80 ? ctx.message.text.slice(0, 80) + "…" : ctx.message.text;
15
- void logInfo(`📩 Message: ${msgPreview}`);
24
+ void logInfo(`📩 Message${isGroup ? " (group)" : ""}: ${msgPreview}`);
16
25
  // React with 👀 to acknowledge message received
17
26
  try {
18
27
  await ctx.react("👀");
@@ -163,6 +172,10 @@ export function registerMessageHandler(bot, getBot) {
163
172
  };
164
173
  // If user replies to a message, include surrounding conversation context
165
174
  let userPrompt = ctx.message.text;
175
+ // Strip bot mention from the prompt in group chats
176
+ if (isGroup && ctx.me.username) {
177
+ userPrompt = userPrompt.replace(new RegExp(`@${ctx.me.username}\\b`, "gi"), "").trim();
178
+ }
166
179
  const replyMsg = ctx.message.reply_to_message;
167
180
  if (replyMsg && "text" in replyMsg && replyMsg.text) {
168
181
  // Try to find full conversation context around the replied message
@@ -209,15 +222,15 @@ export function registerMessageHandler(bot, getBot) {
209
222
  return;
210
223
  }
211
224
  let textWithMeta = text;
212
- if (usageInfo) {
225
+ if (usageInfo && config.usageMode !== "off") {
213
226
  const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
214
227
  const parts = [];
215
- if (usageInfo.model)
228
+ if (config.usageMode === "full" && usageInfo.model)
216
229
  parts.push(usageInfo.model);
217
230
  parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
218
231
  const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
219
232
  parts.push(`Σ${fmtTokens(totalTokens)}`);
220
- if (usageInfo.duration)
233
+ if (config.usageMode === "full" && usageInfo.duration)
221
234
  parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
222
235
  textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
223
236
  }
@@ -43,6 +43,9 @@ export function buildSettingsText(getUptimeStr) {
43
43
  return ("⚙️ Settings\n\n" +
44
44
  `⏱ Worker Timeout: ${getTimeoutLabel()}\n` +
45
45
  `🤖 Model: ${config.copilotModel}\n` +
46
+ `🧠 Thinking: ${config.thinkingLevel}\n` +
47
+ `📝 Verbose: ${config.verboseMode ? "✅ ON" : "❌ OFF"}\n` +
48
+ `📊 Usage: ${config.usageMode}\n` +
46
49
  `🔧 Show Reasoning: ${config.showReasoning ? "✅ ON" : "❌ OFF"}\n\n` +
47
50
  `📌 v${process.env.npm_package_version || "?"} · uptime ${getUptimeStr()}`);
48
51
  }
@@ -103,6 +106,35 @@ export function createMenus(getUptimeStr) {
103
106
  ctx.menu.update();
104
107
  await ctx.editMessageText(buildSettingsText(getUptimeStr));
105
108
  await ctx.answerCallbackQuery(`Reasoning ${config.showReasoning ? "ON" : "OFF"}`);
109
+ })
110
+ .row()
111
+ .text(() => `🧠 Think: ${config.thinkingLevel}`, async (ctx) => {
112
+ const levels = ["off", "low", "medium", "high"];
113
+ const idx = levels.indexOf(config.thinkingLevel);
114
+ const next = levels[(idx + 1) % levels.length];
115
+ config.thinkingLevel = next;
116
+ persistEnvVar("THINKING_LEVEL", next);
117
+ ctx.menu.update();
118
+ await ctx.editMessageText(buildSettingsText(getUptimeStr));
119
+ await ctx.answerCallbackQuery(`Thinking → ${next}`);
120
+ })
121
+ .text(() => `📝 ${config.verboseMode ? "Verbose" : "Concise"}`, async (ctx) => {
122
+ config.verboseMode = !config.verboseMode;
123
+ persistEnvVar("VERBOSE_MODE", config.verboseMode ? "true" : "false");
124
+ ctx.menu.update();
125
+ await ctx.editMessageText(buildSettingsText(getUptimeStr));
126
+ await ctx.answerCallbackQuery(`Verbose ${config.verboseMode ? "ON" : "OFF"}`);
127
+ })
128
+ .row()
129
+ .text(() => `📊 Usage: ${config.usageMode}`, async (ctx) => {
130
+ const modes = ["off", "tokens", "full"];
131
+ const idx = modes.indexOf(config.usageMode);
132
+ const next = modes[(idx + 1) % modes.length];
133
+ config.usageMode = next;
134
+ persistEnvVar("USAGE_MODE", next);
135
+ ctx.menu.update();
136
+ await ctx.editMessageText(buildSettingsText(getUptimeStr));
137
+ await ctx.answerCallbackQuery(`Usage → ${next}`);
106
138
  })
107
139
  .row()
108
140
  .text(() => `📌 v${process.env.npm_package_version || "?"} · uptime ${getUptimeStr()}`, async (ctx) => {
@@ -119,6 +151,9 @@ export function createMenus(getUptimeStr) {
119
151
  const lines = [
120
152
  "📊 NZB Status",
121
153
  `Model: ${config.copilotModel}`,
154
+ `Thinking: ${config.thinkingLevel}`,
155
+ `Verbose: ${config.verboseMode ? "on" : "off"}`,
156
+ `Usage: ${config.usageMode}`,
122
157
  `Uptime: ${getUptimeStr()}`,
123
158
  `Workers: ${workers.length} active`,
124
159
  `Queue: ${getQueueSize()} pending`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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"
@@ -69,4 +69,4 @@
69
69
  "typescript": "^5.9.3",
70
70
  "vitest": "^4.1.0"
71
71
  }
72
- }
72
+ }