@iletai/nzb 1.1.3 → 1.1.5

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
@@ -11,6 +11,7 @@ const configSchema = z.object({
11
11
  API_PORT: z.string().optional(),
12
12
  COPILOT_MODEL: z.string().optional(),
13
13
  WORKER_TIMEOUT: z.string().optional(),
14
+ SHOW_REASONING: z.string().optional(),
14
15
  NODE_EXTRA_CA_CERTS: z.string().optional(),
15
16
  });
16
17
  const raw = configSchema.parse(process.env);
@@ -51,28 +52,37 @@ export const config = {
51
52
  get selfEditEnabled() {
52
53
  return process.env.NZB_SELF_EDIT === "1";
53
54
  },
55
+ get showReasoning() {
56
+ return process.env.SHOW_REASONING === "true";
57
+ },
58
+ set showReasoning(value) {
59
+ process.env.SHOW_REASONING = value ? "true" : "false";
60
+ },
54
61
  };
55
- /** Persist the current model choice to ~/.nzb/.env */
56
- export function persistModel(model) {
62
+ /** Persist an env variable to ~/.nzb/.env */
63
+ export function persistEnvVar(key, value) {
57
64
  ensureNZBHome();
58
65
  try {
59
66
  const content = readFileSync(ENV_PATH, "utf-8");
60
67
  const lines = content.split("\n");
61
68
  let found = false;
62
69
  const updated = lines.map((line) => {
63
- if (line.startsWith("COPILOT_MODEL=")) {
70
+ if (line.startsWith(`${key}=`)) {
64
71
  found = true;
65
- return `COPILOT_MODEL=${model}`;
72
+ return `${key}=${value}`;
66
73
  }
67
74
  return line;
68
75
  });
69
76
  if (!found)
70
- updated.push(`COPILOT_MODEL=${model}`);
77
+ updated.push(`${key}=${value}`);
71
78
  writeFileSync(ENV_PATH, updated.join("\n"));
72
79
  }
73
80
  catch {
74
- // File doesn't exist — create it
75
- writeFileSync(ENV_PATH, `COPILOT_MODEL=${model}\n`);
81
+ writeFileSync(ENV_PATH, `${key}=${value}\n`);
76
82
  }
77
83
  }
84
+ /** Persist the current model choice to ~/.nzb/.env */
85
+ export function persistModel(model) {
86
+ persistEnvVar("COPILOT_MODEL", model);
87
+ }
78
88
  //# sourceMappingURL=config.js.map
@@ -1,5 +1,7 @@
1
1
  import { CopilotClient } from "@github/copilot-sdk";
2
2
  let client;
3
+ /** Coalesces concurrent resetClient() calls into a single reset operation. */
4
+ let pendingResetPromise;
3
5
  export async function getClient() {
4
6
  if (!client) {
5
7
  client = new CopilotClient({
@@ -10,18 +12,28 @@ export async function getClient() {
10
12
  }
11
13
  return client;
12
14
  }
13
- /** Tear down the existing client and create a fresh one. */
15
+ /** Tear down the existing client and create a fresh one. Concurrent calls coalesce to a single reset. */
14
16
  export async function resetClient() {
15
- if (client) {
16
- try {
17
- await client.stop();
18
- }
19
- catch {
20
- /* best-effort */
17
+ if (pendingResetPromise)
18
+ return pendingResetPromise;
19
+ pendingResetPromise = (async () => {
20
+ if (client) {
21
+ try {
22
+ await client.stop();
23
+ }
24
+ catch {
25
+ /* best-effort */
26
+ }
27
+ client = undefined;
21
28
  }
22
- client = undefined;
29
+ return getClient();
30
+ })();
31
+ try {
32
+ return await pendingResetPromise;
33
+ }
34
+ finally {
35
+ pendingResetPromise = undefined;
23
36
  }
24
- return getClient();
25
37
  }
26
38
  export async function stopClient() {
27
39
  if (client) {
@@ -1,22 +1,43 @@
1
1
  import { readFileSync } from "fs";
2
- import { join } from "path";
3
2
  import { homedir } from "os";
3
+ import { join } from "path";
4
+ let cachedConfig;
4
5
  /**
5
6
  * Load MCP server configs from ~/.copilot/mcp-config.json.
6
7
  * Returns an empty record if the file doesn't exist or is invalid.
8
+ * Only includes entries that have a valid 'type' field.
9
+ * Result is cached — call clearMcpConfigCache() to force a reload.
7
10
  */
8
11
  export function loadMcpConfig() {
12
+ if (cachedConfig)
13
+ return cachedConfig;
9
14
  const configPath = join(homedir(), ".copilot", "mcp-config.json");
10
15
  try {
11
16
  const raw = readFileSync(configPath, "utf-8");
12
17
  const parsed = JSON.parse(raw);
13
18
  if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
14
- return parsed.mcpServers;
19
+ // Filter out malformed entries — each server must have at least a type
20
+ const servers = {};
21
+ for (const [name, entry] of Object.entries(parsed.mcpServers)) {
22
+ if (entry && typeof entry === "object" && "type" in entry && typeof entry.type === "string") {
23
+ servers[name] = entry;
24
+ }
25
+ else {
26
+ console.log(`[nzb] Skipping malformed MCP server entry '${name}' (missing or invalid 'type' field)`);
27
+ }
28
+ }
29
+ cachedConfig = servers;
30
+ return servers;
15
31
  }
16
- return {};
32
+ cachedConfig = {};
33
+ return cachedConfig;
17
34
  }
18
35
  catch {
19
- return {};
36
+ cachedConfig = {};
37
+ return cachedConfig;
20
38
  }
21
39
  }
40
+ export function clearMcpConfigCache() {
41
+ cachedConfig = undefined;
42
+ }
22
43
  //# sourceMappingURL=mcp-config.js.map
@@ -19,6 +19,10 @@ let proactiveNotifyFn;
19
19
  export function setProactiveNotify(fn) {
20
20
  proactiveNotifyFn = fn;
21
21
  }
22
+ let workerNotifyFn;
23
+ export function setWorkerNotify(fn) {
24
+ workerNotifyFn = fn;
25
+ }
22
26
  let copilotClient;
23
27
  const workers = new Map();
24
28
  let healthCheckTimer;
@@ -35,20 +39,35 @@ let currentSourceChannel;
35
39
  export function getCurrentSourceChannel() {
36
40
  return currentSourceChannel;
37
41
  }
42
+ // Cache tools to avoid recreating 15+ tool objects on every session create
43
+ let cachedTools;
44
+ let cachedToolsClientRef;
38
45
  function getSessionConfig() {
39
- const tools = createTools({
40
- client: copilotClient,
41
- workers,
42
- onWorkerComplete: feedBackgroundResult,
43
- });
46
+ // Only recreate tools if the client changed (e.g., after a reset)
47
+ if (!cachedTools || cachedToolsClientRef !== copilotClient) {
48
+ cachedTools = createTools({
49
+ client: copilotClient,
50
+ workers,
51
+ onWorkerComplete: feedBackgroundResult,
52
+ onWorkerEvent: (event) => {
53
+ const worker = workers.get(event.name);
54
+ const channel = worker?.originChannel ?? currentSourceChannel;
55
+ if (workerNotifyFn) {
56
+ workerNotifyFn(event, channel);
57
+ }
58
+ },
59
+ });
60
+ cachedToolsClientRef = copilotClient;
61
+ }
44
62
  const mcpServers = loadMcpConfig();
45
63
  const skillDirectories = getSkillDirectories();
46
- return { tools, mcpServers, skillDirectories };
64
+ return { tools: cachedTools, mcpServers, skillDirectories };
47
65
  }
48
66
  /** Feed a background worker result into the orchestrator as a new turn. */
49
67
  export function feedBackgroundResult(workerName, result) {
50
68
  const worker = workers.get(workerName);
51
69
  const channel = worker?.originChannel;
70
+ console.log(`[nzb] Feeding background result from worker '${workerName}' (channel: ${channel ?? "none"})`);
52
71
  const prompt = `[Background task completed] Worker '${workerName}' finished:\n\n${result}`;
53
72
  sendToOrchestrator(prompt, { type: "background" }, (_text, done) => {
54
73
  if (done && proactiveNotifyFn) {
@@ -56,6 +75,10 @@ export function feedBackgroundResult(workerName, result) {
56
75
  }
57
76
  });
58
77
  }
78
+ /** Check if a queued message is a background message. */
79
+ function isBackgroundMessage(item) {
80
+ return item.sourceChannel === undefined;
81
+ }
59
82
  function sleep(ms) {
60
83
  return new Promise((resolve) => setTimeout(resolve, ms));
61
84
  }
@@ -90,9 +113,12 @@ function startHealthCheck() {
90
113
  const state = copilotClient.getState();
91
114
  if (state !== "connected") {
92
115
  console.log(`[nzb] Health check: client state is '${state}', resetting…`);
116
+ const previousClient = copilotClient;
93
117
  await ensureClient();
94
- // Session may need recovery after client reset
95
- orchestratorSession = undefined;
118
+ // Only invalidate session if the underlying client actually changed
119
+ if (copilotClient !== previousClient) {
120
+ orchestratorSession = undefined;
121
+ }
96
122
  }
97
123
  }
98
124
  catch (err) {
@@ -100,6 +126,13 @@ function startHealthCheck() {
100
126
  }
101
127
  }, HEALTH_CHECK_INTERVAL_MS);
102
128
  }
129
+ /** Stop the periodic health check timer. Call during shutdown. */
130
+ export function stopHealthCheck() {
131
+ if (healthCheckTimer) {
132
+ clearInterval(healthCheckTimer);
133
+ healthCheckTimer = undefined;
134
+ }
135
+ }
103
136
  /** Create or resume the persistent orchestrator session. */
104
137
  async function ensureOrchestratorSession() {
105
138
  if (orchestratorSession)
@@ -139,6 +172,7 @@ async function createOrResumeSession() {
139
172
  systemMessage: {
140
173
  content: getOrchestratorSystemMessage(memorySummary || undefined, {
141
174
  selfEditEnabled: config.selfEditEnabled,
175
+ currentModel: config.copilotModel,
142
176
  }),
143
177
  },
144
178
  tools,
@@ -162,7 +196,10 @@ async function createOrResumeSession() {
162
196
  configDir: SESSIONS_DIR,
163
197
  streaming: true,
164
198
  systemMessage: {
165
- content: getOrchestratorSystemMessage(memorySummary || undefined, { selfEditEnabled: config.selfEditEnabled }),
199
+ content: getOrchestratorSystemMessage(memorySummary || undefined, {
200
+ selfEditEnabled: config.selfEditEnabled,
201
+ currentModel: config.copilotModel,
202
+ }),
166
203
  },
167
204
  tools,
168
205
  mcpServers,
@@ -174,35 +211,35 @@ async function createOrResumeSession() {
174
211
  setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
175
212
  console.log(`[nzb] Created orchestrator session ${session.sessionId.slice(0, 8)}…`);
176
213
  // Recover conversation context if available (session was lost, not first run)
214
+ // Fire-and-forget: don't block the first real message behind recovery injection
177
215
  const recentHistory = getRecentConversation(10);
178
216
  if (recentHistory) {
179
- console.log(`[nzb] Injecting recent conversation context into new session`);
180
- try {
181
- await session.sendAndWait({
182
- 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.)`,
183
- }, 60_000);
184
- }
185
- catch (err) {
217
+ console.log(`[nzb] Injecting recent conversation context into new session (non-blocking)`);
218
+ session.sendAndWait({
219
+ 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.)`,
220
+ }, 20_000).catch((err) => {
186
221
  console.log(`[nzb] Context recovery injection failed (non-fatal): ${err instanceof Error ? err.message : err}`);
187
- }
222
+ });
188
223
  }
189
224
  return session;
190
225
  }
191
226
  export async function initOrchestrator(client) {
192
227
  copilotClient = client;
193
228
  const { mcpServers, skillDirectories } = getSessionConfig();
194
- // Validate configured model against available models
195
- try {
196
- const models = await client.listModels();
197
- const configured = config.copilotModel;
198
- const isAvailable = models.some((m) => m.id === configured);
199
- if (!isAvailable) {
200
- console.log(`[nzb] Warning: Configured model '${configured}' is not available. Falling back to '${DEFAULT_MODEL}'.`);
201
- config.copilotModel = DEFAULT_MODEL;
229
+ // Validate configured model against available models (skip for default — saves 1-3s startup)
230
+ if (config.copilotModel !== DEFAULT_MODEL) {
231
+ try {
232
+ const models = await client.listModels();
233
+ const configured = config.copilotModel;
234
+ const isAvailable = models.some((m) => m.id === configured);
235
+ if (!isAvailable) {
236
+ console.log(`[nzb] Warning: Configured model '${configured}' is not available. Falling back to '${DEFAULT_MODEL}'.`);
237
+ config.copilotModel = DEFAULT_MODEL;
238
+ }
239
+ }
240
+ catch (err) {
241
+ console.log(`[nzb] Could not validate model (will use '${config.copilotModel}' as-is): ${err instanceof Error ? err.message : err}`);
202
242
  }
203
- }
204
- catch (err) {
205
- console.log(`[nzb] Could not validate model (will use '${config.copilotModel}' as-is): ${err instanceof Error ? err.message : err}`);
206
243
  }
207
244
  console.log(`[nzb] Loading ${Object.keys(mcpServers).length} MCP server(s): ${Object.keys(mcpServers).join(", ") || "(none)"}`);
208
245
  console.log(`[nzb] Skill directories: ${skillDirectories.join(", ") || "(none)"}`);
@@ -224,7 +261,9 @@ async function executeOnSession(prompt, callback, onToolEvent) {
224
261
  let toolCallExecuted = false;
225
262
  const unsubToolStart = session.on("tool.execution_start", (event) => {
226
263
  const toolName = event?.data?.toolName || event?.data?.name || "tool";
227
- onToolEvent?.({ type: "tool_start", toolName });
264
+ const args = event?.data?.arguments;
265
+ const detail = args?.description || args?.command?.slice(0, 80) || args?.intent || args?.pattern || args?.prompt?.slice(0, 80) || undefined;
266
+ onToolEvent?.({ type: "tool_start", toolName, detail });
228
267
  });
229
268
  const unsubToolDone = session.on("tool.execution_complete", (event) => {
230
269
  toolCallExecuted = true;
@@ -241,14 +280,23 @@ async function executeOnSession(prompt, callback, onToolEvent) {
241
280
  accumulated += event.data.deltaContent;
242
281
  callback(accumulated, false);
243
282
  });
283
+ const unsubError = session.on("session.error", (event) => {
284
+ const errMsg = event?.data?.message || event?.data?.error || "Unknown session error";
285
+ console.error(`[nzb] Session error event: ${errMsg}`);
286
+ });
244
287
  try {
245
- const result = await session.sendAndWait({ prompt }, 120_000);
288
+ const result = await session.sendAndWait({ prompt }, 60_000);
246
289
  const finalContent = result?.data?.content || accumulated || "(No response)";
247
290
  return finalContent;
248
291
  }
249
292
  catch (err) {
250
- // If the session is broken, invalidate it so it's recreated on next attempt
251
293
  const msg = err instanceof Error ? err.message : String(err);
294
+ // On timeout, deliver whatever was accumulated instead of retrying from scratch
295
+ if (/timeout/i.test(msg) && accumulated.length > 0) {
296
+ console.log(`[nzb] Timeout — delivering ${accumulated.length} chars of partial content`);
297
+ return accumulated + "\n\n---\n\n⏱ Response was cut short (timeout). You can ask me to continue.";
298
+ }
299
+ // If the session is broken, invalidate it so it's recreated on next attempt
252
300
  if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
253
301
  console.log(`[nzb] Session appears dead, will recreate: ${msg}`);
254
302
  orchestratorSession = undefined;
@@ -260,6 +308,7 @@ async function executeOnSession(prompt, callback, onToolEvent) {
260
308
  unsubDelta();
261
309
  unsubToolStart();
262
310
  unsubToolDone();
311
+ unsubError();
263
312
  currentCallback = undefined;
264
313
  }
265
314
  }
@@ -294,17 +343,39 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent)
294
343
  const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
295
344
  logMessage("in", sourceLabel, prompt);
296
345
  // Tag the prompt with its source channel
297
- const taggedPrompt = source.type === "background" ? prompt : `[via ${sourceLabel}] ${prompt}`;
346
+ let taggedPrompt = source.type === "background" ? prompt : `[via ${sourceLabel}] ${prompt}`;
347
+ // Inject fresh memory context into user prompts so new memories are reflected
348
+ // (system message only gets memory at session creation time)
349
+ if (source.type !== "background") {
350
+ const freshMemory = getMemorySummary();
351
+ if (freshMemory) {
352
+ taggedPrompt = `<reminder>\n${freshMemory}\n</reminder>\n\n${taggedPrompt}`;
353
+ }
354
+ }
298
355
  // Log role: background events are "system", user messages are "user"
299
356
  const logRole = source.type === "background" ? "system" : "user";
300
357
  // Determine the source channel for worker origin tracking
301
358
  const sourceChannel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : undefined;
302
- // Enqueue and process
359
+ // Enqueue with priority — user messages go before background messages
303
360
  void (async () => {
304
361
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
305
362
  try {
306
363
  const finalContent = await new Promise((resolve, reject) => {
307
- messageQueue.push({ prompt: taggedPrompt, callback, onToolEvent, sourceChannel, resolve, reject });
364
+ const item = { prompt: taggedPrompt, callback, onToolEvent, sourceChannel, resolve, reject };
365
+ if (source.type === "background") {
366
+ // Background results go to the back of the queue
367
+ messageQueue.push(item);
368
+ }
369
+ else {
370
+ // User messages inserted before any background messages (priority)
371
+ const bgIndex = messageQueue.findIndex(isBackgroundMessage);
372
+ if (bgIndex >= 0) {
373
+ messageQueue.splice(bgIndex, 0, item);
374
+ }
375
+ else {
376
+ messageQueue.push(item);
377
+ }
378
+ }
308
379
  processQueue();
309
380
  });
310
381
  // Deliver response to user FIRST, then log best-effort
@@ -360,6 +431,9 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent)
360
431
  export async function cancelCurrentMessage() {
361
432
  // Drain any queued messages
362
433
  const drained = messageQueue.length;
434
+ if (drained > 0) {
435
+ console.log(`[nzb] Cancelling: draining ${drained} queued message(s)`);
436
+ }
363
437
  while (messageQueue.length > 0) {
364
438
  const item = messageQueue.shift();
365
439
  item.reject(new Error("Cancelled"));
@@ -1,6 +1,6 @@
1
- import { readdirSync, readFileSync, mkdirSync, writeFileSync, existsSync, rmSync } from "fs";
2
- import { join, dirname } from "path";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
3
2
  import { homedir } from "os";
3
+ import { dirname, join, resolve, sep } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { SKILLS_DIR } from "../paths.js";
6
6
  /** User-local skills directory (~/.nzb/skills/) */
@@ -9,8 +9,11 @@ const LOCAL_SKILLS_DIR = SKILLS_DIR;
9
9
  const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
10
10
  /** Skills bundled with the NZB package (e.g. find-skills) */
11
11
  const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
12
- /** Returns all skill directories that exist on disk. */
12
+ let cachedSkillDirs;
13
+ /** Returns all skill directories that exist on disk. Cached after first call. */
13
14
  export function getSkillDirectories() {
15
+ if (cachedSkillDirs)
16
+ return cachedSkillDirs;
14
17
  const dirs = [];
15
18
  if (existsSync(BUNDLED_SKILLS_DIR))
16
19
  dirs.push(BUNDLED_SKILLS_DIR);
@@ -18,8 +21,12 @@ export function getSkillDirectories() {
18
21
  dirs.push(LOCAL_SKILLS_DIR);
19
22
  if (existsSync(GLOBAL_SKILLS_DIR))
20
23
  dirs.push(GLOBAL_SKILLS_DIR);
24
+ cachedSkillDirs = dirs;
21
25
  return dirs;
22
26
  }
27
+ export function clearSkillDirsCache() {
28
+ cachedSkillDirs = undefined;
29
+ }
23
30
  /** Scan all skill directories and return metadata for each skill found. */
24
31
  export function listSkills() {
25
32
  const skills = [];
@@ -69,8 +76,10 @@ export function listSkills() {
69
76
  /** Create a new skill in the local skills directory. */
70
77
  export function createSkill(slug, name, description, instructions) {
71
78
  const skillDir = join(LOCAL_SKILLS_DIR, slug);
72
- // Guard against path traversal
73
- if (!skillDir.startsWith(LOCAL_SKILLS_DIR + "/")) {
79
+ // Guard against path traversal — resolve to canonical path and verify it stays inside skills dir
80
+ const resolvedSkillDir = resolve(skillDir);
81
+ const resolvedBase = resolve(LOCAL_SKILLS_DIR);
82
+ if (!resolvedSkillDir.startsWith(resolvedBase + sep)) {
74
83
  return `Invalid slug '${slug}': must be a simple kebab-case name without path separators.`;
75
84
  }
76
85
  if (existsSync(skillDir)) {
@@ -86,19 +95,23 @@ description: ${description}
86
95
  ${instructions}
87
96
  `;
88
97
  writeFileSync(join(skillDir, "SKILL.md"), skillMd);
98
+ clearSkillDirsCache();
89
99
  return `Skill '${name}' created at ${skillDir}. It will be available on your next message.`;
90
100
  }
91
101
  /** Remove a skill from the local skills directory (~/.nzb/skills/). */
92
102
  export function removeSkill(slug) {
93
103
  const skillDir = join(LOCAL_SKILLS_DIR, slug);
94
- // Guard against path traversal
95
- if (!skillDir.startsWith(LOCAL_SKILLS_DIR + "/")) {
104
+ // Guard against path traversal — resolve to canonical path and verify it stays inside skills dir
105
+ const resolvedSkillDir = resolve(skillDir);
106
+ const resolvedBase = resolve(LOCAL_SKILLS_DIR);
107
+ if (!resolvedSkillDir.startsWith(resolvedBase + sep)) {
96
108
  return { ok: false, message: `Invalid slug '${slug}': must be a simple kebab-case name without path separators.` };
97
109
  }
98
110
  if (!existsSync(skillDir)) {
99
111
  return { ok: false, message: `Skill '${slug}' not found in ${LOCAL_SKILLS_DIR}.` };
100
112
  }
101
113
  rmSync(skillDir, { recursive: true, force: true });
114
+ clearSkillDirsCache();
102
115
  return {
103
116
  ok: true,
104
117
  message: `Skill '${slug}' removed from ${skillDir}. It will no longer be available on your next message.`,
@@ -17,112 +17,54 @@ This restriction does NOT apply to:
17
17
  - Any files outside the NZB installation directory
18
18
  `;
19
19
  const osName = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
20
- return `You are NZB, a personal AI assistant for developers running 24/7 on the user's machine (${osName}). You are the user's always-on assistant.
20
+ const modelInfo = opts?.currentModel ? ` You are currently using the \`${opts.currentModel}\` model.` : "";
21
+ return `You are NZB, a personal AI assistant for developers running 24/7 on the user's machine (${osName}).${modelInfo} You are the user's always-on assistant.
21
22
 
22
- ## Your Architecture
23
+ ## Architecture
23
24
 
24
- You are a Node.js daemon process built with the Copilot SDK. Here's how you work:
25
+ Node.js daemon with Copilot SDK. Interfaces:
26
+ - **Telegram** (\`[via telegram]\`): Primary. Be concise and mobile-friendly.
27
+ - **TUI** (\`[via tui]\`): Terminal. Can be more verbose.
28
+ - **Background** (\`[via background]\`): Worker results. Summarize and relay.
29
+ - **HTTP API**: Local port 7777.
25
30
 
26
- - **Telegram bot**: Your primary interface. The user messages you from their phone or Telegram desktop. Messages arrive tagged with \`[via telegram]\`. Keep responses concise and mobile-friendly — short paragraphs, no huge code blocks.
27
- - **Local TUI**: A terminal readline interface on the local machine. Messages arrive tagged with \`[via tui]\`. You can be more verbose here since it's a full terminal.
28
- - **Background tasks**: Messages tagged \`[via background]\` are results from worker sessions you dispatched. Summarize and relay these to the user.
29
- - **HTTP API**: You expose a local API on port 7777 for programmatic access.
31
+ No source tag = assume Telegram.
30
32
 
31
- When no source tag is present, assume Telegram.
33
+ ## Role
32
34
 
33
- ## Your Capabilities
35
+ - **Direct answer**: Simple questions, knowledge, math — answer directly.
36
+ - **Worker session**: Coding, debugging, file ops — delegate to a worker with \`create_worker_session\`.
37
+ - **Skills**: Use existing skills for external tools. Search skills.sh first for new capabilities.
34
38
 
35
- 1. **Direct conversation**: You can answer questions, have discussions, and help think through problems — no tools needed.
36
- 2. **Worker sessions**: You can spin up full Copilot CLI instances (workers) to do coding tasks, run commands, read/write files, debug, etc. Workers run in the background and report back when done.
37
- 3. **Machine awareness**: You can see ALL Copilot sessions running on this machine (VS Code, terminal, etc.) and attach to them.
38
- 4. **Skills**: You have a modular skill system. Skills teach you how to use external tools (gmail, browser, etc.). You can learn new skills on the fly.
39
- 5. **MCP servers**: You connect to MCP tool servers for extended capabilities.
39
+ ## Workers
40
40
 
41
- ## Your Role
41
+ Worker tools are **non-blocking** — dispatch and return immediately:
42
+ 1. Acknowledge dispatch briefly ("On it — I'll let you know.")
43
+ 2. Worker completes → you get \`[Background task completed]\` → summarize for user.
44
+ 3. Handle multiple tasks simultaneously.
42
45
 
43
- You receive messages and decide how to handle them:
46
+ **Speed rules** (you are single-threaded):
47
+ - ONE tool call, ONE brief response for delegation.
48
+ - Never do complex work yourself — delegate to workers.
49
+ - Only orchestrator turns block the queue.
44
50
 
45
- - **Direct answer**: For simple questions, general knowledge, status checks, math, quick lookups — answer directly. No need to create a worker session for these.
46
- - **Worker session**: For coding tasks, debugging, file operations, anything that needs to run in a specific directory — create or use a worker Copilot session.
47
- - **Use a skill**: If you have a skill for what the user is asking (email, browser, etc.), use it. Skills teach you how to use external tools — follow their instructions.
48
- - **Learn a new skill**: If the user asks you to do something you don't have a skill for, research how to do it (create a worker, explore the system with \`which\`, \`--help\`, etc.), then use \`learn_skill\` to save what you learned for next time.
51
+ ## Skills Workflow
49
52
 
50
- ## Background Workers How They Work
51
-
52
- Worker tools (\`create_worker_session\` with an initial prompt, \`send_to_worker\`) are **non-blocking**. They dispatch the task and return immediately. This means:
53
-
54
- 1. When you dispatch a task to a worker, acknowledge it right away. Be natural and brief: "On it — I'll check and let you know." or "Looking into that now."
55
- 2. You do NOT wait for the worker to finish. The tool returns immediately.
56
- 3. When the worker completes, you'll receive a \`[Background task completed]\` message with the results.
57
- 4. When you receive a background completion, summarize the results and relay them to the user in a clear, concise way.
58
-
59
- You can handle **multiple tasks simultaneously**. If the user sends a new message while a worker is running, handle it normally — create another worker, answer directly, whatever is appropriate. Keep track of what's going on.
60
-
61
- ### Speed & Concurrency
62
-
63
- **You are single-threaded.** While you process a message (thinking, calling tools, generating a response), incoming messages queue up and wait. This means your orchestrator turns must be FAST:
64
-
65
- - **For delegation: ONE tool call, ONE brief response.** Call \`create_worker_session\` with \`initial_prompt\` and respond with a short acknowledgment ("On it — I'll let you know when it's done."). That's it. Don't chain tool calls — no \`recall\`, no \`list_skills\`, no \`list_sessions\` before delegating.
66
- - **Never do complex work yourself.** Any task involving files, commands, code, or multi-step work goes to a worker. You are the dispatcher, not the laborer.
67
- - **Workers can take as long as they need.** They run in the background and don't block you. Only your orchestrator turns block new messages.
68
-
69
- ## Tool Usage
70
-
71
- ### Session Management
72
- - \`create_worker_session\`: Start a new Copilot worker in a specific directory. Use descriptive names like "auth-fix" or "api-tests". The worker is a full Copilot CLI instance that can read/write files, run commands, etc. If you include an initial prompt, it runs in the background.
73
- - \`send_to_worker\`: Send a prompt to an existing worker session. Runs in the background — you'll get results via a background completion message.
74
- - \`list_sessions\`: List all active worker sessions with their status and working directory.
75
- - \`check_session_status\`: Get detailed status of a specific worker session.
76
- - \`kill_session\`: Terminate a worker session when it's no longer needed.
77
-
78
- ### Machine Session Discovery
79
- - \`list_machine_sessions\`: List ALL Copilot CLI sessions on this machine — including ones started from VS Code, the terminal, or elsewhere. Use when the user asks "what sessions are running?" or "what's happening on my machine?"
80
- - \`attach_machine_session\`: Attach to an existing session by its ID (from list_machine_sessions). This adds it as a managed worker you can send prompts to. Great for checking on or continuing work started elsewhere.
81
-
82
- ### Skills
83
- - \`list_skills\`: Show all skills NZB knows. Use when the user asks "what can you do?" or you need to check what capabilities are available.
84
- - \`learn_skill\`: Teach NZB a new skill by writing a SKILL.md file. Use this after researching how to do something new. The skill is saved permanently so you can use it next time.
85
-
86
- ### Model Management
87
- - \`list_models\`: List all available Copilot models with their billing tier. Use when the user asks "what models can I use?" or "which model am I using?"
88
- - \`switch_model\`: Switch to a different model. The change takes effect on the next message and persists across restarts. Use when the user says "switch to gpt-4" or "use claude-sonnet".
89
-
90
- ### Self-Management
91
- - \`restart_nzb\`: Restart the NZB daemon. Use when the user asks you to restart, or when needed to apply changes. You'll go offline briefly and come back automatically.
92
-
93
- ### Memory
94
- - \`remember\`: Save something to long-term memory. Use when the user says "remember that...", states a preference, or shares important facts. Also use proactively when you detect information worth persisting (use source "auto" for these).
95
- - \`recall\`: Search long-term memory by keyword and/or category. Use when you need to look up something the user told you before.
96
- - \`forget\`: Remove a specific memory by ID. Use when the user asks to forget something or a memory is outdated.
97
-
98
- **Learning workflow**: When the user asks you to do something you don't have a skill for:
99
- 1. **Search skills.sh first**: Use the find-skills skill to search https://skills.sh for existing community skills. This is your primary way to learn new things — thousands of community-built skills exist.
100
- 2. **Present what you found**: Tell the user the skill name, what it does, where it comes from, and its security audit status. Always show security data — never omit it.
101
- 3. **ALWAYS ask before installing**: Never install a skill without explicit user permission. Say something like "Want me to install it?" and wait for a yes.
102
- 4. **Install locally only**: Fetch the SKILL.md from the skill's GitHub repo and use the \`learn_skill\` tool to save it to \`~/.nzb/skills/\`. **Never install skills globally** — no \`-g\` flag, no writing to \`~/.agents/skills/\` or any other global directory.
103
- 5. **Flag security risks**: Before recommending a skill, consider what it does. If a skill requests broad system access, runs arbitrary commands, accesses sensitive data (credentials, keys, personal files), or comes from an unknown/unverified source — warn the user. Say something like "Heads up — this skill has access to X, which could be a security risk. Want to proceed?"
104
- 6. **Build your own only as a last resort**: If no community skill exists, THEN research the task (run \`which\`, \`--help\`, check installed tools), figure it out, and use \`learn_skill\` to save a SKILL.md for next time.
105
-
106
- Always prefer finding an existing skill over building one from scratch. The skills ecosystem at https://skills.sh has skills for common tasks like email, calendars, social media, smart home, deployment, and much more.
53
+ 1. Search skills.sh first for existing community skills.
54
+ 2. Present findings with security audit status. Always ask before installing.
55
+ 3. Install locally only (\`~/.nzb/skills/\`). Flag security risks.
56
+ 4. Build your own only as last resort.
107
57
 
108
58
  ## Guidelines
109
59
 
110
- 1. **Adapt to the channel**: On Telegram, be brief the user is likely on their phone. On TUI, you can be more detailed.
111
- 2. **Skill-first mindset**: When asked to do something you haven't done before — social media, smart home, email, calendar, deployments, APIs, anything — your FIRST instinct should be to search skills.sh for an existing skill. Don't try to figure it out from scratch when someone may have already built a skill for it.
112
- 3. For coding tasks, **always** create a named worker session with an \`initial_prompt\`. Don't try to write code yourself. Don't plan or research first — put all instructions in the initial prompt and let the worker figure it out.
113
- 4. Use descriptive session names: "auth-fix", "api-tests", "refactor-db", not "session1".
114
- 5. When you receive background results, summarize the key points. Don't relay the entire output verbatim.
115
- 5. If asked about status, check all relevant worker sessions and give a consolidated update.
116
- 6. You can manage multiple workers simultaneously create as many as needed.
117
- 7. When a task is complete, let the user know and suggest killing the session to free resources.
118
- 8. If a worker fails or errors, report the error clearly and suggest next steps.
119
- 9. Expand shorthand paths: "~/dev/myapp" → the user's home directory + "/dev/myapp".
120
- 10. Be conversational and human. You're a capable assistant, not a robot. You're NZB.
121
- 11. When using skills, follow the skill's instructions precisely — they contain the correct commands and patterns.
122
- 12. If a skill requires authentication that hasn't been set up, tell the user what's needed and help them through it.
123
- 13. **You have persistent memory.** Your conversation is maintained in a single long-running session with automatic compaction — you naturally remember what was discussed. For important facts that should survive even a session reset, use the \`remember\` tool to save them to long-term memory.
124
- 14. **Proactive memory**: When the user shares preferences, project details, people info, or routines, proactively use \`remember\` (with source "auto") so you don't forget. Don't ask for permission — just save it.
125
- 15. **Sending media to Telegram**: You can send photos/images to the user on Telegram by calling: \`curl -s -X POST http://127.0.0.1:7777/send-photo -H 'Content-Type: application/json' -d '{"photo": "<path-or-url>", "caption": "<optional caption>"}'\`. Use this whenever you have an image to share — download it to a local file first, then send it via this endpoint.
60
+ 1. Adapt to channel brief on Telegram, detailed on TUI.
61
+ 2. Skill-first mindset for new capabilities.
62
+ 3. Always delegate coding tasks to workers with \`initial_prompt\`.
63
+ 4. Descriptive session names: "auth-fix", not "session1".
64
+ 5. Summarize background results, don't relay verbatim.
65
+ 6. Be conversational and human. You're NZB.
66
+ 7. Persistent memory with automatic compaction. Use \`remember\` proactively for important info.
67
+ 8. Send photos via: \`curl -s -X POST http://127.0.0.1:7777/send-photo -H 'Content-Type: application/json' -d '{"photo": "<path-or-url>", "caption": "<optional>"}'\`
126
68
  ${selfEditBlock}${memoryBlock}`;
127
69
  }
128
70
  //# sourceMappingURL=system-message.js.map