@iletai/nzb 1.1.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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/api/server.js +212 -0
- package/dist/cli.js +95 -0
- package/dist/config.js +72 -0
- package/dist/copilot/client.js +32 -0
- package/dist/copilot/mcp-config.js +22 -0
- package/dist/copilot/orchestrator.js +386 -0
- package/dist/copilot/skills.js +128 -0
- package/dist/copilot/system-message.js +128 -0
- package/dist/copilot/tools.js +502 -0
- package/dist/daemon.js +174 -0
- package/dist/paths.js +24 -0
- package/dist/setup.js +275 -0
- package/dist/store/db.js +179 -0
- package/dist/telegram/bot.js +343 -0
- package/dist/telegram/formatter.js +137 -0
- package/dist/tui/index.js +928 -0
- package/dist/update.js +72 -0
- package/package.json +64 -0
- package/scripts/fix-esm-imports.cjs +25 -0
- package/skills/.gitkeep +0 -0
- package/skills/find-skills/SKILL.md +161 -0
- package/skills/find-skills/_meta.json +4 -0
- package/skills/gogcli/SKILL.md +168 -0
- package/skills/gogcli/_meta.json +4 -0
- package/skills/skills-lock.json +10 -0
- package/skills/telegram-bot-builder/SKILL.md +267 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { approveAll } from "@github/copilot-sdk";
|
|
2
|
+
import { config, DEFAULT_MODEL } from "../config.js";
|
|
3
|
+
import { SESSIONS_DIR } from "../paths.js";
|
|
4
|
+
import { deleteState, getMemorySummary, getRecentConversation, getState, logConversation, setState, } from "../store/db.js";
|
|
5
|
+
import { resetClient } from "./client.js";
|
|
6
|
+
import { loadMcpConfig } from "./mcp-config.js";
|
|
7
|
+
import { getSkillDirectories } from "./skills.js";
|
|
8
|
+
import { getOrchestratorSystemMessage } from "./system-message.js";
|
|
9
|
+
import { createTools } from "./tools.js";
|
|
10
|
+
const MAX_RETRIES = 3;
|
|
11
|
+
const RECONNECT_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
12
|
+
const HEALTH_CHECK_INTERVAL_MS = 30_000;
|
|
13
|
+
const ORCHESTRATOR_SESSION_KEY = "orchestrator_session_id";
|
|
14
|
+
let logMessage = () => { };
|
|
15
|
+
export function setMessageLogger(fn) {
|
|
16
|
+
logMessage = fn;
|
|
17
|
+
}
|
|
18
|
+
let proactiveNotifyFn;
|
|
19
|
+
export function setProactiveNotify(fn) {
|
|
20
|
+
proactiveNotifyFn = fn;
|
|
21
|
+
}
|
|
22
|
+
let copilotClient;
|
|
23
|
+
const workers = new Map();
|
|
24
|
+
let healthCheckTimer;
|
|
25
|
+
// Persistent orchestrator session
|
|
26
|
+
let orchestratorSession;
|
|
27
|
+
// Coalesces concurrent ensureOrchestratorSession calls
|
|
28
|
+
let sessionCreatePromise;
|
|
29
|
+
const messageQueue = [];
|
|
30
|
+
let processing = false;
|
|
31
|
+
let currentCallback;
|
|
32
|
+
/** The channel currently being processed — tools use this to tag new workers. */
|
|
33
|
+
let currentSourceChannel;
|
|
34
|
+
/** Get the channel that originated the message currently being processed. */
|
|
35
|
+
export function getCurrentSourceChannel() {
|
|
36
|
+
return currentSourceChannel;
|
|
37
|
+
}
|
|
38
|
+
function getSessionConfig() {
|
|
39
|
+
const tools = createTools({
|
|
40
|
+
client: copilotClient,
|
|
41
|
+
workers,
|
|
42
|
+
onWorkerComplete: feedBackgroundResult,
|
|
43
|
+
});
|
|
44
|
+
const mcpServers = loadMcpConfig();
|
|
45
|
+
const skillDirectories = getSkillDirectories();
|
|
46
|
+
return { tools, mcpServers, skillDirectories };
|
|
47
|
+
}
|
|
48
|
+
/** Feed a background worker result into the orchestrator as a new turn. */
|
|
49
|
+
export function feedBackgroundResult(workerName, result) {
|
|
50
|
+
const worker = workers.get(workerName);
|
|
51
|
+
const channel = worker?.originChannel;
|
|
52
|
+
const prompt = `[Background task completed] Worker '${workerName}' finished:\n\n${result}`;
|
|
53
|
+
sendToOrchestrator(prompt, { type: "background" }, (_text, done) => {
|
|
54
|
+
if (done && proactiveNotifyFn) {
|
|
55
|
+
proactiveNotifyFn(_text, channel);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function sleep(ms) {
|
|
60
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
}
|
|
62
|
+
/** Ensure the SDK client is connected, resetting if necessary. Coalesces concurrent resets. */
|
|
63
|
+
let resetPromise;
|
|
64
|
+
async function ensureClient() {
|
|
65
|
+
if (copilotClient && copilotClient.getState() === "connected") {
|
|
66
|
+
return copilotClient;
|
|
67
|
+
}
|
|
68
|
+
if (!resetPromise) {
|
|
69
|
+
console.log(`[nzb] Client not connected (state: ${copilotClient?.getState() ?? "null"}), resetting…`);
|
|
70
|
+
resetPromise = resetClient()
|
|
71
|
+
.then((c) => {
|
|
72
|
+
console.log(`[nzb] Client reset successful, state: ${c.getState()}`);
|
|
73
|
+
copilotClient = c;
|
|
74
|
+
return c;
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
resetPromise = undefined;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return resetPromise;
|
|
81
|
+
}
|
|
82
|
+
/** Start periodic health check that proactively reconnects the client. */
|
|
83
|
+
function startHealthCheck() {
|
|
84
|
+
if (healthCheckTimer)
|
|
85
|
+
return;
|
|
86
|
+
healthCheckTimer = setInterval(async () => {
|
|
87
|
+
if (!copilotClient)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
const state = copilotClient.getState();
|
|
91
|
+
if (state !== "connected") {
|
|
92
|
+
console.log(`[nzb] Health check: client state is '${state}', resetting…`);
|
|
93
|
+
await ensureClient();
|
|
94
|
+
// Session may need recovery after client reset
|
|
95
|
+
orchestratorSession = undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(`[nzb] Health check error:`, err instanceof Error ? err.message : err);
|
|
100
|
+
}
|
|
101
|
+
}, HEALTH_CHECK_INTERVAL_MS);
|
|
102
|
+
}
|
|
103
|
+
/** Create or resume the persistent orchestrator session. */
|
|
104
|
+
async function ensureOrchestratorSession() {
|
|
105
|
+
if (orchestratorSession)
|
|
106
|
+
return orchestratorSession;
|
|
107
|
+
// Coalesce concurrent callers — wait for an in-flight creation
|
|
108
|
+
if (sessionCreatePromise)
|
|
109
|
+
return sessionCreatePromise;
|
|
110
|
+
sessionCreatePromise = createOrResumeSession();
|
|
111
|
+
try {
|
|
112
|
+
const session = await sessionCreatePromise;
|
|
113
|
+
orchestratorSession = session;
|
|
114
|
+
return session;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
sessionCreatePromise = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Internal: actually create or resume a session (not concurrency-safe — use ensureOrchestratorSession). */
|
|
121
|
+
async function createOrResumeSession() {
|
|
122
|
+
const client = await ensureClient();
|
|
123
|
+
const { tools, mcpServers, skillDirectories } = getSessionConfig();
|
|
124
|
+
const memorySummary = getMemorySummary();
|
|
125
|
+
const infiniteSessions = {
|
|
126
|
+
enabled: true,
|
|
127
|
+
backgroundCompactionThreshold: 0.8,
|
|
128
|
+
bufferExhaustionThreshold: 0.95,
|
|
129
|
+
};
|
|
130
|
+
// Try to resume a previous session
|
|
131
|
+
const savedSessionId = getState(ORCHESTRATOR_SESSION_KEY);
|
|
132
|
+
if (savedSessionId) {
|
|
133
|
+
try {
|
|
134
|
+
console.log(`[nzb] Resuming orchestrator session ${savedSessionId.slice(0, 8)}…`);
|
|
135
|
+
const session = await client.resumeSession(savedSessionId, {
|
|
136
|
+
model: config.copilotModel,
|
|
137
|
+
configDir: SESSIONS_DIR,
|
|
138
|
+
streaming: true,
|
|
139
|
+
systemMessage: {
|
|
140
|
+
content: getOrchestratorSystemMessage(memorySummary || undefined, {
|
|
141
|
+
selfEditEnabled: config.selfEditEnabled,
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
tools,
|
|
145
|
+
mcpServers,
|
|
146
|
+
skillDirectories,
|
|
147
|
+
onPermissionRequest: approveAll,
|
|
148
|
+
infiniteSessions,
|
|
149
|
+
});
|
|
150
|
+
console.log(`[nzb] Resumed orchestrator session successfully`);
|
|
151
|
+
return session;
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.log(`[nzb] Could not resume session: ${err instanceof Error ? err.message : err}. Creating new.`);
|
|
155
|
+
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Create a fresh session
|
|
159
|
+
console.log(`[nzb] Creating new persistent orchestrator session`);
|
|
160
|
+
const session = await client.createSession({
|
|
161
|
+
model: config.copilotModel,
|
|
162
|
+
configDir: SESSIONS_DIR,
|
|
163
|
+
streaming: true,
|
|
164
|
+
systemMessage: {
|
|
165
|
+
content: getOrchestratorSystemMessage(memorySummary || undefined, { selfEditEnabled: config.selfEditEnabled }),
|
|
166
|
+
},
|
|
167
|
+
tools,
|
|
168
|
+
mcpServers,
|
|
169
|
+
skillDirectories,
|
|
170
|
+
onPermissionRequest: approveAll,
|
|
171
|
+
infiniteSessions,
|
|
172
|
+
});
|
|
173
|
+
// Persist the session ID for future restarts
|
|
174
|
+
setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
|
|
175
|
+
console.log(`[nzb] Created orchestrator session ${session.sessionId.slice(0, 8)}…`);
|
|
176
|
+
// Recover conversation context if available (session was lost, not first run)
|
|
177
|
+
const recentHistory = getRecentConversation(10);
|
|
178
|
+
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) {
|
|
186
|
+
console.log(`[nzb] Context recovery injection failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return session;
|
|
190
|
+
}
|
|
191
|
+
export async function initOrchestrator(client) {
|
|
192
|
+
copilotClient = client;
|
|
193
|
+
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;
|
|
202
|
+
}
|
|
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
|
+
}
|
|
207
|
+
console.log(`[nzb] Loading ${Object.keys(mcpServers).length} MCP server(s): ${Object.keys(mcpServers).join(", ") || "(none)"}`);
|
|
208
|
+
console.log(`[nzb] Skill directories: ${skillDirectories.join(", ") || "(none)"}`);
|
|
209
|
+
console.log(`[nzb] Persistent session mode — conversation history maintained by SDK`);
|
|
210
|
+
startHealthCheck();
|
|
211
|
+
// Eagerly create/resume the orchestrator session
|
|
212
|
+
try {
|
|
213
|
+
await ensureOrchestratorSession();
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
console.error(`[nzb] Failed to create initial session (will retry on first message):`, err instanceof Error ? err.message : err);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Send a prompt on the persistent session, return the response. */
|
|
220
|
+
async function executeOnSession(prompt, callback, onToolEvent) {
|
|
221
|
+
const session = await ensureOrchestratorSession();
|
|
222
|
+
currentCallback = callback;
|
|
223
|
+
let accumulated = "";
|
|
224
|
+
let toolCallExecuted = false;
|
|
225
|
+
const unsubToolStart = session.on("tool.execution_start", (event) => {
|
|
226
|
+
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
227
|
+
onToolEvent?.({ type: "tool_start", toolName });
|
|
228
|
+
});
|
|
229
|
+
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
230
|
+
toolCallExecuted = true;
|
|
231
|
+
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
232
|
+
onToolEvent?.({ type: "tool_complete", toolName });
|
|
233
|
+
});
|
|
234
|
+
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
235
|
+
// After a tool call completes, ensure a line break separates the text blocks
|
|
236
|
+
// so they don't visually run together in the TUI.
|
|
237
|
+
if (toolCallExecuted && accumulated.length > 0 && !accumulated.endsWith("\n")) {
|
|
238
|
+
accumulated += "\n";
|
|
239
|
+
}
|
|
240
|
+
toolCallExecuted = false;
|
|
241
|
+
accumulated += event.data.deltaContent;
|
|
242
|
+
callback(accumulated, false);
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const result = await session.sendAndWait({ prompt }, 300_000);
|
|
246
|
+
const finalContent = result?.data?.content || accumulated || "(No response)";
|
|
247
|
+
return finalContent;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
// If the session is broken, invalidate it so it's recreated on next attempt
|
|
251
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
252
|
+
if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
|
|
253
|
+
console.log(`[nzb] Session appears dead, will recreate: ${msg}`);
|
|
254
|
+
orchestratorSession = undefined;
|
|
255
|
+
deleteState(ORCHESTRATOR_SESSION_KEY);
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
unsubDelta();
|
|
261
|
+
unsubToolStart();
|
|
262
|
+
unsubToolDone();
|
|
263
|
+
currentCallback = undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Process the message queue one at a time. */
|
|
267
|
+
async function processQueue() {
|
|
268
|
+
if (processing) {
|
|
269
|
+
if (messageQueue.length > 0) {
|
|
270
|
+
console.log(`[nzb] Message queued (${messageQueue.length} waiting — orchestrator is busy)`);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
processing = true;
|
|
275
|
+
while (messageQueue.length > 0) {
|
|
276
|
+
const item = messageQueue.shift();
|
|
277
|
+
currentSourceChannel = item.sourceChannel;
|
|
278
|
+
try {
|
|
279
|
+
const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent);
|
|
280
|
+
item.resolve(result);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
item.reject(err);
|
|
284
|
+
}
|
|
285
|
+
currentSourceChannel = undefined;
|
|
286
|
+
}
|
|
287
|
+
processing = false;
|
|
288
|
+
}
|
|
289
|
+
function isRecoverableError(err) {
|
|
290
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
291
|
+
return /timeout|disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
|
|
292
|
+
}
|
|
293
|
+
export async function sendToOrchestrator(prompt, source, callback, onToolEvent) {
|
|
294
|
+
const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
|
|
295
|
+
logMessage("in", sourceLabel, prompt);
|
|
296
|
+
// Tag the prompt with its source channel
|
|
297
|
+
const taggedPrompt = source.type === "background" ? prompt : `[via ${sourceLabel}] ${prompt}`;
|
|
298
|
+
// Log role: background events are "system", user messages are "user"
|
|
299
|
+
const logRole = source.type === "background" ? "system" : "user";
|
|
300
|
+
// Determine the source channel for worker origin tracking
|
|
301
|
+
const sourceChannel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : undefined;
|
|
302
|
+
// Enqueue and process
|
|
303
|
+
void (async () => {
|
|
304
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
305
|
+
try {
|
|
306
|
+
const finalContent = await new Promise((resolve, reject) => {
|
|
307
|
+
messageQueue.push({ prompt: taggedPrompt, callback, onToolEvent, sourceChannel, resolve, reject });
|
|
308
|
+
processQueue();
|
|
309
|
+
});
|
|
310
|
+
// Deliver response to user FIRST, then log best-effort
|
|
311
|
+
callback(finalContent, true);
|
|
312
|
+
try {
|
|
313
|
+
logMessage("out", sourceLabel, finalContent);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* best-effort */
|
|
317
|
+
}
|
|
318
|
+
// Log both sides of the conversation after delivery
|
|
319
|
+
try {
|
|
320
|
+
logConversation(logRole, prompt, sourceLabel);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
/* best-effort */
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
logConversation("assistant", finalContent, sourceLabel);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
/* best-effort */
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
335
|
+
// Don't retry cancelled messages
|
|
336
|
+
if (/cancelled|abort/i.test(msg)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (isRecoverableError(err) && attempt < MAX_RETRIES) {
|
|
340
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
341
|
+
console.error(`[nzb] Recoverable error: ${msg}. Retry ${attempt + 1}/${MAX_RETRIES} after ${delay}ms…`);
|
|
342
|
+
await sleep(delay);
|
|
343
|
+
// Reset client before retry in case the connection is stale
|
|
344
|
+
try {
|
|
345
|
+
await ensureClient();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
/* will fail again on next attempt */
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
console.error(`[nzb] Error processing message: ${msg}`);
|
|
353
|
+
callback(`Error: ${msg}`, true);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
})();
|
|
358
|
+
}
|
|
359
|
+
/** Cancel the in-flight message and drain the queue. */
|
|
360
|
+
export async function cancelCurrentMessage() {
|
|
361
|
+
// Drain any queued messages
|
|
362
|
+
const drained = messageQueue.length;
|
|
363
|
+
while (messageQueue.length > 0) {
|
|
364
|
+
const item = messageQueue.shift();
|
|
365
|
+
item.reject(new Error("Cancelled"));
|
|
366
|
+
}
|
|
367
|
+
// Abort the active session request
|
|
368
|
+
if (orchestratorSession && currentCallback) {
|
|
369
|
+
try {
|
|
370
|
+
await orchestratorSession.abort();
|
|
371
|
+
console.log(`[nzb] Aborted in-flight request`);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.error(`[nzb] Abort failed:`, err instanceof Error ? err.message : err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return drained > 0;
|
|
379
|
+
}
|
|
380
|
+
export function getWorkers() {
|
|
381
|
+
return workers;
|
|
382
|
+
}
|
|
383
|
+
export function getQueueSize() {
|
|
384
|
+
return messageQueue.length;
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { SKILLS_DIR } from "../paths.js";
|
|
6
|
+
/** User-local skills directory (~/.nzb/skills/) */
|
|
7
|
+
const LOCAL_SKILLS_DIR = SKILLS_DIR;
|
|
8
|
+
/** Global shared skills directory */
|
|
9
|
+
const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
|
|
10
|
+
/** Skills bundled with the NZB package (e.g. find-skills) */
|
|
11
|
+
const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
12
|
+
/** Returns all skill directories that exist on disk. */
|
|
13
|
+
export function getSkillDirectories() {
|
|
14
|
+
const dirs = [];
|
|
15
|
+
if (existsSync(BUNDLED_SKILLS_DIR))
|
|
16
|
+
dirs.push(BUNDLED_SKILLS_DIR);
|
|
17
|
+
if (existsSync(LOCAL_SKILLS_DIR))
|
|
18
|
+
dirs.push(LOCAL_SKILLS_DIR);
|
|
19
|
+
if (existsSync(GLOBAL_SKILLS_DIR))
|
|
20
|
+
dirs.push(GLOBAL_SKILLS_DIR);
|
|
21
|
+
return dirs;
|
|
22
|
+
}
|
|
23
|
+
/** Scan all skill directories and return metadata for each skill found. */
|
|
24
|
+
export function listSkills() {
|
|
25
|
+
const skills = [];
|
|
26
|
+
for (const [dir, source] of [
|
|
27
|
+
[BUNDLED_SKILLS_DIR, "bundled"],
|
|
28
|
+
[LOCAL_SKILLS_DIR, "local"],
|
|
29
|
+
[GLOBAL_SKILLS_DIR, "global"],
|
|
30
|
+
]) {
|
|
31
|
+
if (!existsSync(dir))
|
|
32
|
+
continue;
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = readdirSync(dir);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const skillDir = join(dir, entry);
|
|
42
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
43
|
+
if (!existsSync(skillMd))
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
47
|
+
const { name, description } = parseFrontmatter(content);
|
|
48
|
+
skills.push({
|
|
49
|
+
slug: entry,
|
|
50
|
+
name: name || entry,
|
|
51
|
+
description: description || "(no description)",
|
|
52
|
+
directory: skillDir,
|
|
53
|
+
source,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
skills.push({
|
|
58
|
+
slug: entry,
|
|
59
|
+
name: entry,
|
|
60
|
+
description: "(could not read SKILL.md)",
|
|
61
|
+
directory: skillDir,
|
|
62
|
+
source,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return skills;
|
|
68
|
+
}
|
|
69
|
+
/** Create a new skill in the local skills directory. */
|
|
70
|
+
export function createSkill(slug, name, description, instructions) {
|
|
71
|
+
const skillDir = join(LOCAL_SKILLS_DIR, slug);
|
|
72
|
+
// Guard against path traversal
|
|
73
|
+
if (!skillDir.startsWith(LOCAL_SKILLS_DIR + "/")) {
|
|
74
|
+
return `Invalid slug '${slug}': must be a simple kebab-case name without path separators.`;
|
|
75
|
+
}
|
|
76
|
+
if (existsSync(skillDir)) {
|
|
77
|
+
return `Skill '${slug}' already exists at ${skillDir}. Edit it directly or delete it first.`;
|
|
78
|
+
}
|
|
79
|
+
mkdirSync(skillDir, { recursive: true });
|
|
80
|
+
writeFileSync(join(skillDir, "_meta.json"), JSON.stringify({ slug, version: "1.0.0" }, null, 2) + "\n");
|
|
81
|
+
const skillMd = `---
|
|
82
|
+
name: ${name}
|
|
83
|
+
description: ${description}
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
${instructions}
|
|
87
|
+
`;
|
|
88
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillMd);
|
|
89
|
+
return `Skill '${name}' created at ${skillDir}. It will be available on your next message.`;
|
|
90
|
+
}
|
|
91
|
+
/** Remove a skill from the local skills directory (~/.nzb/skills/). */
|
|
92
|
+
export function removeSkill(slug) {
|
|
93
|
+
const skillDir = join(LOCAL_SKILLS_DIR, slug);
|
|
94
|
+
// Guard against path traversal
|
|
95
|
+
if (!skillDir.startsWith(LOCAL_SKILLS_DIR + "/")) {
|
|
96
|
+
return { ok: false, message: `Invalid slug '${slug}': must be a simple kebab-case name without path separators.` };
|
|
97
|
+
}
|
|
98
|
+
if (!existsSync(skillDir)) {
|
|
99
|
+
return { ok: false, message: `Skill '${slug}' not found in ${LOCAL_SKILLS_DIR}.` };
|
|
100
|
+
}
|
|
101
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
message: `Skill '${slug}' removed from ${skillDir}. It will no longer be available on your next message.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Parse YAML frontmatter from a SKILL.md file. */
|
|
108
|
+
function parseFrontmatter(content) {
|
|
109
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
110
|
+
if (!match)
|
|
111
|
+
return { name: "", description: "" };
|
|
112
|
+
const frontmatter = match[1];
|
|
113
|
+
let name = "";
|
|
114
|
+
let description = "";
|
|
115
|
+
for (const line of frontmatter.split("\n")) {
|
|
116
|
+
const idx = line.indexOf(": ");
|
|
117
|
+
if (idx <= 0)
|
|
118
|
+
continue;
|
|
119
|
+
const key = line.slice(0, idx).trim();
|
|
120
|
+
const value = line.slice(idx + 2).trim();
|
|
121
|
+
if (key === "name")
|
|
122
|
+
name = value;
|
|
123
|
+
if (key === "description")
|
|
124
|
+
description = value;
|
|
125
|
+
}
|
|
126
|
+
return { name, description };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function getOrchestratorSystemMessage(memorySummary, opts) {
|
|
2
|
+
const memoryBlock = memorySummary
|
|
3
|
+
? `\n## Long-Term Memory\nThese are things you've been asked to remember or have noted as important:\n\n${memorySummary}\n`
|
|
4
|
+
: "";
|
|
5
|
+
const selfEditBlock = opts?.selfEditEnabled
|
|
6
|
+
? ""
|
|
7
|
+
: `\n## Self-Edit Protection
|
|
8
|
+
|
|
9
|
+
**You must NEVER modify your own source code.** This includes the NZB codebase, configuration files in the project repo, your own system message, skill definitions that ship with you, or any file that is part of the NZB application itself.
|
|
10
|
+
|
|
11
|
+
If you break yourself, you cannot repair yourself. If the user asks you to modify your own code, politely decline and explain that self-editing is disabled for safety. Suggest they make the changes manually or start NZB with \`--self-edit\` to temporarily allow it.
|
|
12
|
+
|
|
13
|
+
This restriction does NOT apply to:
|
|
14
|
+
- User project files (code the user asks you to work on)
|
|
15
|
+
- Learned skills in ~/.nzb/skills/ (these are user data, not NZB source)
|
|
16
|
+
- The ~/.nzb/.env config file (model switching, etc.)
|
|
17
|
+
- Any files outside the NZB installation directory
|
|
18
|
+
`;
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Your Architecture
|
|
23
|
+
|
|
24
|
+
You are a Node.js daemon process built with the Copilot SDK. Here's how you work:
|
|
25
|
+
|
|
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.
|
|
30
|
+
|
|
31
|
+
When no source tag is present, assume Telegram.
|
|
32
|
+
|
|
33
|
+
## Your Capabilities
|
|
34
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
## Your Role
|
|
42
|
+
|
|
43
|
+
You receive messages and decide how to handle them:
|
|
44
|
+
|
|
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.
|
|
49
|
+
|
|
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.
|
|
107
|
+
|
|
108
|
+
## Guidelines
|
|
109
|
+
|
|
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.
|
|
126
|
+
${selfEditBlock}${memoryBlock}`;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=system-message.js.map
|