@iletai/nzb 1.1.4 → 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 +17 -7
- package/dist/copilot/orchestrator.js +77 -39
- package/dist/copilot/system-message.js +33 -92
- package/dist/store/db.js +35 -6
- package/dist/telegram/bot.js +65 -3
- package/dist/telegram/formatter.js +20 -1
- package/package.json +64 -63
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
|
|
56
|
-
export function
|
|
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(
|
|
70
|
+
if (line.startsWith(`${key}=`)) {
|
|
64
71
|
found = true;
|
|
65
|
-
return
|
|
72
|
+
return `${key}=${value}`;
|
|
66
73
|
}
|
|
67
74
|
return line;
|
|
68
75
|
});
|
|
69
76
|
if (!found)
|
|
70
|
-
updated.push(
|
|
77
|
+
updated.push(`${key}=${value}`);
|
|
71
78
|
writeFileSync(ENV_PATH, updated.join("\n"));
|
|
72
79
|
}
|
|
73
80
|
catch {
|
|
74
|
-
|
|
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
|
|
@@ -39,22 +39,29 @@ let currentSourceChannel;
|
|
|
39
39
|
export function getCurrentSourceChannel() {
|
|
40
40
|
return currentSourceChannel;
|
|
41
41
|
}
|
|
42
|
+
// Cache tools to avoid recreating 15+ tool objects on every session create
|
|
43
|
+
let cachedTools;
|
|
44
|
+
let cachedToolsClientRef;
|
|
42
45
|
function getSessionConfig() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
55
62
|
const mcpServers = loadMcpConfig();
|
|
56
63
|
const skillDirectories = getSkillDirectories();
|
|
57
|
-
return { tools, mcpServers, skillDirectories };
|
|
64
|
+
return { tools: cachedTools, mcpServers, skillDirectories };
|
|
58
65
|
}
|
|
59
66
|
/** Feed a background worker result into the orchestrator as a new turn. */
|
|
60
67
|
export function feedBackgroundResult(workerName, result) {
|
|
@@ -68,6 +75,10 @@ export function feedBackgroundResult(workerName, result) {
|
|
|
68
75
|
}
|
|
69
76
|
});
|
|
70
77
|
}
|
|
78
|
+
/** Check if a queued message is a background message. */
|
|
79
|
+
function isBackgroundMessage(item) {
|
|
80
|
+
return item.sourceChannel === undefined;
|
|
81
|
+
}
|
|
71
82
|
function sleep(ms) {
|
|
72
83
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
84
|
}
|
|
@@ -102,9 +113,12 @@ function startHealthCheck() {
|
|
|
102
113
|
const state = copilotClient.getState();
|
|
103
114
|
if (state !== "connected") {
|
|
104
115
|
console.log(`[nzb] Health check: client state is '${state}', resetting…`);
|
|
116
|
+
const previousClient = copilotClient;
|
|
105
117
|
await ensureClient();
|
|
106
|
-
//
|
|
107
|
-
|
|
118
|
+
// Only invalidate session if the underlying client actually changed
|
|
119
|
+
if (copilotClient !== previousClient) {
|
|
120
|
+
orchestratorSession = undefined;
|
|
121
|
+
}
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
catch (err) {
|
|
@@ -197,35 +211,35 @@ async function createOrResumeSession() {
|
|
|
197
211
|
setState(ORCHESTRATOR_SESSION_KEY, session.sessionId);
|
|
198
212
|
console.log(`[nzb] Created orchestrator session ${session.sessionId.slice(0, 8)}…`);
|
|
199
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
|
|
200
215
|
const recentHistory = getRecentConversation(10);
|
|
201
216
|
if (recentHistory) {
|
|
202
|
-
console.log(`[nzb] Injecting recent conversation context into new session`);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}, 60_000);
|
|
207
|
-
}
|
|
208
|
-
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) => {
|
|
209
221
|
console.log(`[nzb] Context recovery injection failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
210
|
-
}
|
|
222
|
+
});
|
|
211
223
|
}
|
|
212
224
|
return session;
|
|
213
225
|
}
|
|
214
226
|
export async function initOrchestrator(client) {
|
|
215
227
|
copilotClient = client;
|
|
216
228
|
const { mcpServers, skillDirectories } = getSessionConfig();
|
|
217
|
-
// Validate configured model against available models
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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}`);
|
|
225
242
|
}
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
console.log(`[nzb] Could not validate model (will use '${config.copilotModel}' as-is): ${err instanceof Error ? err.message : err}`);
|
|
229
243
|
}
|
|
230
244
|
console.log(`[nzb] Loading ${Object.keys(mcpServers).length} MCP server(s): ${Object.keys(mcpServers).join(", ") || "(none)"}`);
|
|
231
245
|
console.log(`[nzb] Skill directories: ${skillDirectories.join(", ") || "(none)"}`);
|
|
@@ -247,7 +261,9 @@ async function executeOnSession(prompt, callback, onToolEvent) {
|
|
|
247
261
|
let toolCallExecuted = false;
|
|
248
262
|
const unsubToolStart = session.on("tool.execution_start", (event) => {
|
|
249
263
|
const toolName = event?.data?.toolName || event?.data?.name || "tool";
|
|
250
|
-
|
|
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 });
|
|
251
267
|
});
|
|
252
268
|
const unsubToolDone = session.on("tool.execution_complete", (event) => {
|
|
253
269
|
toolCallExecuted = true;
|
|
@@ -269,7 +285,7 @@ async function executeOnSession(prompt, callback, onToolEvent) {
|
|
|
269
285
|
console.error(`[nzb] Session error event: ${errMsg}`);
|
|
270
286
|
});
|
|
271
287
|
try {
|
|
272
|
-
const result = await session.sendAndWait({ prompt },
|
|
288
|
+
const result = await session.sendAndWait({ prompt }, 60_000);
|
|
273
289
|
const finalContent = result?.data?.content || accumulated || "(No response)";
|
|
274
290
|
return finalContent;
|
|
275
291
|
}
|
|
@@ -327,17 +343,39 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent)
|
|
|
327
343
|
const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
|
|
328
344
|
logMessage("in", sourceLabel, prompt);
|
|
329
345
|
// Tag the prompt with its source channel
|
|
330
|
-
|
|
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
|
+
}
|
|
331
355
|
// Log role: background events are "system", user messages are "user"
|
|
332
356
|
const logRole = source.type === "background" ? "system" : "user";
|
|
333
357
|
// Determine the source channel for worker origin tracking
|
|
334
358
|
const sourceChannel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : undefined;
|
|
335
|
-
// Enqueue
|
|
359
|
+
// Enqueue with priority — user messages go before background messages
|
|
336
360
|
void (async () => {
|
|
337
361
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
338
362
|
try {
|
|
339
363
|
const finalContent = await new Promise((resolve, reject) => {
|
|
340
|
-
|
|
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
|
+
}
|
|
341
379
|
processQueue();
|
|
342
380
|
});
|
|
343
381
|
// Deliver response to user FIRST, then log best-effort
|
|
@@ -20,110 +20,51 @@ This restriction does NOT apply to:
|
|
|
20
20
|
const modelInfo = opts?.currentModel ? ` You are currently using the \`${opts.currentModel}\` model.` : "";
|
|
21
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.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Architecture
|
|
24
24
|
|
|
25
|
-
|
|
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.
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
- **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.
|
|
29
|
-
- **Background tasks**: Messages tagged \`[via background]\` are results from worker sessions you dispatched. Summarize and relay these to the user.
|
|
30
|
-
- **HTTP API**: You expose a local API on port 7777 for programmatic access.
|
|
31
|
+
No source tag = assume Telegram.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
## Role
|
|
33
34
|
|
|
34
|
-
|
|
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.
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
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.
|
|
38
|
-
3. **Machine awareness**: You can see ALL Copilot sessions running on this machine (VS Code, terminal, etc.) and attach to them.
|
|
39
|
-
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.
|
|
40
|
-
5. **MCP servers**: You connect to MCP tool servers for extended capabilities.
|
|
39
|
+
## Workers
|
|
41
40
|
|
|
42
|
-
|
|
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.
|
|
43
45
|
|
|
44
|
-
|
|
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.
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
- **Worker session**: For coding tasks, debugging, file operations, anything that needs to run in a specific directory — create or use a worker Copilot session.
|
|
48
|
-
- **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.
|
|
49
|
-
- **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
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
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."
|
|
56
|
-
2. You do NOT wait for the worker to finish. The tool returns immediately.
|
|
57
|
-
3. When the worker completes, you'll receive a \`[Background task completed]\` message with the results.
|
|
58
|
-
4. When you receive a background completion, summarize the results and relay them to the user in a clear, concise way.
|
|
59
|
-
|
|
60
|
-
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.
|
|
61
|
-
|
|
62
|
-
### Speed & Concurrency
|
|
63
|
-
|
|
64
|
-
**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:
|
|
65
|
-
|
|
66
|
-
- **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.
|
|
67
|
-
- **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.
|
|
68
|
-
- **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.
|
|
69
|
-
|
|
70
|
-
## Tool Usage
|
|
71
|
-
|
|
72
|
-
### Session Management
|
|
73
|
-
- \`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.
|
|
74
|
-
- \`send_to_worker\`: Send a prompt to an existing worker session. Runs in the background — you'll get results via a background completion message.
|
|
75
|
-
- \`list_sessions\`: List all active worker sessions with their status and working directory.
|
|
76
|
-
- \`check_session_status\`: Get detailed status of a specific worker session.
|
|
77
|
-
- \`kill_session\`: Terminate a worker session when it's no longer needed.
|
|
78
|
-
|
|
79
|
-
### Machine Session Discovery
|
|
80
|
-
- \`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?"
|
|
81
|
-
- \`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.
|
|
82
|
-
|
|
83
|
-
### Skills
|
|
84
|
-
- \`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.
|
|
85
|
-
- \`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.
|
|
86
|
-
|
|
87
|
-
### Model Management
|
|
88
|
-
- \`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?"
|
|
89
|
-
- \`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".
|
|
90
|
-
|
|
91
|
-
### Self-Management
|
|
92
|
-
- \`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.
|
|
93
|
-
|
|
94
|
-
### Memory
|
|
95
|
-
- \`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).
|
|
96
|
-
- \`recall\`: Search long-term memory by keyword and/or category. Use when you need to look up something the user told you before.
|
|
97
|
-
- \`forget\`: Remove a specific memory by ID. Use when the user asks to forget something or a memory is outdated.
|
|
98
|
-
|
|
99
|
-
**Learning workflow**: When the user asks you to do something you don't have a skill for:
|
|
100
|
-
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.
|
|
101
|
-
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.
|
|
102
|
-
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.
|
|
103
|
-
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.
|
|
104
|
-
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?"
|
|
105
|
-
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.
|
|
106
|
-
|
|
107
|
-
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.
|
|
108
57
|
|
|
109
58
|
## Guidelines
|
|
110
59
|
|
|
111
|
-
1.
|
|
112
|
-
2.
|
|
113
|
-
3.
|
|
114
|
-
4.
|
|
115
|
-
5.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
8. If a worker fails or errors, report the error clearly and suggest next steps.
|
|
120
|
-
9. Expand shorthand paths: "~/dev/myapp" → the user's home directory + "/dev/myapp".
|
|
121
|
-
10. Be conversational and human. You're a capable assistant, not a robot. You're NZB.
|
|
122
|
-
11. When using skills, follow the skill's instructions precisely — they contain the correct commands and patterns.
|
|
123
|
-
12. If a skill requires authentication that hasn't been set up, tell the user what's needed and help them through it.
|
|
124
|
-
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.
|
|
125
|
-
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.
|
|
126
|
-
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>"}'\`
|
|
127
68
|
${selfEditBlock}${memoryBlock}`;
|
|
128
69
|
}
|
|
129
70
|
//# sourceMappingURL=system-message.js.map
|
package/dist/store/db.js
CHANGED
|
@@ -68,6 +68,16 @@ export function getDb() {
|
|
|
68
68
|
}
|
|
69
69
|
// Prune conversation log at startup
|
|
70
70
|
db.prepare(`DELETE FROM conversation_log WHERE id NOT IN (SELECT id FROM conversation_log ORDER BY id DESC LIMIT 200)`).run();
|
|
71
|
+
// FTS5 virtual table for fast full-text memory search
|
|
72
|
+
try {
|
|
73
|
+
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(content, content=memories, content_rowid=id)`);
|
|
74
|
+
// Populate FTS index from existing data
|
|
75
|
+
db.exec(`INSERT OR IGNORE INTO memories_fts(memories_fts) VALUES('rebuild')`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// FTS5 may not be available — will fall back to LIKE
|
|
79
|
+
console.log("[nzb] FTS5 not available, using LIKE fallback for memory search");
|
|
80
|
+
}
|
|
71
81
|
// Initialize cached prepared statements for hot-path operations
|
|
72
82
|
stmtCache = {
|
|
73
83
|
getState: db.prepare(`SELECT value FROM nzb_state WHERE key = ?`),
|
|
@@ -131,9 +141,31 @@ export function addMemory(category, content, source = "user") {
|
|
|
131
141
|
const result = stmtCache.addMemory.run(category, content, source);
|
|
132
142
|
return result.lastInsertRowid;
|
|
133
143
|
}
|
|
134
|
-
/** Search memories by keyword and/or category. */
|
|
144
|
+
/** Search memories by keyword and/or category. Uses FTS5 when available, falls back to LIKE. */
|
|
135
145
|
export function searchMemories(keyword, category, limit = 20) {
|
|
136
146
|
const db = getDb();
|
|
147
|
+
// Try FTS5 first for keyword search (much faster than LIKE)
|
|
148
|
+
if (keyword) {
|
|
149
|
+
try {
|
|
150
|
+
const catFilter = category ? `AND m.category = ?` : "";
|
|
151
|
+
const params = [keyword + "*"];
|
|
152
|
+
if (category)
|
|
153
|
+
params.push(category);
|
|
154
|
+
params.push(limit);
|
|
155
|
+
const rows = db
|
|
156
|
+
.prepare(`SELECT m.id, m.category, m.content, m.source, m.created_at
|
|
157
|
+
FROM memories_fts f
|
|
158
|
+
JOIN memories m ON f.rowid = m.id
|
|
159
|
+
WHERE memories_fts MATCH ? ${catFilter}
|
|
160
|
+
ORDER BY rank LIMIT ?`)
|
|
161
|
+
.all(...params);
|
|
162
|
+
return rows;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// FTS5 not available — fall through to LIKE
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Fallback: LIKE-based search
|
|
137
169
|
const conditions = [];
|
|
138
170
|
const params = [];
|
|
139
171
|
if (keyword) {
|
|
@@ -149,11 +181,8 @@ export function searchMemories(keyword, category, limit = 20) {
|
|
|
149
181
|
const rows = db
|
|
150
182
|
.prepare(`SELECT id, category, content, source, created_at FROM memories ${where} ORDER BY last_accessed DESC LIMIT ?`)
|
|
151
183
|
.all(...params);
|
|
152
|
-
// Update last_accessed
|
|
153
|
-
|
|
154
|
-
const placeholders = rows.map(() => "?").join(",");
|
|
155
|
-
db.prepare(`UPDATE memories SET last_accessed = CURRENT_TIMESTAMP WHERE id IN (${placeholders})`).run(...rows.map((r) => r.id));
|
|
156
|
-
}
|
|
184
|
+
// Update last_accessed only when explicitly requested, not on every search
|
|
185
|
+
// (removed automatic last_accessed update to avoid write side effects on reads)
|
|
157
186
|
return rows;
|
|
158
187
|
}
|
|
159
188
|
/** Remove a memory by ID. */
|
package/dist/telegram/bot.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { appendFileSync } from "fs";
|
|
1
2
|
import { Bot, InlineKeyboard } from "grammy";
|
|
2
3
|
import { Agent as HttpsAgent } from "https";
|
|
3
|
-
import { config, persistModel } from "../config.js";
|
|
4
|
+
import { config, persistEnvVar, persistModel } from "../config.js";
|
|
4
5
|
import { cancelCurrentMessage, getQueueSize, getWorkers, sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
5
6
|
import { listSkills } from "../copilot/skills.js";
|
|
6
7
|
import { restartDaemon } from "../daemon.js";
|
|
7
8
|
import { searchMemories } from "../store/db.js";
|
|
8
|
-
import { chunkMessage, toTelegramMarkdown } from "./formatter.js";
|
|
9
|
+
import { chunkMessage, formatToolSummaryExpandable, toTelegramMarkdown } from "./formatter.js";
|
|
9
10
|
let bot;
|
|
10
11
|
const startedAt = Date.now();
|
|
11
12
|
// Inline keyboard menu for quick actions
|
|
@@ -17,6 +18,8 @@ const mainMenu = new InlineKeyboard()
|
|
|
17
18
|
.text("🧠 Skills", "action:skills")
|
|
18
19
|
.row()
|
|
19
20
|
.text("🗂 Memory", "action:memory")
|
|
21
|
+
.text("⚙️ Settings", "action:settings")
|
|
22
|
+
.row()
|
|
20
23
|
.text("❌ Cancel", "action:cancel");
|
|
21
24
|
// Direct-connection HTTPS agent for Telegram API requests.
|
|
22
25
|
// This bypasses corporate proxy (HTTP_PROXY/HTTPS_PROXY env vars) without
|
|
@@ -58,6 +61,7 @@ export function createBot() {
|
|
|
58
61
|
"/skills — List installed skills\n" +
|
|
59
62
|
"/workers — List active worker sessions\n" +
|
|
60
63
|
"/status — Show system status\n" +
|
|
64
|
+
"/settings — Bot settings\n" +
|
|
61
65
|
"/restart — Restart NZB\n" +
|
|
62
66
|
"/help — Show this help", { reply_markup: mainMenu }));
|
|
63
67
|
bot.command("cancel", async (ctx) => {
|
|
@@ -148,6 +152,19 @@ export function createBot() {
|
|
|
148
152
|
});
|
|
149
153
|
}, 500);
|
|
150
154
|
});
|
|
155
|
+
// /settings — show toggleable settings with inline keyboard
|
|
156
|
+
const buildSettingsKeyboard = () => new InlineKeyboard()
|
|
157
|
+
.text(`${config.showReasoning ? "✅" : "❌"} Show Reasoning`, "setting:toggle:reasoning")
|
|
158
|
+
.row()
|
|
159
|
+
.text("🔙 Back to Menu", "action:menu");
|
|
160
|
+
const buildSettingsText = () => "⚙️ Settings\n\n" +
|
|
161
|
+
`🔧 Show Reasoning: ${config.showReasoning ? "✅ ON" : "❌ OFF"}\n` +
|
|
162
|
+
` └ Hiển thị tools đã dùng + thời gian cuối mỗi phản hồi\n\n` +
|
|
163
|
+
`🤖 Model: ${config.copilotModel}\n` +
|
|
164
|
+
` └ Dùng /model <name> để đổi`;
|
|
165
|
+
bot.command("settings", async (ctx) => {
|
|
166
|
+
await ctx.reply(buildSettingsText(), { reply_markup: buildSettingsKeyboard() });
|
|
167
|
+
});
|
|
151
168
|
// Callback query handlers for inline menu buttons
|
|
152
169
|
bot.callbackQuery("action:status", async (ctx) => {
|
|
153
170
|
await ctx.answerCallbackQuery();
|
|
@@ -208,6 +225,20 @@ export function createBot() {
|
|
|
208
225
|
const cancelled = await cancelCurrentMessage();
|
|
209
226
|
await ctx.reply(cancelled ? "Cancelled." : "Nothing to cancel.");
|
|
210
227
|
});
|
|
228
|
+
bot.callbackQuery("action:settings", async (ctx) => {
|
|
229
|
+
await ctx.answerCallbackQuery();
|
|
230
|
+
await ctx.reply(buildSettingsText(), { reply_markup: buildSettingsKeyboard() });
|
|
231
|
+
});
|
|
232
|
+
bot.callbackQuery("action:menu", async (ctx) => {
|
|
233
|
+
await ctx.answerCallbackQuery();
|
|
234
|
+
await ctx.editMessageText("NZB Menu:", { reply_markup: mainMenu });
|
|
235
|
+
});
|
|
236
|
+
bot.callbackQuery("setting:toggle:reasoning", async (ctx) => {
|
|
237
|
+
config.showReasoning = !config.showReasoning;
|
|
238
|
+
persistEnvVar("SHOW_REASONING", config.showReasoning ? "true" : "false");
|
|
239
|
+
await ctx.answerCallbackQuery(`Reasoning ${config.showReasoning ? "ON" : "OFF"}`);
|
|
240
|
+
await ctx.editMessageText(buildSettingsText(), { reply_markup: buildSettingsKeyboard() });
|
|
241
|
+
});
|
|
211
242
|
// Handle all text messages — progressive streaming with tool event feedback
|
|
212
243
|
bot.on("message:text", async (ctx) => {
|
|
213
244
|
const chatId = ctx.chat.id;
|
|
@@ -246,6 +277,7 @@ export function createBot() {
|
|
|
246
277
|
let lastEditTime = 0;
|
|
247
278
|
let lastEditedText = "";
|
|
248
279
|
let currentToolName;
|
|
280
|
+
const toolHistory = [];
|
|
249
281
|
let finalized = false;
|
|
250
282
|
let editChain = Promise.resolve();
|
|
251
283
|
const EDIT_INTERVAL_MS = 5000;
|
|
@@ -292,12 +324,24 @@ export function createBot() {
|
|
|
292
324
|
.catch(() => { });
|
|
293
325
|
};
|
|
294
326
|
const onToolEvent = (event) => {
|
|
327
|
+
try {
|
|
328
|
+
appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} BOT ${event.type} ${event.toolName}\n`);
|
|
329
|
+
}
|
|
330
|
+
catch { }
|
|
331
|
+
console.log(`[nzb] Bot received tool event: ${event.type} ${event.toolName}`);
|
|
295
332
|
if (event.type === "tool_start") {
|
|
296
333
|
currentToolName = event.toolName;
|
|
334
|
+
toolHistory.push({ name: event.toolName, startTime: Date.now() });
|
|
297
335
|
const existingText = lastEditedText.replace(/^🔧 .*\n\n/, "");
|
|
298
336
|
enqueueEdit(`🔧 ${event.toolName}\n\n${existingText}`.trim() || `🔧 ${event.toolName}`);
|
|
299
337
|
}
|
|
300
338
|
else if (event.type === "tool_complete") {
|
|
339
|
+
for (let i = toolHistory.length - 1; i >= 0; i--) {
|
|
340
|
+
if (toolHistory[i].name === event.toolName && toolHistory[i].durationMs === undefined) {
|
|
341
|
+
toolHistory[i].durationMs = Date.now() - toolHistory[i].startTime;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
301
345
|
currentToolName = undefined;
|
|
302
346
|
}
|
|
303
347
|
};
|
|
@@ -341,7 +385,24 @@ export function createBot() {
|
|
|
341
385
|
return;
|
|
342
386
|
}
|
|
343
387
|
const formatted = toTelegramMarkdown(text);
|
|
344
|
-
|
|
388
|
+
let fullFormatted = formatted;
|
|
389
|
+
try {
|
|
390
|
+
appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} FINAL showReasoning=${config.showReasoning} toolHistory=${toolHistory.length}\n`);
|
|
391
|
+
}
|
|
392
|
+
catch { }
|
|
393
|
+
if (config.showReasoning && toolHistory.length > 0) {
|
|
394
|
+
const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs })));
|
|
395
|
+
fullFormatted += expandable;
|
|
396
|
+
try {
|
|
397
|
+
appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} EXPANDABLE=${JSON.stringify(expandable)}\n`);
|
|
398
|
+
}
|
|
399
|
+
catch { }
|
|
400
|
+
try {
|
|
401
|
+
appendFileSync("/tmp/nzb-tool-debug.log", `${new Date().toISOString()} FULL_LAST200=${JSON.stringify(fullFormatted.slice(-200))}\n`);
|
|
402
|
+
}
|
|
403
|
+
catch { }
|
|
404
|
+
}
|
|
405
|
+
const chunks = chunkMessage(fullFormatted);
|
|
345
406
|
const fallbackChunks = chunkMessage(text);
|
|
346
407
|
// Single chunk: edit placeholder in place
|
|
347
408
|
if (placeholderMsgId && chunks.length === 1) {
|
|
@@ -439,6 +500,7 @@ export async function startBot() {
|
|
|
439
500
|
{ command: "workers", description: "List active workers" },
|
|
440
501
|
{ command: "skills", description: "List installed skills" },
|
|
441
502
|
{ command: "memory", description: "Show stored memories" },
|
|
503
|
+
{ command: "settings", description: "Bot settings" },
|
|
442
504
|
{ command: "restart", description: "Restart NZB" },
|
|
443
505
|
]);
|
|
444
506
|
console.log("[nzb] Bot commands registered with Telegram");
|
|
@@ -42,7 +42,7 @@ export function chunkMessage(text) {
|
|
|
42
42
|
/**
|
|
43
43
|
* Escape special characters for Telegram MarkdownV2 plain text segments.
|
|
44
44
|
*/
|
|
45
|
-
function escapeSegment(text) {
|
|
45
|
+
export function escapeSegment(text) {
|
|
46
46
|
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -147,4 +147,23 @@ export function toTelegramMarkdown(text) {
|
|
|
147
147
|
out = out.replace(/\n{3,}/g, "\n\n");
|
|
148
148
|
return out.trim();
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Format tool call info as a Telegram MarkdownV2 expandable blockquote.
|
|
152
|
+
* First line (title) is always visible, tool list expands on tap.
|
|
153
|
+
*/
|
|
154
|
+
export function formatToolSummaryExpandable(toolCalls) {
|
|
155
|
+
if (toolCalls.length === 0)
|
|
156
|
+
return "";
|
|
157
|
+
const lines = toolCalls.map((t) => {
|
|
158
|
+
const name = escapeSegment(t.name);
|
|
159
|
+
const dur = t.durationMs !== undefined
|
|
160
|
+
? ` \\(${escapeSegment((t.durationMs / 1000).toFixed(1) + "s")}\\)`
|
|
161
|
+
: "";
|
|
162
|
+
return `${escapeSegment("• ")}${name}${dur}`;
|
|
163
|
+
});
|
|
164
|
+
const header = escapeSegment("🔧 Tools used:");
|
|
165
|
+
const toolList = lines.join(`\n>`);
|
|
166
|
+
// Expandable: header visible, tool list hidden until tapped
|
|
167
|
+
return `\n\n**>${header}\n>${toolList}||`;
|
|
168
|
+
}
|
|
150
169
|
//# sourceMappingURL=formatter.js.map
|
package/package.json
CHANGED
|
@@ -1,64 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
2
|
+
"name": "@iletai/nzb",
|
|
3
|
+
"version": "1.1.5",
|
|
4
|
+
"description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nzb": "dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*.js",
|
|
13
|
+
"scripts/fix-esm-imports.cjs",
|
|
14
|
+
"skills/",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"postinstall": "node scripts/fix-esm-imports.cjs",
|
|
20
|
+
"daemon": "tsx src/daemon.ts",
|
|
21
|
+
"tui": "tsx src/tui/index.ts",
|
|
22
|
+
"dev": "tsx --watch src/daemon.ts",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check .",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"copilot",
|
|
32
|
+
"telegram",
|
|
33
|
+
"orchestrator",
|
|
34
|
+
"ai",
|
|
35
|
+
"cli"
|
|
36
|
+
],
|
|
37
|
+
"author": "iletai",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/iletai/AI-Agent-Assistant.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/iletai/AI-Agent-Assistant#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/iletai/AI-Agent-Assistant/issues"
|
|
46
|
+
},
|
|
47
|
+
"type": "module",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@github/copilot-sdk": "^0.1.26",
|
|
50
|
+
"better-sqlite3": "^12.6.2",
|
|
51
|
+
"dotenv": "^17.3.1",
|
|
52
|
+
"express": "^5.2.1",
|
|
53
|
+
"grammy": "^1.40.0",
|
|
54
|
+
"zod": "^4.3.6"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
58
|
+
"@types/express": "^5.0.6",
|
|
59
|
+
"@types/node": "^25.3.0",
|
|
60
|
+
"prettier": "^3.8.1",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.1.0"
|
|
64
|
+
}
|
|
65
|
+
}
|