@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Burke Holland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# NZB
|
|
2
|
+
|
|
3
|
+
AI orchestrator powered by [Copilot SDK](https://github.com/github/copilot-sdk) — control multiple Copilot CLI sessions from Telegram or a local terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -fsSL https://raw.githubusercontent.com/iletai/AI-Agent-Assistant/main/install.sh | bash
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install directly with npm:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g nzb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### 1. Run setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
nzb setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This creates `~/.nzb/` and walks you through configuration (Telegram bot token, etc.). Telegram is optional — you can use NZB with just the terminal UI.
|
|
26
|
+
|
|
27
|
+
### 2. Make sure Copilot CLI is authenticated
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
copilot login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Start NZB
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
nzb start
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 4. Connect via terminal
|
|
40
|
+
|
|
41
|
+
In a separate terminal:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
nzb tui
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 5. Talk to NZB
|
|
48
|
+
|
|
49
|
+
From Telegram or the TUI, just send natural language:
|
|
50
|
+
|
|
51
|
+
- "Start working on the auth bug in ~/dev/myapp"
|
|
52
|
+
- "What sessions are running?"
|
|
53
|
+
- "Check on the api-tests session"
|
|
54
|
+
- "Kill the auth-fix session"
|
|
55
|
+
- "What's the capital of France?"
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `nzb start` | Start the NZB daemon |
|
|
62
|
+
| `nzb tui` | Connect to the daemon via terminal |
|
|
63
|
+
| `nzb setup` | Interactive first-run configuration |
|
|
64
|
+
| `nzb update` | Check for and install updates |
|
|
65
|
+
| `nzb help` | Show available commands |
|
|
66
|
+
|
|
67
|
+
### Flags
|
|
68
|
+
|
|
69
|
+
| Flag | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `--self-edit` | Allow NZB to modify his own source code (use with `nzb start`) |
|
|
72
|
+
|
|
73
|
+
### TUI commands
|
|
74
|
+
|
|
75
|
+
| Command | Description |
|
|
76
|
+
|---------|-------------|
|
|
77
|
+
| `/model [name]` | Show or switch the current model |
|
|
78
|
+
| `/memory` | Show stored memories |
|
|
79
|
+
| `/skills` | List installed skills |
|
|
80
|
+
| `/workers` | List active worker sessions |
|
|
81
|
+
| `/copy` | Copy last response to clipboard |
|
|
82
|
+
| `/status` | Daemon health check |
|
|
83
|
+
| `/restart` | Restart the daemon |
|
|
84
|
+
| `/cancel` | Cancel the current in-flight message |
|
|
85
|
+
| `/clear` | Clear the screen |
|
|
86
|
+
| `/help` | Show help |
|
|
87
|
+
| `/quit` | Exit the TUI |
|
|
88
|
+
| `Escape` | Cancel a running response |
|
|
89
|
+
|
|
90
|
+
## How it Works
|
|
91
|
+
|
|
92
|
+
NZB runs a persistent **orchestrator Copilot session** — an always-on AI brain that receives your messages and decides how to handle them. For coding tasks, it spawns **worker Copilot sessions** in specific directories. For simple questions, it answers directly.
|
|
93
|
+
|
|
94
|
+
You can talk to NZB from:
|
|
95
|
+
|
|
96
|
+
- **Telegram** — remote access from your phone (authenticated by user ID)
|
|
97
|
+
- **TUI** — local terminal client (no auth needed)
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Telegram ──→ NZB Daemon ←── TUI
|
|
103
|
+
│
|
|
104
|
+
Orchestrator Session (Copilot SDK)
|
|
105
|
+
│
|
|
106
|
+
┌─────────┼─────────┐
|
|
107
|
+
Worker 1 Worker 2 Worker N
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **Daemon** (`nzb start`) — persistent service running Copilot SDK + Telegram bot + HTTP API
|
|
111
|
+
- **TUI** (`nzb tui`) — lightweight terminal client connecting to the daemon
|
|
112
|
+
- **Orchestrator** — long-running Copilot session with custom tools for session management
|
|
113
|
+
- **Workers** — child Copilot sessions for specific coding tasks
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Clone and install
|
|
119
|
+
git clone https://github.com/iletai/AI-Agent-Assistant.git
|
|
120
|
+
cd AI-Agent-Assistant
|
|
121
|
+
npm install
|
|
122
|
+
|
|
123
|
+
# Watch mode
|
|
124
|
+
npm run dev
|
|
125
|
+
|
|
126
|
+
# Build TypeScript
|
|
127
|
+
npm run build
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This project uses TypeScript, Zod for schema validation, and a simple file-based SQLite database for state management. The Copilot SDK is used to create and manage AI sessions, and the Telegram Bot API is used for remote messaging. Reference the source code for implementation details!
|
|
131
|
+
|
|
132
|
+
Thankful for <https://github.com/burkeholland/max> which inspired this project.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { config, persistModel } from "../config.js";
|
|
5
|
+
import { cancelCurrentMessage, getWorkers, sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
6
|
+
import { listSkills, removeSkill } from "../copilot/skills.js";
|
|
7
|
+
import { restartDaemon } from "../daemon.js";
|
|
8
|
+
import { API_TOKEN_PATH, ensureNZBHome } from "../paths.js";
|
|
9
|
+
import { searchMemories } from "../store/db.js";
|
|
10
|
+
import { sendPhoto } from "../telegram/bot.js";
|
|
11
|
+
// Ensure token file exists (generate on first run)
|
|
12
|
+
let apiToken = null;
|
|
13
|
+
try {
|
|
14
|
+
if (existsSync(API_TOKEN_PATH)) {
|
|
15
|
+
apiToken = readFileSync(API_TOKEN_PATH, "utf-8").trim();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
ensureNZBHome();
|
|
19
|
+
apiToken = randomBytes(32).toString("hex");
|
|
20
|
+
writeFileSync(API_TOKEN_PATH, apiToken, { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error(`[auth] Failed to load/generate API token: ${err}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(express.json());
|
|
29
|
+
// Bearer token authentication middleware (skip /status health check)
|
|
30
|
+
app.use((req, res, next) => {
|
|
31
|
+
if (!apiToken || req.path === "/status" || req.path === "/send-photo")
|
|
32
|
+
return next();
|
|
33
|
+
const auth = req.headers.authorization;
|
|
34
|
+
if (!auth || auth !== `Bearer ${apiToken}`) {
|
|
35
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
});
|
|
40
|
+
// Active SSE connections
|
|
41
|
+
const sseClients = new Map();
|
|
42
|
+
let connectionCounter = 0;
|
|
43
|
+
// Health check
|
|
44
|
+
app.get("/status", (_req, res) => {
|
|
45
|
+
res.json({
|
|
46
|
+
status: "ok",
|
|
47
|
+
workers: Array.from(getWorkers().values()).map((w) => ({
|
|
48
|
+
name: w.name,
|
|
49
|
+
workingDir: w.workingDir,
|
|
50
|
+
status: w.status,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// List worker sessions
|
|
55
|
+
app.get("/sessions", (_req, res) => {
|
|
56
|
+
const workers = Array.from(getWorkers().values()).map((w) => ({
|
|
57
|
+
name: w.name,
|
|
58
|
+
workingDir: w.workingDir,
|
|
59
|
+
status: w.status,
|
|
60
|
+
lastOutput: w.lastOutput?.slice(0, 500),
|
|
61
|
+
}));
|
|
62
|
+
res.json(workers);
|
|
63
|
+
});
|
|
64
|
+
// SSE stream for real-time responses
|
|
65
|
+
app.get("/stream", (req, res) => {
|
|
66
|
+
const connectionId = `tui-${++connectionCounter}`;
|
|
67
|
+
res.writeHead(200, {
|
|
68
|
+
"Content-Type": "text/event-stream",
|
|
69
|
+
"Cache-Control": "no-cache",
|
|
70
|
+
Connection: "keep-alive",
|
|
71
|
+
});
|
|
72
|
+
res.write(`data: ${JSON.stringify({ type: "connected", connectionId })}\n\n`);
|
|
73
|
+
sseClients.set(connectionId, res);
|
|
74
|
+
// Heartbeat to keep connection alive
|
|
75
|
+
const heartbeat = setInterval(() => {
|
|
76
|
+
res.write(`:ping\n\n`);
|
|
77
|
+
}, 20_000);
|
|
78
|
+
req.on("close", () => {
|
|
79
|
+
clearInterval(heartbeat);
|
|
80
|
+
sseClients.delete(connectionId);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// Send a message to the orchestrator
|
|
84
|
+
app.post("/message", (req, res) => {
|
|
85
|
+
const { prompt, connectionId } = req.body;
|
|
86
|
+
if (!prompt || typeof prompt !== "string") {
|
|
87
|
+
res.status(400).json({ error: "Missing 'prompt' in request body" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!connectionId || !sseClients.has(connectionId)) {
|
|
91
|
+
res.status(400).json({ error: "Missing or invalid 'connectionId'. Connect to /stream first." });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
sendToOrchestrator(prompt, { type: "tui", connectionId }, (text, done) => {
|
|
95
|
+
const sseRes = sseClients.get(connectionId);
|
|
96
|
+
if (sseRes) {
|
|
97
|
+
sseRes.write(`data: ${JSON.stringify({ type: done ? "message" : "delta", content: text })}\n\n`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
res.json({ status: "queued" });
|
|
101
|
+
});
|
|
102
|
+
// Cancel the current in-flight message
|
|
103
|
+
app.post("/cancel", async (_req, res) => {
|
|
104
|
+
const cancelled = await cancelCurrentMessage();
|
|
105
|
+
// Notify all SSE clients that the message was cancelled
|
|
106
|
+
for (const [, sseRes] of sseClients) {
|
|
107
|
+
sseRes.write(`data: ${JSON.stringify({ type: "cancelled" })}\n\n`);
|
|
108
|
+
}
|
|
109
|
+
res.json({ status: "ok", cancelled });
|
|
110
|
+
});
|
|
111
|
+
// Get or switch model
|
|
112
|
+
app.get("/model", (_req, res) => {
|
|
113
|
+
res.json({ model: config.copilotModel });
|
|
114
|
+
});
|
|
115
|
+
app.post("/model", async (req, res) => {
|
|
116
|
+
const { model } = req.body;
|
|
117
|
+
if (!model || typeof model !== "string") {
|
|
118
|
+
res.status(400).json({ error: "Missing 'model' in request body" });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Validate against available models before persisting
|
|
122
|
+
try {
|
|
123
|
+
const { getClient } = await import("../copilot/client.js");
|
|
124
|
+
const client = await getClient();
|
|
125
|
+
const models = await client.listModels();
|
|
126
|
+
const match = models.find((m) => m.id === model);
|
|
127
|
+
if (!match) {
|
|
128
|
+
const suggestions = models
|
|
129
|
+
.filter((m) => m.id.includes(model) || m.id.toLowerCase().includes(model.toLowerCase()))
|
|
130
|
+
.map((m) => m.id);
|
|
131
|
+
const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
132
|
+
res.status(400).json({ error: `Model '${model}' not found.${hint}` });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// If we can't validate (client not ready), allow the switch — it'll fail on next message if wrong
|
|
138
|
+
}
|
|
139
|
+
const previous = config.copilotModel;
|
|
140
|
+
config.copilotModel = model;
|
|
141
|
+
persistModel(model);
|
|
142
|
+
res.json({ previous, current: model });
|
|
143
|
+
});
|
|
144
|
+
// List memories
|
|
145
|
+
app.get("/memory", (_req, res) => {
|
|
146
|
+
const memories = searchMemories(undefined, undefined, 100);
|
|
147
|
+
res.json(memories);
|
|
148
|
+
});
|
|
149
|
+
// List skills
|
|
150
|
+
app.get("/skills", (_req, res) => {
|
|
151
|
+
const skills = listSkills();
|
|
152
|
+
res.json(skills);
|
|
153
|
+
});
|
|
154
|
+
// Remove a local skill
|
|
155
|
+
app.delete("/skills/:slug", (req, res) => {
|
|
156
|
+
const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
157
|
+
const result = removeSkill(slug);
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
res.status(400).json({ error: result.message });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
res.json({ ok: true, message: result.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Restart daemon
|
|
166
|
+
app.post("/restart", (_req, res) => {
|
|
167
|
+
res.json({ status: "restarting" });
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
restartDaemon().catch((err) => {
|
|
170
|
+
console.error("[nzb] Restart failed:", err);
|
|
171
|
+
});
|
|
172
|
+
}, 500);
|
|
173
|
+
});
|
|
174
|
+
// Send a photo to Telegram (protected by bearer token auth middleware)
|
|
175
|
+
app.post("/send-photo", async (req, res) => {
|
|
176
|
+
const { photo, caption } = req.body;
|
|
177
|
+
if (!photo || typeof photo !== "string") {
|
|
178
|
+
res.status(400).json({ error: "Missing 'photo' (file path or URL) in request body" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
await sendPhoto(photo, caption);
|
|
183
|
+
res.json({ status: "sent" });
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
res.status(500).json({ error: msg });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
export function startApiServer() {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const server = app.listen(config.apiPort, "127.0.0.1", () => {
|
|
193
|
+
console.log(`[nzb] HTTP API listening on http://127.0.0.1:${config.apiPort}`);
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
server.on("error", (err) => {
|
|
197
|
+
if (err.code === "EADDRINUSE") {
|
|
198
|
+
reject(new Error(`Port ${config.apiPort} is already in use. Is another NZB instance running?`));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
reject(err);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Broadcast a proactive message to all connected SSE clients (for background task completions). */
|
|
207
|
+
export function broadcastToSSE(text) {
|
|
208
|
+
for (const [, res] of sseClients) {
|
|
209
|
+
res.write(`data: ${JSON.stringify({ type: "message", content: text })}\n\n`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=server.js.map
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
function getVersion() {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
9
|
+
return pkg.version || "0.0.0";
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "0.0.0";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function printHelp() {
|
|
16
|
+
const version = getVersion();
|
|
17
|
+
console.log(`
|
|
18
|
+
nzb v${version} — AI orchestrator powered by Copilot SDK
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
nzb <command>
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
start Start the NZB daemon (Telegram bot + HTTP API)
|
|
25
|
+
tui Connect to the daemon via terminal UI
|
|
26
|
+
setup Interactive first-run configuration
|
|
27
|
+
update Check for updates and install the latest version
|
|
28
|
+
help Show this help message
|
|
29
|
+
|
|
30
|
+
Flags (start):
|
|
31
|
+
--self-edit Allow NZB to modify his own source code (off by default)
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
nzb start Start the daemon
|
|
35
|
+
nzb start --self-edit Start with self-edit enabled
|
|
36
|
+
nzb tui Open the terminal client
|
|
37
|
+
nzb setup Configure Telegram token and settings
|
|
38
|
+
`.trim());
|
|
39
|
+
}
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const command = args[0] || "help";
|
|
42
|
+
switch (command) {
|
|
43
|
+
case "start": {
|
|
44
|
+
// Parse flags for start command
|
|
45
|
+
const startFlags = args.slice(1);
|
|
46
|
+
if (startFlags.includes("--self-edit")) {
|
|
47
|
+
process.env.NZB_SELF_EDIT = "1";
|
|
48
|
+
}
|
|
49
|
+
await import("./daemon.js");
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "tui":
|
|
53
|
+
await import("./tui/index.js");
|
|
54
|
+
break;
|
|
55
|
+
case "setup":
|
|
56
|
+
await import("./setup.js");
|
|
57
|
+
break;
|
|
58
|
+
case "update": {
|
|
59
|
+
const { checkForUpdate, performUpdate } = await import("./update.js");
|
|
60
|
+
const check = await checkForUpdate();
|
|
61
|
+
if (!check.checkSucceeded) {
|
|
62
|
+
console.error("Warning: Could not reach the npm registry. Check your network and try again.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!check.updateAvailable) {
|
|
66
|
+
console.log(`nzb v${check.current} is already the latest version.`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
console.log(`Update available: v${check.current} → v${check.latest}`);
|
|
70
|
+
console.log("Installing...");
|
|
71
|
+
const result = await performUpdate();
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
console.log(`Updated to v${check.latest}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`Update failed: ${result.output}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "help":
|
|
82
|
+
case "--help":
|
|
83
|
+
case "-h":
|
|
84
|
+
printHelp();
|
|
85
|
+
break;
|
|
86
|
+
case "--version":
|
|
87
|
+
case "-v":
|
|
88
|
+
console.log(getVersion());
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
console.error(`Unknown command: ${command}\n`);
|
|
92
|
+
printHelp();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { config as loadEnv } from "dotenv";
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ENV_PATH, ensureNZBHome } from "./paths.js";
|
|
5
|
+
// Load from ~/.nzb/.env, fall back to cwd .env for dev
|
|
6
|
+
loadEnv({ path: ENV_PATH });
|
|
7
|
+
loadEnv(); // also check cwd for backwards compat
|
|
8
|
+
const configSchema = z.object({
|
|
9
|
+
TELEGRAM_BOT_TOKEN: z.string().min(1).optional(),
|
|
10
|
+
AUTHORIZED_USER_ID: z.string().min(1).optional(),
|
|
11
|
+
API_PORT: z.string().optional(),
|
|
12
|
+
COPILOT_MODEL: z.string().optional(),
|
|
13
|
+
WORKER_TIMEOUT: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
const raw = configSchema.parse(process.env);
|
|
16
|
+
const parsedUserId = raw.AUTHORIZED_USER_ID ? parseInt(raw.AUTHORIZED_USER_ID, 10) : undefined;
|
|
17
|
+
const parsedPort = parseInt(raw.API_PORT || "7777", 10);
|
|
18
|
+
if (parsedUserId !== undefined && (Number.isNaN(parsedUserId) || parsedUserId <= 0)) {
|
|
19
|
+
throw new Error(`AUTHORIZED_USER_ID must be a positive integer, got: "${raw.AUTHORIZED_USER_ID}"`);
|
|
20
|
+
}
|
|
21
|
+
if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
22
|
+
throw new Error(`API_PORT must be 1-65535, got: "${raw.API_PORT}"`);
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_WORKER_TIMEOUT_MS = 600_000; // 10 minutes
|
|
25
|
+
const parsedWorkerTimeout = raw.WORKER_TIMEOUT ? Number(raw.WORKER_TIMEOUT) : DEFAULT_WORKER_TIMEOUT_MS;
|
|
26
|
+
if (!Number.isInteger(parsedWorkerTimeout) || parsedWorkerTimeout <= 0) {
|
|
27
|
+
throw new Error(`WORKER_TIMEOUT must be a positive integer (ms), got: "${raw.WORKER_TIMEOUT}"`);
|
|
28
|
+
}
|
|
29
|
+
export const DEFAULT_MODEL = "claude-sonnet-4.6";
|
|
30
|
+
let _copilotModel = raw.COPILOT_MODEL || DEFAULT_MODEL;
|
|
31
|
+
export const config = {
|
|
32
|
+
telegramBotToken: raw.TELEGRAM_BOT_TOKEN,
|
|
33
|
+
authorizedUserId: parsedUserId,
|
|
34
|
+
apiPort: parsedPort,
|
|
35
|
+
workerTimeoutMs: parsedWorkerTimeout,
|
|
36
|
+
get copilotModel() {
|
|
37
|
+
return _copilotModel;
|
|
38
|
+
},
|
|
39
|
+
set copilotModel(model) {
|
|
40
|
+
_copilotModel = model;
|
|
41
|
+
},
|
|
42
|
+
get telegramEnabled() {
|
|
43
|
+
return !!this.telegramBotToken && this.authorizedUserId !== undefined;
|
|
44
|
+
},
|
|
45
|
+
get selfEditEnabled() {
|
|
46
|
+
return process.env.NZB_SELF_EDIT === "1";
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
/** Persist the current model choice to ~/.nzb/.env */
|
|
50
|
+
export function persistModel(model) {
|
|
51
|
+
ensureNZBHome();
|
|
52
|
+
try {
|
|
53
|
+
const content = readFileSync(ENV_PATH, "utf-8");
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
let found = false;
|
|
56
|
+
const updated = lines.map((line) => {
|
|
57
|
+
if (line.startsWith("COPILOT_MODEL=")) {
|
|
58
|
+
found = true;
|
|
59
|
+
return `COPILOT_MODEL=${model}`;
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
});
|
|
63
|
+
if (!found)
|
|
64
|
+
updated.push(`COPILOT_MODEL=${model}`);
|
|
65
|
+
writeFileSync(ENV_PATH, updated.join("\n"));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// File doesn't exist — create it
|
|
69
|
+
writeFileSync(ENV_PATH, `COPILOT_MODEL=${model}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
2
|
+
let client;
|
|
3
|
+
export async function getClient() {
|
|
4
|
+
if (!client) {
|
|
5
|
+
client = new CopilotClient({
|
|
6
|
+
autoStart: true,
|
|
7
|
+
autoRestart: true,
|
|
8
|
+
});
|
|
9
|
+
await client.start();
|
|
10
|
+
}
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
/** Tear down the existing client and create a fresh one. */
|
|
14
|
+
export async function resetClient() {
|
|
15
|
+
if (client) {
|
|
16
|
+
try {
|
|
17
|
+
await client.stop();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* best-effort */
|
|
21
|
+
}
|
|
22
|
+
client = undefined;
|
|
23
|
+
}
|
|
24
|
+
return getClient();
|
|
25
|
+
}
|
|
26
|
+
export async function stopClient() {
|
|
27
|
+
if (client) {
|
|
28
|
+
await client.stop();
|
|
29
|
+
client = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
/**
|
|
5
|
+
* Load MCP server configs from ~/.copilot/mcp-config.json.
|
|
6
|
+
* Returns an empty record if the file doesn't exist or is invalid.
|
|
7
|
+
*/
|
|
8
|
+
export function loadMcpConfig() {
|
|
9
|
+
const configPath = join(homedir(), ".copilot", "mcp-config.json");
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
|
|
14
|
+
return parsed.mcpServers;
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=mcp-config.js.map
|