blun-king-cli 5.0.4 → 5.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/blun-cli.js +3041 -704
- package/package.json +1 -1
package/blun-cli.js
CHANGED
|
@@ -1,848 +1,3185 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// BLUN King CLI — Interactive Console
|
|
2
3
|
const readline = require("readline");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const os = require("os");
|
|
5
4
|
const fs = require("fs");
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync, spawn } = require("child_process");
|
|
7
|
+
const https = require("https");
|
|
8
|
+
const http = require("http");
|
|
9
|
+
|
|
10
|
+
// ── Config ──
|
|
11
|
+
var _rawHome = process.env.BLUN_HOME || process.env.REAL_HOME || require("os").homedir();
|
|
12
|
+
// Strip .claude-telegram-profile suffix if present (Claude Code Telegram session override)
|
|
13
|
+
const HOME = _rawHome.replace(/[\/\\]\.claude-telegram-profile$/, "");
|
|
14
|
+
const CONFIG_DIR = path.join(HOME, ".blun");
|
|
15
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
16
|
+
const MEMORY_DIR = path.join(CONFIG_DIR, "memory");
|
|
17
|
+
const SKILLS_DIR = path.join(CONFIG_DIR, "skills");
|
|
18
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, "history.json");
|
|
19
|
+
const LOG_FILE = path.join(CONFIG_DIR, "cli.log");
|
|
20
|
+
const PKG_VERSION = (() => { try { return require(path.join(__dirname, "..", "package.json")).version; } catch(e) { return "1.7.0"; } })();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
24
|
+
if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
25
|
+
|
|
26
|
+
// ── Default Config ──
|
|
20
27
|
function loadConfig() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
var defaults = {
|
|
29
|
+
auth: { type: "api_key", api_key: "", oauth_token: "", oauth_expires: null },
|
|
30
|
+
api: { base_url: "http://176.9.158.30:3200", timeout: 300000, key: "" },
|
|
31
|
+
telegram: { enabled: false, bot_token: "", chat_id: "" },
|
|
32
|
+
model: "blun-king-v100",
|
|
33
|
+
workdir: process.cwd(),
|
|
34
|
+
theme: "dark",
|
|
35
|
+
verbose: false,
|
|
36
|
+
watchdog: { enabled: false, interval: 600 }
|
|
37
|
+
};
|
|
38
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
39
|
+
try {
|
|
40
|
+
var saved = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
41
|
+
// Deep merge saved over defaults
|
|
42
|
+
Object.keys(defaults).forEach(function(k) {
|
|
43
|
+
if (typeof defaults[k] === "object" && defaults[k] !== null && saved[k]) {
|
|
44
|
+
defaults[k] = Object.assign({}, defaults[k], saved[k]);
|
|
45
|
+
} else if (saved[k] !== undefined) {
|
|
46
|
+
defaults[k] = saved[k];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch(e) {}
|
|
24
50
|
}
|
|
51
|
+
return defaults;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function saveConfig(cfg) {
|
|
55
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
var config = loadConfig();
|
|
59
|
+
|
|
60
|
+
// ── Colors ──
|
|
61
|
+
const C = {
|
|
62
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", italic: "\x1b[3m",
|
|
63
|
+
red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
|
|
64
|
+
blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m",
|
|
65
|
+
gray: "\x1b[90m", brightBlue: "\x1b[94m", brightCyan: "\x1b[96m", brightWhite: "\x1b[97m",
|
|
66
|
+
bg_blue: "\x1b[44m", bg_green: "\x1b[42m", bg_gray: "\x1b[100m", bg_darkBlue: "\x1b[48;5;17m"
|
|
67
|
+
};
|
|
68
|
+
// Box drawing helpers
|
|
69
|
+
const BOX = {
|
|
70
|
+
tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│",
|
|
71
|
+
sep: "├", sepR: "┤", dot: "●", arrow: "❯", chat: "💬", bot: "🤖", user: "👤"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function log(msg) { if (config.verbose) fs.appendFileSync(LOG_FILE, new Date().toISOString() + " " + msg + "\n"); }
|
|
75
|
+
|
|
76
|
+
// ── API Call ──
|
|
77
|
+
function apiCall(method, endpoint, body) {
|
|
78
|
+
return new Promise(function(resolve, reject) {
|
|
79
|
+
var url = new URL(config.api.base_url + endpoint);
|
|
80
|
+
var isHttps = url.protocol === "https:";
|
|
81
|
+
var options = {
|
|
82
|
+
hostname: url.hostname, port: url.port, path: url.pathname,
|
|
83
|
+
method: method,
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
timeout: config.api.timeout
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
var authToken = config.api.key || config.auth.api_key;
|
|
89
|
+
if (authToken) {
|
|
90
|
+
options.headers["Authorization"] = "Bearer " + authToken;
|
|
91
|
+
} else if (config.auth.type === "oauth" && config.auth.oauth_token) {
|
|
92
|
+
options.headers["Authorization"] = "Bearer " + config.auth.oauth_token;
|
|
93
|
+
}
|
|
25
94
|
|
|
95
|
+
var data = body ? JSON.stringify(body) : null;
|
|
96
|
+
if (data) options.headers["Content-Length"] = Buffer.byteLength(data);
|
|
97
|
+
|
|
98
|
+
var mod = isHttps ? https : http;
|
|
99
|
+
var req = mod.request(options, function(res) {
|
|
100
|
+
var chunks = [];
|
|
101
|
+
res.on("data", function(c) { chunks.push(c); });
|
|
102
|
+
res.on("end", function() {
|
|
103
|
+
var raw = Buffer.concat(chunks).toString();
|
|
104
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(raw) }); }
|
|
105
|
+
catch(e) { resolve({ status: res.statusCode, data: raw }); }
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
req.on("error", function(e) { reject(e); });
|
|
109
|
+
req.on("timeout", function() { req.destroy(); reject(new Error("Timeout")); });
|
|
110
|
+
if (data) req.write(data);
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Chat History ──
|
|
116
|
+
var chatHistory = [];
|
|
117
|
+
var lastCreatedFiles = []; // Track files from last response for follow-up context
|
|
118
|
+
|
|
119
|
+
// ── Print Helpers ──
|
|
120
|
+
function printHeader() {
|
|
121
|
+
var w = Math.min(process.stdout.columns || 60, 60);
|
|
122
|
+
var inner = w - 4; // inner width between │ and │
|
|
123
|
+
var line = BOX.h.repeat(inner);
|
|
124
|
+
// Helper: pad text to exact inner width
|
|
125
|
+
function row(text, visLen) { return C.brightBlue + " " + BOX.v + C.reset + text + " ".repeat(Math.max(0, inner - visLen)) + C.brightBlue + BOX.v + C.reset; }
|
|
126
|
+
var modelStr = typeof config.model === "string" ? config.model : (config.model && config.model.name ? config.model.name : "default");
|
|
127
|
+
var wdShort = config.workdir.length > inner - 10 ? "..." + config.workdir.slice(-(inner - 13)) : config.workdir;
|
|
128
|
+
var titleText = " " + BOX.bot + " BLUN KING CLI v" + PKG_VERSION;
|
|
129
|
+
var subText = " Premium KI \u2014 Local First \u2014 Autonom";
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(C.brightBlue + " " + BOX.tl + line + BOX.tr + C.reset);
|
|
132
|
+
console.log(row(C.bold + C.brightWhite + titleText + C.reset, titleText.length));
|
|
133
|
+
console.log(row(C.gray + subText + C.reset, subText.length));
|
|
134
|
+
console.log(C.brightBlue + " " + BOX.v + line + BOX.v + C.reset);
|
|
135
|
+
var apiText = " API " + config.api.base_url;
|
|
136
|
+
console.log(row(C.gray + " API " + C.brightCyan + config.api.base_url + C.reset, apiText.length));
|
|
137
|
+
var modelText = " Model " + modelStr;
|
|
138
|
+
console.log(row(C.gray + " Model " + C.brightCyan + modelStr + C.reset, modelText.length));
|
|
139
|
+
var dirText = " Dir " + wdShort;
|
|
140
|
+
console.log(row(C.gray + " Dir " + C.brightCyan + wdShort + C.reset, dirText.length));
|
|
141
|
+
console.log(C.brightBlue + " " + BOX.bl + line + BOX.br + C.reset);
|
|
142
|
+
console.log("");
|
|
143
|
+
console.log(C.gray + " /help " + C.dim + "for commands " + C.gray + BOX.dot + " just type to chat" + C.reset);
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Update Checker ──
|
|
148
|
+
function checkForUpdates() {
|
|
26
149
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
150
|
+
var currentVersion = "1.4.0";
|
|
151
|
+
try { currentVersion = require(path.join(__dirname, "..", "package.json")).version; } catch(e) {}
|
|
152
|
+
var nullDev = process.platform === "win32" ? "2>NUL" : "2>/dev/null";
|
|
153
|
+
var latest = execSync("npm view blun-king-cli version " + nullDev, { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
154
|
+
if (latest && latest !== currentVersion) {
|
|
155
|
+
console.log(C.yellow + C.bold + " " + BOX.arrow + " Update available: " + currentVersion + " → " + latest + C.reset);
|
|
156
|
+
console.log(C.yellow + " npm update -g blun-king-cli" + C.reset);
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
159
|
+
} catch(e) { /* silent */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Inline markdown: **bold**, `code`, *italic*
|
|
163
|
+
function renderInline(text) {
|
|
164
|
+
return text
|
|
165
|
+
.replace(/\*\*(.+?)\*\*/g, C.bold + C.brightWhite + "$1" + C.reset)
|
|
166
|
+
.replace(/`(.+?)`/g, C.cyan + "$1" + C.reset)
|
|
167
|
+
.replace(/\*(.+?)\*/g, C.italic + "$1" + C.reset);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Word-wrap text to terminal width
|
|
171
|
+
function wordWrap(text, indent) {
|
|
172
|
+
var maxW = (process.stdout.columns || 80) - indent - 2;
|
|
173
|
+
if (text.length <= maxW) return [text];
|
|
174
|
+
var words = text.split(" ");
|
|
175
|
+
var lines = [];
|
|
176
|
+
var current = "";
|
|
177
|
+
for (var i = 0; i < words.length; i++) {
|
|
178
|
+
if (current.length + words[i].length + 1 > maxW && current.length > 0) {
|
|
179
|
+
lines.push(current);
|
|
180
|
+
current = words[i];
|
|
181
|
+
} else {
|
|
182
|
+
current = current ? current + " " + words[i] : words[i];
|
|
183
|
+
}
|
|
30
184
|
}
|
|
185
|
+
if (current) lines.push(current);
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function printAnswer(answer, meta) {
|
|
190
|
+
console.log("");
|
|
191
|
+
console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset + (meta ? C.gray + " " + BOX.dot + " " + meta + C.reset : ""));
|
|
192
|
+
console.log(C.green + " " + BOX.h.repeat(40) + C.reset);
|
|
193
|
+
// Markdown rendering
|
|
194
|
+
var lines = answer.split("\n");
|
|
195
|
+
var inCode = false;
|
|
196
|
+
for (var i = 0; i < lines.length; i++) {
|
|
197
|
+
var line = lines[i];
|
|
198
|
+
if (line.startsWith("```")) {
|
|
199
|
+
inCode = !inCode;
|
|
200
|
+
if (inCode) {
|
|
201
|
+
var lang = line.slice(3).trim();
|
|
202
|
+
console.log(C.dim + " " + BOX.tl + BOX.h.repeat(38) + (lang ? " " + lang + " " : "") + C.reset);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(C.dim + " " + BOX.bl + BOX.h.repeat(38) + C.reset);
|
|
205
|
+
}
|
|
206
|
+
} else if (inCode) {
|
|
207
|
+
console.log(C.cyan + " " + BOX.v + " " + line + C.reset);
|
|
208
|
+
} else if (line.startsWith("# ")) {
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(C.bold + C.yellow + " \u2588 " + line.slice(2) + C.reset);
|
|
211
|
+
console.log("");
|
|
212
|
+
} else if (line.startsWith("## ")) {
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log(C.bold + C.magenta + " \u25B6 " + line.slice(3) + C.reset);
|
|
215
|
+
} else if (line.startsWith("### ")) {
|
|
216
|
+
console.log(C.bold + C.brightWhite + " \u25AA " + line.slice(4) + C.reset);
|
|
217
|
+
} else if (line.match(/^\s*[-*] /)) {
|
|
218
|
+
var bulletText = line.replace(/^\s*[-*] /, "");
|
|
219
|
+
var wrapped = wordWrap(bulletText, 8);
|
|
220
|
+
console.log(" " + C.green + "\u2022 " + C.reset + renderInline(wrapped[0]));
|
|
221
|
+
for (var w = 1; w < wrapped.length; w++) console.log(" " + renderInline(wrapped[w]));
|
|
222
|
+
} else if (line.match(/^\s*\d+\.\s/)) {
|
|
223
|
+
var numMatch = line.match(/^(\s*\d+\.)\s(.*)/);
|
|
224
|
+
var wrapped = wordWrap(numMatch[2], 8);
|
|
225
|
+
console.log(" " + C.yellow + numMatch[1] + C.reset + " " + renderInline(wrapped[0]));
|
|
226
|
+
for (var w = 1; w < wrapped.length; w++) console.log(" " + renderInline(wrapped[w]));
|
|
227
|
+
} else if (line.trim() === "") {
|
|
228
|
+
console.log("");
|
|
229
|
+
} else {
|
|
230
|
+
var wrapped = wordWrap(line, 2);
|
|
231
|
+
for (var w = 0; w < wrapped.length; w++) console.log(" " + renderInline(wrapped[w]));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
console.log("");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Streaming typewriter output — word by word
|
|
238
|
+
async function streamAnswer(answer, meta) {
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset + (meta ? C.gray + " " + BOX.dot + " " + meta + C.reset : ""));
|
|
241
|
+
console.log(C.green + " " + BOX.h.repeat(40) + C.reset);
|
|
242
|
+
|
|
243
|
+
var lines = answer.split("\n");
|
|
244
|
+
var inCode = false;
|
|
245
|
+
var delay = 15; // ms per word
|
|
246
|
+
|
|
247
|
+
for (var i = 0; i < lines.length; i++) {
|
|
248
|
+
var line = lines[i];
|
|
249
|
+
|
|
250
|
+
if (line.startsWith("```")) {
|
|
251
|
+
inCode = !inCode;
|
|
252
|
+
if (inCode) {
|
|
253
|
+
var lang = line.slice(3).trim();
|
|
254
|
+
console.log(C.dim + " " + BOX.tl + BOX.h.repeat(38) + (lang ? " " + lang + " " : "") + C.reset);
|
|
255
|
+
} else {
|
|
256
|
+
console.log(C.dim + " " + BOX.bl + BOX.h.repeat(38) + C.reset);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (inCode) {
|
|
262
|
+
console.log(C.cyan + " " + BOX.v + " " + line + C.reset);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Determine prefix and color
|
|
267
|
+
var prefix = " ";
|
|
268
|
+
var color = C.reset;
|
|
269
|
+
var text = line;
|
|
270
|
+
|
|
271
|
+
if (line.startsWith("# ")) { prefix = "\n" + C.bold + C.yellow + " \u2588 "; text = line.slice(2); color = C.reset + "\n"; }
|
|
272
|
+
else if (line.startsWith("## ")) { prefix = "\n" + C.bold + C.magenta + " \u25B6 "; text = line.slice(3); color = C.reset; }
|
|
273
|
+
else if (line.startsWith("### ")) { prefix = C.bold + C.brightWhite + " \u25AA "; text = line.slice(4); color = C.reset; }
|
|
274
|
+
else if (line.match(/^\s*[-*] /)) { prefix = " " + C.green + "\u2022 " + C.reset; text = line.replace(/^\s*[-*] /, ""); }
|
|
275
|
+
else if (line.match(/^\s*\d+\.\s/)) { var m = line.match(/^(\s*\d+\.)\s(.*)/); prefix = " " + C.yellow + m[1] + C.reset + " "; text = m[2]; }
|
|
276
|
+
else if (line.trim() === "") { console.log(""); continue; }
|
|
277
|
+
|
|
278
|
+
// Stream words with word-wrap
|
|
279
|
+
var wrappedLines = wordWrap(text, 4);
|
|
280
|
+
for (var wl = 0; wl < wrappedLines.length; wl++) {
|
|
281
|
+
var words = wrappedLines[wl].split(" ");
|
|
282
|
+
process.stdout.write(wl === 0 ? prefix : " ");
|
|
283
|
+
for (var w = 0; w < words.length; w++) {
|
|
284
|
+
var word = words[w];
|
|
285
|
+
// Inline formatting
|
|
286
|
+
word = word.replace(/\*\*(.+?)\*\*/g, C.bold + C.brightWhite + "$1" + C.reset);
|
|
287
|
+
word = word.replace(/`(.+?)`/g, C.cyan + "$1" + C.reset);
|
|
288
|
+
process.stdout.write(word + (w < words.length - 1 ? " " : ""));
|
|
289
|
+
await new Promise(function(r) { setTimeout(r, delay); });
|
|
290
|
+
}
|
|
291
|
+
process.stdout.write((wl === wrappedLines.length - 1 ? color : "") + "\n");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
console.log("");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function printUserMessage(msg) {
|
|
298
|
+
console.log("");
|
|
299
|
+
console.log(C.brightBlue + C.bold + " " + BOX.user + " Du" + C.reset);
|
|
300
|
+
console.log(C.brightWhite + " " + msg + C.reset);
|
|
31
301
|
}
|
|
32
302
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
303
|
+
function printError(msg) { console.log(C.red + "ERROR: " + msg + C.reset); }
|
|
304
|
+
function printInfo(msg) { console.log(C.cyan + msg + C.reset); }
|
|
305
|
+
function printSuccess(msg) { console.log(C.green + "✓ " + msg + C.reset); }
|
|
306
|
+
|
|
307
|
+
// ── Commands ──
|
|
308
|
+
async function handleCommand(input) {
|
|
309
|
+
var parts = input.trim().split(/\s+/);
|
|
310
|
+
var cmd = parts[0].toLowerCase();
|
|
311
|
+
var args = parts.slice(1).join(" ");
|
|
312
|
+
|
|
313
|
+
switch(cmd) {
|
|
314
|
+
case "/help":
|
|
315
|
+
console.log("");
|
|
316
|
+
console.log(C.bold + "BLUN King CLI — Commands:" + C.reset);
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log(C.yellow + " CHAT" + C.reset);
|
|
319
|
+
console.log(" (just type) Chat with BLUN King");
|
|
320
|
+
console.log(" /clear Clear chat history");
|
|
321
|
+
console.log("");
|
|
322
|
+
console.log(C.yellow + " SKILLS" + C.reset);
|
|
323
|
+
console.log(" /skills List all skills");
|
|
324
|
+
console.log(" /skill install Install a skill from server");
|
|
325
|
+
console.log(" /skill info <n> Show skill details");
|
|
326
|
+
console.log("");
|
|
327
|
+
console.log(C.yellow + " TOOLS" + C.reset);
|
|
328
|
+
console.log(" /search <query> Web search");
|
|
329
|
+
console.log(" /learn <text> Teach BLUN King");
|
|
330
|
+
console.log(" /learn-url <url> Learn from URL");
|
|
331
|
+
console.log(" /generate <desc> Generate a file");
|
|
332
|
+
console.log(" /analyze <url> Analyze a website");
|
|
333
|
+
console.log(" /score <text> Score a response");
|
|
334
|
+
console.log(" /eval Run eval suite");
|
|
335
|
+
console.log("");
|
|
336
|
+
console.log(C.yellow + " PLUGINS (MCP)" + C.reset);
|
|
337
|
+
console.log(" /plugin list List installed plugins");
|
|
338
|
+
console.log(" /plugin add <cmd> Add MCP server (npm pkg or local path)");
|
|
339
|
+
console.log(" /plugin remove <n>Remove a plugin");
|
|
340
|
+
console.log(" /plugin run <n> Start a plugin server");
|
|
341
|
+
console.log(" /plugin stop <n> Stop a plugin server");
|
|
342
|
+
console.log(" /permissions Show permission settings");
|
|
343
|
+
console.log(" /permissions allow-all Allow all tool calls");
|
|
344
|
+
console.log(" /permissions ask Ask before each tool call");
|
|
345
|
+
console.log("");
|
|
346
|
+
console.log(C.yellow + " SETTINGS" + C.reset);
|
|
347
|
+
console.log(" /settings Show all settings");
|
|
348
|
+
console.log(" /set auth api Switch to API key auth");
|
|
349
|
+
console.log(" /set auth oauth Switch to OAuth");
|
|
350
|
+
console.log(" /set key <key> Set API key");
|
|
351
|
+
console.log(" /set url <url> Set API base URL");
|
|
352
|
+
console.log(" /set telegram Configure Telegram bridge");
|
|
353
|
+
console.log(" /set verbose Toggle verbose logging");
|
|
354
|
+
console.log("");
|
|
355
|
+
console.log(C.yellow + " DEV" + C.reset);
|
|
356
|
+
console.log(" /sh <cmd> Run shell command");
|
|
357
|
+
console.log(" /git <cmd> Git commands (status, log, diff, add, commit, push, pull, branch, checkout, clone, init)");
|
|
358
|
+
console.log(" /ssh add/del/list Connect & manage SSH hosts");
|
|
359
|
+
console.log(" /ssh <host> <cmd> Run command on remote host");
|
|
360
|
+
console.log(" /deploy <method> Deploy: git, ssh, sync, pm2");
|
|
361
|
+
console.log(" /read <file> Read a file");
|
|
362
|
+
console.log(" /write <file> Write/create a file");
|
|
363
|
+
console.log(" /init Init project (AGENT.md, .gitignore, git init)");
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(C.yellow + " CONFIG" + C.reset);
|
|
366
|
+
console.log(" /config list List all settings (all scopes)");
|
|
367
|
+
console.log(" /config get <key> Get a setting value");
|
|
368
|
+
console.log(" /config set <k> <v> Set a value [--global|--project|--local]");
|
|
369
|
+
console.log(" /config add <k> <v> Add to array setting");
|
|
370
|
+
console.log(" /config remove <k> Remove setting");
|
|
371
|
+
console.log("");
|
|
372
|
+
console.log(C.yellow + " HOOKS" + C.reset);
|
|
373
|
+
console.log(" /hooks list List all hooks");
|
|
374
|
+
console.log(" /hooks add <trigger> <cmd> Add hook (pre:chat, post:deploy...)");
|
|
375
|
+
console.log(" /hooks remove <trigger> Remove hook");
|
|
376
|
+
console.log("");
|
|
377
|
+
console.log(C.yellow + " AGENTS" + C.reset);
|
|
378
|
+
console.log(" /agents list List subagents");
|
|
379
|
+
console.log(" /agents create <n> Create new agent");
|
|
380
|
+
console.log(" /agents run <n> <task> Run agent with task");
|
|
381
|
+
console.log(" /agents info <n> Show agent details");
|
|
382
|
+
console.log("");
|
|
383
|
+
console.log(C.yellow + " SYSTEM" + C.reset);
|
|
384
|
+
console.log(" /status Runtime status");
|
|
385
|
+
console.log(" /versions Prompt versions");
|
|
386
|
+
console.log(" /health Quick health check");
|
|
387
|
+
console.log(" /doctor Full system diagnostics");
|
|
388
|
+
console.log(" /model [name] Show/switch model");
|
|
389
|
+
console.log(" /cost Session cost estimate");
|
|
390
|
+
console.log(" /compact Clear old context");
|
|
391
|
+
console.log(" /review Review current git diff");
|
|
392
|
+
console.log(" /login key <k> Login with API key");
|
|
393
|
+
console.log(" /logout Logout");
|
|
394
|
+
console.log(" /watchdog Watchdog status");
|
|
395
|
+
console.log(" /memory Show local memory");
|
|
396
|
+
console.log(" /files List workdir files");
|
|
397
|
+
console.log(" /exit Exit CLI");
|
|
398
|
+
console.log("");
|
|
399
|
+
console.log(C.yellow + " SHORTCUTS" + C.reset);
|
|
400
|
+
console.log(" !<cmd> Run bash command directly");
|
|
401
|
+
console.log(" #<key> Read memory entry");
|
|
402
|
+
console.log(" #save <k> <v> Save to memory");
|
|
403
|
+
console.log("");
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case "/clear":
|
|
407
|
+
chatHistory = [];
|
|
408
|
+
printSuccess("Chat history cleared.");
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case "/skills":
|
|
412
|
+
await cmdSkills();
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case "/skill":
|
|
416
|
+
await cmdSkillAction(args);
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case "/search":
|
|
420
|
+
await cmdSearch(args);
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
case "/learn":
|
|
424
|
+
await cmdLearn(args, false);
|
|
425
|
+
break;
|
|
426
|
+
|
|
427
|
+
case "/learn-url":
|
|
428
|
+
await cmdLearn(args, true);
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case "/generate":
|
|
432
|
+
await cmdGenerate(args);
|
|
433
|
+
break;
|
|
434
|
+
|
|
435
|
+
case "/analyze":
|
|
436
|
+
await cmdAnalyze(args);
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case "/score":
|
|
440
|
+
await cmdScore(args);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case "/eval":
|
|
444
|
+
await cmdEval();
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case "/settings":
|
|
448
|
+
cmdSettings();
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case "/set":
|
|
452
|
+
cmdSet(args);
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case "/status":
|
|
456
|
+
await cmdStatus();
|
|
457
|
+
break;
|
|
458
|
+
|
|
459
|
+
case "/versions":
|
|
460
|
+
await cmdVersions();
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
case "/health":
|
|
464
|
+
await cmdHealth();
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case "/watchdog":
|
|
468
|
+
cmdWatchdog(args);
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
case "/memory":
|
|
472
|
+
cmdMemory(args);
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case "/files":
|
|
476
|
+
cmdFiles();
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
case "/sh":
|
|
480
|
+
cmdShell(args);
|
|
481
|
+
break;
|
|
482
|
+
|
|
483
|
+
case "/git":
|
|
484
|
+
cmdGit(args);
|
|
485
|
+
break;
|
|
486
|
+
|
|
487
|
+
case "/ssh":
|
|
488
|
+
cmdSsh(args);
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case "/deploy":
|
|
492
|
+
cmdDeploy(args);
|
|
493
|
+
break;
|
|
494
|
+
|
|
495
|
+
case "/read":
|
|
496
|
+
cmdRead(args);
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case "/write":
|
|
500
|
+
cmdWrite(args);
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
case "/init":
|
|
504
|
+
cmdInit();
|
|
505
|
+
break;
|
|
506
|
+
|
|
507
|
+
case "/plugin":
|
|
508
|
+
case "/mcp":
|
|
509
|
+
cmdPlugin(args);
|
|
510
|
+
break;
|
|
511
|
+
|
|
512
|
+
case "/permissions":
|
|
513
|
+
cmdPermissions(args);
|
|
514
|
+
break;
|
|
515
|
+
|
|
516
|
+
case "/config":
|
|
517
|
+
cmdConfig(args);
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case "/hooks":
|
|
521
|
+
cmdHooks(args);
|
|
522
|
+
break;
|
|
523
|
+
|
|
524
|
+
case "/agents":
|
|
525
|
+
cmdAgents(args);
|
|
526
|
+
break;
|
|
527
|
+
|
|
528
|
+
case "/doctor":
|
|
529
|
+
await cmdDoctor();
|
|
530
|
+
break;
|
|
531
|
+
|
|
532
|
+
case "/model":
|
|
533
|
+
cmdModel(args);
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case "/login":
|
|
537
|
+
cmdLogin(args);
|
|
538
|
+
break;
|
|
539
|
+
|
|
540
|
+
case "/logout":
|
|
541
|
+
cmdLogout();
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case "/compact":
|
|
545
|
+
cmdCompact();
|
|
546
|
+
break;
|
|
547
|
+
|
|
548
|
+
case "/review":
|
|
549
|
+
await cmdReview();
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case "/cost":
|
|
553
|
+
cmdCost();
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case "/agent":
|
|
557
|
+
await cmdAgent(args);
|
|
558
|
+
break;
|
|
559
|
+
|
|
560
|
+
case "/screenshot":
|
|
561
|
+
await cmdScreenshot(args);
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case "/render":
|
|
565
|
+
await cmdRender(args);
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
case "/exit":
|
|
569
|
+
case "/quit":
|
|
570
|
+
case "/q":
|
|
571
|
+
console.log(C.dim + "Bye." + C.reset);
|
|
572
|
+
process.exit(0);
|
|
36
573
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
574
|
+
default:
|
|
575
|
+
printError("Unknown command: " + cmd + ". Type /help for commands.");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Global UI bridge (set by interactive mode) ──
|
|
580
|
+
var _globalDrawPrompt = null;
|
|
581
|
+
var _globalEraseUI = null;
|
|
582
|
+
var _globalUILines = 5; // how many lines the input box takes up
|
|
583
|
+
|
|
584
|
+
// ── Chat ──
|
|
585
|
+
// ── SSE Stream Request ──
|
|
586
|
+
function apiStreamCall(endpoint, body) {
|
|
587
|
+
return new Promise(function(resolve, reject) {
|
|
588
|
+
var url = new URL(config.api.base_url + endpoint);
|
|
589
|
+
var isHttps = url.protocol === "https:";
|
|
590
|
+
var options = {
|
|
591
|
+
hostname: url.hostname, port: url.port, path: url.pathname,
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: { "Content-Type": "application/json" }
|
|
594
|
+
};
|
|
595
|
+
var authToken = config.api.key || config.auth.api_key;
|
|
596
|
+
if (authToken) options.headers["Authorization"] = "Bearer " + authToken;
|
|
597
|
+
|
|
598
|
+
var data = JSON.stringify(body);
|
|
599
|
+
options.headers["Content-Length"] = Buffer.byteLength(data);
|
|
600
|
+
|
|
601
|
+
var mod = isHttps ? https : http;
|
|
602
|
+
var req = mod.request(options, function(res) {
|
|
603
|
+
if (res.statusCode !== 200) {
|
|
604
|
+
var chunks = [];
|
|
605
|
+
res.on("data", function(c) { chunks.push(c); });
|
|
606
|
+
res.on("end", function() {
|
|
607
|
+
try { reject(new Error(JSON.parse(Buffer.concat(chunks).toString()).error || "API Error " + res.statusCode)); }
|
|
608
|
+
catch(e) { reject(new Error("API Error " + res.statusCode)); }
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
resolve(res);
|
|
613
|
+
});
|
|
614
|
+
req.on("error", function(e) { reject(e); });
|
|
615
|
+
req.write(data);
|
|
616
|
+
req.end();
|
|
41
617
|
});
|
|
42
|
-
|
|
43
|
-
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function sendChat(message) {
|
|
621
|
+
try {
|
|
622
|
+
printUserMessage(message);
|
|
623
|
+
|
|
624
|
+
// Show thinking animation
|
|
625
|
+
var dots = 0;
|
|
626
|
+
var thinkFrames = ["thinking", "thinking.", "thinking..", "thinking..."];
|
|
627
|
+
console.log("");
|
|
628
|
+
var thinkTimer = setInterval(function() {
|
|
629
|
+
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " " + thinkFrames[dots % 4] + " " + C.reset);
|
|
630
|
+
dots++;
|
|
631
|
+
}, 300);
|
|
632
|
+
|
|
633
|
+
// Try streaming first, fall back to non-streaming
|
|
634
|
+
var useStreaming = true;
|
|
635
|
+
var fullAnswer = "";
|
|
636
|
+
var receivedFiles = [];
|
|
637
|
+
var meta = "";
|
|
638
|
+
var headerPrinted = false;
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
// Include last created files as context for follow-ups
|
|
642
|
+
var contextMsg = message;
|
|
643
|
+
if (lastCreatedFiles.length > 0) {
|
|
644
|
+
contextMsg = message + "\n\n[Kontext: Zuletzt erstellte Dateien im Ordner " + config.workdir + ": " +
|
|
645
|
+
lastCreatedFiles.map(function(f) { return f.name; }).join(", ") + "]";
|
|
646
|
+
}
|
|
647
|
+
var stream = await apiStreamCall("/chat/stream", {
|
|
648
|
+
message: contextMsg,
|
|
649
|
+
workdir: config.workdir,
|
|
650
|
+
history: chatHistory.slice(-10)
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
var buffer = "";
|
|
654
|
+
var streamMeta = {};
|
|
655
|
+
var headerShown = false;
|
|
656
|
+
|
|
657
|
+
await new Promise(function(resolve, reject) {
|
|
658
|
+
stream.on("data", function(chunk) {
|
|
659
|
+
buffer += chunk.toString();
|
|
660
|
+
var lines = buffer.split("\n");
|
|
661
|
+
buffer = lines.pop() || "";
|
|
662
|
+
|
|
663
|
+
for (var i = 0; i < lines.length; i++) {
|
|
664
|
+
var line = lines[i].trim();
|
|
665
|
+
if (line.startsWith("event: ")) {
|
|
666
|
+
var eventType = line.slice(7);
|
|
667
|
+
i++;
|
|
668
|
+
if (i < lines.length && lines[i].trim().startsWith("data: ")) {
|
|
669
|
+
var dataStr = lines[i].trim().slice(6);
|
|
670
|
+
try {
|
|
671
|
+
var eventData = JSON.parse(dataStr);
|
|
672
|
+
|
|
673
|
+
if (eventType === "meta") {
|
|
674
|
+
streamMeta = eventData;
|
|
675
|
+
// Stop thinking, show header
|
|
676
|
+
clearInterval(thinkTimer);
|
|
677
|
+
process.stdout.write("\r\x1b[2K");
|
|
678
|
+
console.log("");
|
|
679
|
+
console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset +
|
|
680
|
+
C.gray + " " + BOX.dot + " " + (eventData.task_type || "") + "/" + (eventData.role || "") + C.reset);
|
|
681
|
+
console.log(C.green + " " + BOX.h.repeat(40) + C.reset);
|
|
682
|
+
process.stdout.write(" ");
|
|
683
|
+
headerShown = true;
|
|
684
|
+
}
|
|
685
|
+
else if (eventType === "token") {
|
|
686
|
+
fullAnswer += eventData.text;
|
|
687
|
+
// Live output — write token directly
|
|
688
|
+
process.stdout.write(eventData.text);
|
|
689
|
+
}
|
|
690
|
+
else if (eventType === "retry") {
|
|
691
|
+
fullAnswer = "";
|
|
692
|
+
process.stdout.write("\n" + C.yellow + " \u21BB retry..." + C.reset + "\n ");
|
|
693
|
+
}
|
|
694
|
+
else if (eventType === "tool") {
|
|
695
|
+
process.stdout.write("\n" + C.cyan + " \u2699 " + eventData.name + C.reset);
|
|
696
|
+
if (eventData.args && eventData.args.name) process.stdout.write(C.dim + " " + eventData.args.name + C.reset);
|
|
697
|
+
process.stdout.write("\n ");
|
|
698
|
+
}
|
|
699
|
+
else if (eventType === "files") {
|
|
700
|
+
receivedFiles = eventData.files || [];
|
|
701
|
+
}
|
|
702
|
+
else if (eventType === "done") {
|
|
703
|
+
meta = (streamMeta.task_type || "") + "/" + (streamMeta.role || "") + " " + BOX.dot +
|
|
704
|
+
" score: " + ((eventData.quality || {}).score || "?") +
|
|
705
|
+
(eventData.status ? " " + BOX.dot + " " + eventData.status : "");
|
|
706
|
+
}
|
|
707
|
+
else if (eventType === "error") {
|
|
708
|
+
process.stdout.write("\n" + C.red + " ERROR: " + eventData.error + C.reset + "\n");
|
|
709
|
+
}
|
|
710
|
+
} catch(e) {}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
stream.on("end", function() { resolve(); });
|
|
717
|
+
stream.on("error", function(e) { reject(e); });
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Finish streaming output
|
|
721
|
+
if (!headerShown) {
|
|
722
|
+
clearInterval(thinkTimer);
|
|
723
|
+
process.stdout.write("\r\x1b[2K");
|
|
724
|
+
}
|
|
725
|
+
console.log("\n");
|
|
726
|
+
if (meta) console.log(C.dim + " " + meta + C.reset);
|
|
727
|
+
console.log("");
|
|
728
|
+
|
|
729
|
+
} catch(streamErr) {
|
|
730
|
+
// Fallback to non-streaming
|
|
731
|
+
useStreaming = false;
|
|
732
|
+
clearInterval(thinkTimer);
|
|
733
|
+
process.stdout.write("\r\x1b[2K");
|
|
734
|
+
|
|
735
|
+
var resp = await apiCall("POST", "/chat", {
|
|
736
|
+
message: message,
|
|
737
|
+
workdir: config.workdir,
|
|
738
|
+
history: chatHistory.slice(-10)
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
if (resp.status !== 200) {
|
|
742
|
+
printError(resp.data.error || "API Error " + resp.status);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
fullAnswer = resp.data.answer || "";
|
|
747
|
+
receivedFiles = resp.data.files || [];
|
|
748
|
+
var q = resp.data.quality || {};
|
|
749
|
+
meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
|
|
750
|
+
(q.retried ? " " + BOX.dot + " retried" : "") +
|
|
751
|
+
(resp.data.status ? " " + BOX.dot + " " + resp.data.status : "");
|
|
752
|
+
|
|
753
|
+
await streamAnswer(fullAnswer, meta);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
chatHistory.push({ role: "user", content: message });
|
|
757
|
+
chatHistory.push({ role: "assistant", content: fullAnswer });
|
|
758
|
+
|
|
759
|
+
// Token tracking
|
|
760
|
+
var inTok = Math.ceil(message.length / 3.5);
|
|
761
|
+
var outTok = Math.ceil(fullAnswer.length / 3.5);
|
|
762
|
+
sessionCost.requests++;
|
|
763
|
+
sessionCost.inputTokensEst += inTok;
|
|
764
|
+
sessionCost.outputTokensEst += outTok;
|
|
765
|
+
|
|
766
|
+
// ── Save received files to local workdir ──
|
|
767
|
+
if (receivedFiles.length > 0) {
|
|
768
|
+
console.log(C.green + " \uD83D\uDCC1 Dateien gespeichert:" + C.reset);
|
|
769
|
+
lastCreatedFiles = [];
|
|
770
|
+
for (var fi = 0; fi < receivedFiles.length; fi++) {
|
|
771
|
+
var f = receivedFiles[fi];
|
|
772
|
+
if (f.content && f.name) {
|
|
773
|
+
var localPath = path.join(config.workdir, f.name);
|
|
774
|
+
var localDir = path.dirname(localPath);
|
|
775
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
776
|
+
fs.writeFileSync(localPath, f.content, "utf8");
|
|
777
|
+
console.log(" " + C.brightCyan + localPath + C.reset + C.dim + " (" + f.size + " bytes)" + C.reset);
|
|
778
|
+
lastCreatedFiles.push({ name: f.name, path: localPath, size: f.size });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
console.log("");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Auto Memory Dream Mode
|
|
785
|
+
dreamCounter++;
|
|
786
|
+
if (dreamCounter >= DREAM_INTERVAL) {
|
|
787
|
+
autoCompact().catch(function() {});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
} catch(e) {
|
|
791
|
+
if (typeof thinkTimer !== "undefined") clearInterval(thinkTimer);
|
|
792
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
793
|
+
printError("Connection failed: " + e.message);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── Command Implementations ──
|
|
798
|
+
async function cmdSkills() {
|
|
44
799
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
800
|
+
var resp = await apiCall("POST", "/classify", { message: "test" });
|
|
801
|
+
// List skills from known role files
|
|
802
|
+
var skills = [
|
|
803
|
+
"api", "artifact", "artifact_reviewer", "automation", "business", "creative",
|
|
804
|
+
"critic", "data", "debug", "decomposer", "design", "dev", "devops", "docs",
|
|
805
|
+
"eval_designer", "learn_curator", "librarian", "marketing", "math", "mobile",
|
|
806
|
+
"operator", "prompt_architect", "research", "sales", "security", "seo",
|
|
807
|
+
"support", "text", "tool_brain", "translator", "web_ui", "website_builder"
|
|
808
|
+
];
|
|
809
|
+
console.log("");
|
|
810
|
+
console.log(C.bold + "BLUN King Skills (" + skills.length + "):" + C.reset);
|
|
811
|
+
console.log("");
|
|
812
|
+
for (var i = 0; i < skills.length; i++) {
|
|
813
|
+
var installed = fs.existsSync(path.join(SKILLS_DIR, skills[i] + ".txt"));
|
|
814
|
+
console.log(" " + (installed ? C.green + "●" : C.dim + "○") + C.reset +
|
|
815
|
+
" " + skills[i] + (installed ? C.dim + " (local)" + C.reset : ""));
|
|
816
|
+
}
|
|
817
|
+
console.log("");
|
|
818
|
+
console.log(C.dim + " /skill install <name> — Install skill locally" + C.reset);
|
|
819
|
+
console.log(C.dim + " /skill info <name> — Show skill details" + C.reset);
|
|
820
|
+
console.log("");
|
|
821
|
+
} catch(e) {
|
|
822
|
+
printError(e.message);
|
|
48
823
|
}
|
|
49
|
-
if (!resp.ok) throw new Error(data.error || `HTTP ${resp.status}`);
|
|
50
|
-
return data;
|
|
51
824
|
}
|
|
52
825
|
|
|
53
|
-
async function
|
|
54
|
-
|
|
55
|
-
|
|
826
|
+
async function cmdSkillAction(args) {
|
|
827
|
+
var parts = args.split(/\s+/);
|
|
828
|
+
var action = parts[0];
|
|
829
|
+
var name = parts[1];
|
|
830
|
+
|
|
831
|
+
if (action === "install" && name) {
|
|
832
|
+
// Fetch from server
|
|
833
|
+
try {
|
|
834
|
+
var resp = await apiCall("POST", "/chat", {
|
|
835
|
+
message: "Zeige mir den kompletten Inhalt der Rolle " + name + " — nur den Prompt-Text, keine Erklaerung."
|
|
836
|
+
});
|
|
837
|
+
if (resp.status === 200 && resp.data.answer) {
|
|
838
|
+
fs.writeFileSync(path.join(SKILLS_DIR, name + ".txt"), resp.data.answer);
|
|
839
|
+
printSuccess("Skill '" + name + "' installed to " + SKILLS_DIR);
|
|
840
|
+
}
|
|
841
|
+
} catch(e) { printError(e.message); }
|
|
842
|
+
} else if (action === "info" && name) {
|
|
843
|
+
var file = path.join(SKILLS_DIR, name + ".txt");
|
|
844
|
+
if (fs.existsSync(file)) {
|
|
845
|
+
console.log("");
|
|
846
|
+
console.log(C.bold + "Skill: " + name + C.reset);
|
|
847
|
+
console.log(C.dim + "─".repeat(40) + C.reset);
|
|
848
|
+
console.log(fs.readFileSync(file, "utf8"));
|
|
849
|
+
} else {
|
|
850
|
+
printInfo("Skill not installed locally. Use /skill install " + name);
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
printError("Usage: /skill install <name> or /skill info <name>");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
56
856
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
857
|
+
async function cmdSearch(query) {
|
|
858
|
+
if (!query) { printError("Usage: /search <query>"); return; }
|
|
859
|
+
try {
|
|
860
|
+
process.stdout.write(C.dim + "🔍 searching..." + C.reset);
|
|
861
|
+
var resp = await apiCall("POST", "/web-search", { query: query, summarize: true });
|
|
862
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
863
|
+
if (resp.status === 200) {
|
|
864
|
+
console.log("");
|
|
865
|
+
console.log(C.bold + "Search: " + query + C.reset);
|
|
866
|
+
console.log("");
|
|
867
|
+
var results = resp.data.results || [];
|
|
868
|
+
for (var i = 0; i < results.length; i++) {
|
|
869
|
+
console.log(C.yellow + " " + (i+1) + ". " + results[i].title + C.reset);
|
|
870
|
+
if (results[i].snippet) console.log(C.dim + " " + results[i].snippet.slice(0, 100) + C.reset);
|
|
871
|
+
if (results[i].url) console.log(C.blue + " " + results[i].url + C.reset);
|
|
872
|
+
}
|
|
873
|
+
if (resp.data.summary) {
|
|
874
|
+
console.log("");
|
|
875
|
+
console.log(C.green + C.bold + "Summary:" + C.reset);
|
|
876
|
+
console.log(resp.data.summary);
|
|
877
|
+
}
|
|
878
|
+
console.log("");
|
|
879
|
+
} else { printError(resp.data.error || "Search failed"); }
|
|
880
|
+
} catch(e) {
|
|
881
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
882
|
+
printError(e.message);
|
|
65
883
|
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function cmdLearn(input, isUrl) {
|
|
887
|
+
if (!input) { printError("Usage: /learn <text> or /learn-url <url>"); return; }
|
|
888
|
+
try {
|
|
889
|
+
process.stdout.write(C.dim + "📚 learning..." + C.reset);
|
|
890
|
+
var body = isUrl ? { url: input } : { content: input };
|
|
891
|
+
var resp = await apiCall("POST", "/learn", body);
|
|
892
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
893
|
+
if (resp.status === 200) {
|
|
894
|
+
printSuccess("Learned! Category: " + resp.data.category + " | File: " + resp.data.file);
|
|
895
|
+
} else { printError(resp.data.error || "Learn failed"); }
|
|
896
|
+
} catch(e) {
|
|
897
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
898
|
+
printError(e.message);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
66
901
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
902
|
+
async function cmdGenerate(desc) {
|
|
903
|
+
if (!desc) { printError("Usage: /generate <description>"); return; }
|
|
904
|
+
// Parse format from description
|
|
905
|
+
var format = "txt";
|
|
906
|
+
var fmatch = desc.match(/\.(html|css|js|json|md|py|txt|yaml|xml|sql|csv)\b/i);
|
|
907
|
+
if (fmatch) format = fmatch[1].toLowerCase();
|
|
908
|
+
else if (/html|webseite|landing/i.test(desc)) format = "html";
|
|
909
|
+
else if (/json|config/i.test(desc)) format = "json";
|
|
910
|
+
else if (/javascript|node|function/i.test(desc)) format = "js";
|
|
911
|
+
else if (/python/i.test(desc)) format = "py";
|
|
912
|
+
else if (/css|style/i.test(desc)) format = "css";
|
|
913
|
+
else if (/markdown/i.test(desc)) format = "md";
|
|
71
914
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
915
|
+
try {
|
|
916
|
+
process.stdout.write(C.dim + "⚙️ generating " + format + "..." + C.reset);
|
|
917
|
+
var resp = await apiCall("POST", "/generate-file", { prompt: desc, format: format });
|
|
918
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
919
|
+
if (resp.status === 200) {
|
|
920
|
+
// Save locally too
|
|
921
|
+
var localPath = path.join(config.workdir, resp.data.filename);
|
|
922
|
+
fs.writeFileSync(localPath, resp.data.content);
|
|
923
|
+
printSuccess("Generated: " + resp.data.filename + " (" + resp.data.size + " bytes)");
|
|
924
|
+
printInfo("Local: " + localPath);
|
|
925
|
+
printInfo("Remote: " + config.api.base_url + resp.data.download);
|
|
926
|
+
} else { printError(resp.data.error || "Generate failed"); }
|
|
927
|
+
} catch(e) {
|
|
928
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
929
|
+
printError(e.message);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
76
932
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
933
|
+
async function cmdAnalyze(url) {
|
|
934
|
+
if (!url) { printError("Usage: /analyze <url>"); return; }
|
|
935
|
+
try {
|
|
936
|
+
process.stdout.write(C.dim + "🔎 analyzing..." + C.reset);
|
|
937
|
+
var resp = await apiCall("POST", "/analyze", { url: url, type: "website" });
|
|
938
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
939
|
+
if (resp.status === 200) {
|
|
940
|
+
var meta = resp.data.meta || {};
|
|
941
|
+
console.log("");
|
|
942
|
+
console.log(C.bold + "Analysis: " + url + C.reset);
|
|
943
|
+
console.log(C.dim + "─".repeat(50) + C.reset);
|
|
944
|
+
if (meta.title) console.log(" Title: " + meta.title);
|
|
945
|
+
if (meta.h1s) console.log(" H1s: " + meta.h1s.join(", "));
|
|
946
|
+
console.log(" Links: " + meta.links + " | Images: " + meta.images);
|
|
947
|
+
console.log(" Responsive: " + (meta.responsive ? "yes" : "no"));
|
|
948
|
+
if (meta.frameworks && meta.frameworks.length) console.log(" Frameworks: " + meta.frameworks.join(", "));
|
|
949
|
+
console.log("");
|
|
950
|
+
printAnswer(resp.data.analysis);
|
|
951
|
+
} else { printError(resp.data.error || "Analyze failed"); }
|
|
952
|
+
} catch(e) {
|
|
953
|
+
process.stdout.write("\r" + " ".repeat(30) + "\r");
|
|
954
|
+
printError(e.message);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
82
957
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
958
|
+
async function cmdScore(text) {
|
|
959
|
+
if (!text) { printError("Usage: /score <text>"); return; }
|
|
960
|
+
try {
|
|
961
|
+
var resp = await apiCall("POST", "/score", { answer: text, task_type: "chat" });
|
|
962
|
+
if (resp.status === 200) {
|
|
963
|
+
var s = resp.data.score;
|
|
964
|
+
var color = s >= 80 ? C.green : s >= 50 ? C.yellow : C.red;
|
|
965
|
+
console.log("");
|
|
966
|
+
console.log(C.bold + "Score: " + color + s + "/100" + C.reset);
|
|
967
|
+
if (resp.data.reasons && resp.data.reasons.length > 0) {
|
|
968
|
+
console.log(C.dim + "Flags: " + resp.data.reasons.join(", ") + C.reset);
|
|
88
969
|
}
|
|
970
|
+
console.log("");
|
|
971
|
+
} else { printError(resp.data.error || "Score failed"); }
|
|
972
|
+
} catch(e) { printError(e.message); }
|
|
973
|
+
}
|
|
89
974
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
975
|
+
async function cmdEval() {
|
|
976
|
+
printInfo("Running eval suite... this takes a few minutes.");
|
|
977
|
+
try {
|
|
978
|
+
var resp = await apiCall("GET", "/eval");
|
|
979
|
+
if (resp.status === 200) {
|
|
980
|
+
var d = resp.data;
|
|
981
|
+
console.log("");
|
|
982
|
+
console.log(C.bold + "EVAL RESULTS" + C.reset);
|
|
983
|
+
console.log(C.dim + "─".repeat(50) + C.reset);
|
|
984
|
+
if (d.results) {
|
|
985
|
+
for (var i = 0; i < d.results.length; i++) {
|
|
986
|
+
var r = d.results[i];
|
|
987
|
+
var icon = r.passed ? C.green + "PASS" : C.red + "FAIL";
|
|
988
|
+
console.log(" " + icon + C.reset + " " + r.name +
|
|
989
|
+
(r.failures && r.failures.length > 0 ? C.dim + " — " + r.failures.join(", ") + C.reset : ""));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
console.log(C.dim + "─".repeat(50) + C.reset);
|
|
993
|
+
var scoreColor = d.score >= 83 ? C.green : C.red;
|
|
994
|
+
console.log(" " + C.bold + "Score: " + scoreColor + d.score + "% (" + d.passed + "/" + d.total + ")" + C.reset);
|
|
995
|
+
console.log("");
|
|
996
|
+
} else { printError(resp.data.error || "Eval failed"); }
|
|
997
|
+
} catch(e) { printError(e.message); }
|
|
998
|
+
}
|
|
93
999
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1000
|
+
function cmdSettings() {
|
|
1001
|
+
console.log("");
|
|
1002
|
+
console.log(C.bold + "BLUN King Settings:" + C.reset);
|
|
1003
|
+
console.log(C.dim + "─".repeat(40) + C.reset);
|
|
1004
|
+
console.log(" Auth Type: " + config.auth.type);
|
|
1005
|
+
console.log(" API Key: " + (config.auth.api_key ? config.auth.api_key.slice(0, 10) + "..." : "(not set)"));
|
|
1006
|
+
console.log(" OAuth Token: " + (config.auth.oauth_token ? "set (expires: " + config.auth.oauth_expires + ")" : "(not set)"));
|
|
1007
|
+
console.log(" API URL: " + config.api.base_url);
|
|
1008
|
+
console.log(" Model: " + config.model);
|
|
1009
|
+
console.log(" Workdir: " + config.workdir);
|
|
1010
|
+
console.log(" Verbose: " + config.verbose);
|
|
1011
|
+
console.log(" Telegram: " + (config.telegram.enabled ? "on (chat: " + config.telegram.chat_id + ")" : "off"));
|
|
1012
|
+
console.log(" Watchdog: " + (config.watchdog.enabled ? "on (" + config.watchdog.interval + "s)" : "off"));
|
|
1013
|
+
console.log("");
|
|
1014
|
+
console.log(C.dim + " Config file: " + CONFIG_FILE + C.reset);
|
|
1015
|
+
console.log("");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function cmdSet(args) {
|
|
1019
|
+
var parts = args.split(/\s+/);
|
|
1020
|
+
var key = parts[0];
|
|
1021
|
+
var val = parts.slice(1).join(" ");
|
|
1022
|
+
|
|
1023
|
+
switch(key) {
|
|
1024
|
+
case "auth":
|
|
1025
|
+
if (val === "api" || val === "api_key") {
|
|
1026
|
+
config.auth.type = "api_key";
|
|
1027
|
+
saveConfig(config);
|
|
1028
|
+
printSuccess("Auth switched to API key.");
|
|
1029
|
+
} else if (val === "oauth") {
|
|
1030
|
+
config.auth.type = "oauth";
|
|
1031
|
+
printInfo("Enter OAuth token:");
|
|
1032
|
+
// Will be set interactively
|
|
1033
|
+
var rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1034
|
+
rl2.question(" OAuth Token: ", function(token) {
|
|
1035
|
+
config.auth.oauth_token = token.trim();
|
|
1036
|
+
rl2.question(" Expires (ISO date or 'never'): ", function(exp) {
|
|
1037
|
+
config.auth.oauth_expires = exp.trim() === "never" ? null : exp.trim();
|
|
1038
|
+
saveConfig(config);
|
|
1039
|
+
printSuccess("OAuth configured.");
|
|
1040
|
+
rl2.close();
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
return;
|
|
1044
|
+
} else {
|
|
1045
|
+
printError("Usage: /set auth api or /set auth oauth");
|
|
1046
|
+
}
|
|
1047
|
+
break;
|
|
1048
|
+
|
|
1049
|
+
case "key":
|
|
1050
|
+
config.auth.api_key = val;
|
|
1051
|
+
config.auth.type = "api_key";
|
|
1052
|
+
saveConfig(config);
|
|
1053
|
+
printSuccess("API key set: " + val.slice(0, 10) + "...");
|
|
1054
|
+
break;
|
|
1055
|
+
|
|
1056
|
+
case "url":
|
|
1057
|
+
config.api.base_url = val;
|
|
1058
|
+
saveConfig(config);
|
|
1059
|
+
printSuccess("API URL set: " + val);
|
|
1060
|
+
break;
|
|
1061
|
+
|
|
1062
|
+
case "telegram":
|
|
1063
|
+
printInfo("Telegram Bridge Setup:");
|
|
1064
|
+
var rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1065
|
+
rl3.question(" Bot Token: ", function(token) {
|
|
1066
|
+
config.telegram.bot_token = token.trim();
|
|
1067
|
+
rl3.question(" Chat ID: ", function(chatId) {
|
|
1068
|
+
config.telegram.chat_id = chatId.trim();
|
|
1069
|
+
config.telegram.enabled = true;
|
|
1070
|
+
saveConfig(config);
|
|
1071
|
+
printSuccess("Telegram bridge configured.");
|
|
1072
|
+
rl3.close();
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
return;
|
|
1076
|
+
|
|
1077
|
+
case "verbose":
|
|
1078
|
+
config.verbose = !config.verbose;
|
|
1079
|
+
saveConfig(config);
|
|
1080
|
+
printSuccess("Verbose logging: " + (config.verbose ? "ON" : "OFF"));
|
|
1081
|
+
break;
|
|
1082
|
+
|
|
1083
|
+
case "workdir":
|
|
1084
|
+
if (fs.existsSync(val)) {
|
|
1085
|
+
config.workdir = path.resolve(val);
|
|
1086
|
+
saveConfig(config);
|
|
1087
|
+
printSuccess("Workdir: " + config.workdir);
|
|
1088
|
+
} else { printError("Directory not found: " + val); }
|
|
1089
|
+
break;
|
|
1090
|
+
|
|
1091
|
+
default:
|
|
1092
|
+
printError("Unknown setting. Use /settings to see all options.");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function cmdStatus() {
|
|
1097
|
+
try {
|
|
1098
|
+
process.stdout.write(C.dim + "checking..." + C.reset);
|
|
1099
|
+
var resp = await apiCall("GET", "/runtime/status");
|
|
1100
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
1101
|
+
if (resp.status === 200) {
|
|
1102
|
+
var d = resp.data;
|
|
1103
|
+
console.log("");
|
|
1104
|
+
console.log(C.bold + "BLUN King Runtime Status:" + C.reset);
|
|
1105
|
+
console.log(C.dim + "─".repeat(40) + C.reset);
|
|
1106
|
+
console.log(" Status: " + C.green + d.status + C.reset);
|
|
1107
|
+
console.log(" Model: " + d.model);
|
|
1108
|
+
console.log(" Ollama: " + (d.ollama === "connected" ? C.green : C.red) + d.ollama + C.reset);
|
|
1109
|
+
console.log(" Uptime: " + d.uptime_seconds + "s");
|
|
1110
|
+
console.log(" Artifacts: " + d.artifacts_count);
|
|
1111
|
+
console.log(" Models: " + (d.available_models || []).join(", "));
|
|
1112
|
+
if (d.prompt_registry && d.prompt_registry.layers) {
|
|
1113
|
+
console.log("");
|
|
1114
|
+
console.log(C.bold + " Prompt Registry:" + C.reset);
|
|
1115
|
+
d.prompt_registry.layers.forEach(function(l) { console.log(" " + l); });
|
|
1116
|
+
}
|
|
1117
|
+
console.log("");
|
|
1118
|
+
} else { printError(resp.data.error || "Status failed"); }
|
|
1119
|
+
} catch(e) {
|
|
1120
|
+
process.stdout.write("\r" + " ".repeat(20) + "\r");
|
|
1121
|
+
printError(e.message);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function cmdVersions() {
|
|
1126
|
+
try {
|
|
1127
|
+
var resp = await apiCall("GET", "/versions");
|
|
1128
|
+
if (resp.status === 200) {
|
|
1129
|
+
console.log("");
|
|
1130
|
+
console.log(C.bold + "Prompt Versions:" + C.reset);
|
|
1131
|
+
if (resp.data.versions) {
|
|
1132
|
+
resp.data.versions.forEach(function(v) {
|
|
1133
|
+
console.log(" " + (v.active ? C.green + "● " : C.dim + "○ ") + v.version + C.reset + " " + v.file);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
console.log("");
|
|
1137
|
+
} else { printError(resp.data.error || "Versions failed"); }
|
|
1138
|
+
} catch(e) { printError(e.message); }
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function cmdHealth() {
|
|
1142
|
+
try {
|
|
1143
|
+
var resp = await apiCall("GET", "/health");
|
|
1144
|
+
if (resp.status === 200) {
|
|
1145
|
+
printSuccess("API healthy — " + resp.data.model + " | uptime: " + Math.round(resp.data.uptime) + "s");
|
|
1146
|
+
} else { printError("API unhealthy: " + resp.status); }
|
|
1147
|
+
} catch(e) { printError("API unreachable: " + e.message); }
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function cmdWatchdog(args) {
|
|
1151
|
+
if (args === "on") {
|
|
1152
|
+
config.watchdog.enabled = true;
|
|
1153
|
+
saveConfig(config);
|
|
1154
|
+
printSuccess("Watchdog enabled (interval: " + config.watchdog.interval + "s)");
|
|
1155
|
+
} else if (args === "off") {
|
|
1156
|
+
config.watchdog.enabled = false;
|
|
1157
|
+
saveConfig(config);
|
|
1158
|
+
printSuccess("Watchdog disabled.");
|
|
1159
|
+
} else {
|
|
1160
|
+
console.log("");
|
|
1161
|
+
console.log(C.bold + "Watchdog:" + C.reset);
|
|
1162
|
+
console.log(" Status: " + (config.watchdog.enabled ? C.green + "ON" : C.red + "OFF") + C.reset);
|
|
1163
|
+
console.log(" Interval: " + config.watchdog.interval + "s");
|
|
1164
|
+
console.log("");
|
|
1165
|
+
console.log(C.dim + " /watchdog on — Enable" + C.reset);
|
|
1166
|
+
console.log(C.dim + " /watchdog off — Disable" + C.reset);
|
|
1167
|
+
console.log("");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function cmdMemory(args) {
|
|
1172
|
+
if (!args) {
|
|
1173
|
+
// List memory
|
|
1174
|
+
var files = fs.existsSync(MEMORY_DIR) ? fs.readdirSync(MEMORY_DIR) : [];
|
|
1175
|
+
console.log("");
|
|
1176
|
+
console.log(C.bold + "Local Memory (" + files.length + " entries):" + C.reset);
|
|
1177
|
+
files.forEach(function(f) {
|
|
1178
|
+
var content = fs.readFileSync(path.join(MEMORY_DIR, f), "utf8").trim();
|
|
1179
|
+
console.log(" " + C.yellow + f.replace(".txt", "") + C.reset + " — " + content.slice(0, 60));
|
|
1180
|
+
});
|
|
1181
|
+
if (files.length === 0) console.log(C.dim + " (empty)" + C.reset);
|
|
1182
|
+
console.log("");
|
|
1183
|
+
} else {
|
|
1184
|
+
var parts = args.split(/\s+/);
|
|
1185
|
+
if (parts[0] === "save" && parts.length >= 3) {
|
|
1186
|
+
var key = parts[1];
|
|
1187
|
+
var value = parts.slice(2).join(" ");
|
|
1188
|
+
fs.writeFileSync(path.join(MEMORY_DIR, key + ".txt"), value);
|
|
1189
|
+
printSuccess("Saved: " + key);
|
|
1190
|
+
} else if (parts[0] === "del" && parts[1]) {
|
|
1191
|
+
var file = path.join(MEMORY_DIR, parts[1] + ".txt");
|
|
1192
|
+
if (fs.existsSync(file)) { fs.unlinkSync(file); printSuccess("Deleted: " + parts[1]); }
|
|
1193
|
+
else { printError("Not found: " + parts[1]); }
|
|
1194
|
+
} else {
|
|
1195
|
+
printError("Usage: /memory save <key> <value> or /memory del <key>");
|
|
98
1196
|
}
|
|
99
1197
|
}
|
|
100
1198
|
}
|
|
101
1199
|
|
|
102
|
-
function
|
|
103
|
-
|
|
1200
|
+
function cmdFiles() {
|
|
1201
|
+
try {
|
|
1202
|
+
var files = fs.readdirSync(config.workdir).slice(0, 30);
|
|
1203
|
+
console.log("");
|
|
1204
|
+
console.log(C.bold + "Workdir: " + config.workdir + C.reset);
|
|
1205
|
+
files.forEach(function(f) {
|
|
1206
|
+
var stat = fs.statSync(path.join(config.workdir, f));
|
|
1207
|
+
console.log(" " + (stat.isDirectory() ? C.blue + "📁 " : "📄 ") + f + C.reset +
|
|
1208
|
+
(stat.isFile() ? C.dim + " (" + stat.size + " bytes)" + C.reset : ""));
|
|
1209
|
+
});
|
|
1210
|
+
console.log("");
|
|
1211
|
+
} catch(e) { printError(e.message); }
|
|
104
1212
|
}
|
|
105
1213
|
|
|
106
|
-
|
|
107
|
-
|
|
1214
|
+
// ── Shell Execution ──
|
|
1215
|
+
function cmdShell(command) {
|
|
1216
|
+
if (!command) { printError("Usage: /sh <command>"); return; }
|
|
1217
|
+
try {
|
|
1218
|
+
console.log(C.dim + "$ " + command + C.reset);
|
|
1219
|
+
var output = execSync(command, { cwd: config.workdir, encoding: "utf8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1220
|
+
console.log(output);
|
|
1221
|
+
} catch(e) {
|
|
1222
|
+
if (e.stdout) console.log(e.stdout);
|
|
1223
|
+
if (e.stderr) console.log(C.red + e.stderr + C.reset);
|
|
1224
|
+
printError("Exit code: " + (e.status || "unknown"));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// ── Git Commands ──
|
|
1229
|
+
function cmdGit(args) {
|
|
1230
|
+
if (!args) {
|
|
1231
|
+
// Show git status
|
|
1232
|
+
cmdShell("git status --short");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
var sub = args.split(/\s+/)[0];
|
|
1236
|
+
var rest = args.slice(sub.length).trim();
|
|
1237
|
+
|
|
1238
|
+
switch(sub) {
|
|
1239
|
+
case "status": cmdShell("git status"); break;
|
|
1240
|
+
case "log": cmdShell("git log --oneline -10"); break;
|
|
1241
|
+
case "diff": cmdShell("git diff --stat"); break;
|
|
1242
|
+
case "add":
|
|
1243
|
+
cmdShell("git add " + (rest || "."));
|
|
1244
|
+
printSuccess("Files staged.");
|
|
1245
|
+
break;
|
|
1246
|
+
case "commit":
|
|
1247
|
+
if (!rest) { printError("Usage: /git commit <message>"); return; }
|
|
1248
|
+
cmdShell('git commit -m "' + rest.replace(/"/g, '\\"') + '"');
|
|
1249
|
+
break;
|
|
1250
|
+
case "push":
|
|
1251
|
+
var remote = rest || "origin";
|
|
1252
|
+
try {
|
|
1253
|
+
var branch = execSync("git branch --show-current", { cwd: config.workdir, encoding: "utf8" }).trim();
|
|
1254
|
+
printInfo("Pushing " + branch + " to " + remote + "...");
|
|
1255
|
+
cmdShell("git push " + remote + " " + branch);
|
|
1256
|
+
printSuccess("Pushed!");
|
|
1257
|
+
} catch(e) { printError("Push failed: " + e.message); }
|
|
1258
|
+
break;
|
|
1259
|
+
case "pull":
|
|
1260
|
+
cmdShell("git pull " + (rest || ""));
|
|
1261
|
+
break;
|
|
1262
|
+
case "branch":
|
|
1263
|
+
cmdShell("git branch " + (rest || "-a"));
|
|
1264
|
+
break;
|
|
1265
|
+
case "checkout":
|
|
1266
|
+
if (!rest) { printError("Usage: /git checkout <branch>"); return; }
|
|
1267
|
+
cmdShell("git checkout " + rest);
|
|
1268
|
+
break;
|
|
1269
|
+
case "clone":
|
|
1270
|
+
if (!rest) { printError("Usage: /git clone <url>"); return; }
|
|
1271
|
+
cmdShell("git clone " + rest);
|
|
1272
|
+
break;
|
|
1273
|
+
case "init":
|
|
1274
|
+
cmdShell("git init");
|
|
1275
|
+
break;
|
|
1276
|
+
case "remote":
|
|
1277
|
+
cmdShell("git remote -v");
|
|
1278
|
+
break;
|
|
1279
|
+
default:
|
|
1280
|
+
cmdShell("git " + args);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// ── SSH Commands ──
|
|
1285
|
+
function cmdSsh(args) {
|
|
1286
|
+
if (!args) {
|
|
1287
|
+
// Show saved connections
|
|
1288
|
+
var sshFile = path.join(CONFIG_DIR, "ssh.json");
|
|
1289
|
+
if (fs.existsSync(sshFile)) {
|
|
1290
|
+
var connections = JSON.parse(fs.readFileSync(sshFile, "utf8"));
|
|
1291
|
+
console.log("");
|
|
1292
|
+
console.log(C.bold + "SSH Connections:" + C.reset);
|
|
1293
|
+
for (var name in connections) {
|
|
1294
|
+
var c = connections[name];
|
|
1295
|
+
console.log(" " + C.yellow + name + C.reset + " — " + c.user + "@" + c.host + (c.port !== 22 ? ":" + c.port : ""));
|
|
1296
|
+
}
|
|
1297
|
+
console.log("");
|
|
1298
|
+
console.log(C.dim + " /ssh <name> <command> — Run command" + C.reset);
|
|
1299
|
+
console.log(C.dim + " /ssh add <name> <user@host> — Add connection" + C.reset);
|
|
1300
|
+
console.log(C.dim + " /ssh del <name> — Remove connection" + C.reset);
|
|
1301
|
+
} else {
|
|
1302
|
+
printInfo("No SSH connections saved. Use /ssh add <name> <user@host>");
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
var parts = args.split(/\s+/);
|
|
1308
|
+
var sshFile = path.join(CONFIG_DIR, "ssh.json");
|
|
1309
|
+
var connections = {};
|
|
1310
|
+
if (fs.existsSync(sshFile)) {
|
|
1311
|
+
try { connections = JSON.parse(fs.readFileSync(sshFile, "utf8")); } catch(e) {}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (parts[0] === "add" && parts[1] && parts[2]) {
|
|
1315
|
+
var match = parts[2].match(/^([^@]+)@([^:]+)(?::(\d+))?$/);
|
|
1316
|
+
if (!match) { printError("Format: user@host or user@host:port"); return; }
|
|
1317
|
+
connections[parts[1]] = { user: match[1], host: match[2], port: parseInt(match[3] || "22"), key: parts[3] || null };
|
|
1318
|
+
fs.writeFileSync(sshFile, JSON.stringify(connections, null, 2));
|
|
1319
|
+
printSuccess("SSH connection '" + parts[1] + "' saved.");
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (parts[0] === "del" && parts[1]) {
|
|
1324
|
+
delete connections[parts[1]];
|
|
1325
|
+
fs.writeFileSync(sshFile, JSON.stringify(connections, null, 2));
|
|
1326
|
+
printSuccess("SSH connection '" + parts[1] + "' removed.");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Execute command on saved connection
|
|
1331
|
+
var connName = parts[0];
|
|
1332
|
+
var remoteCmd = parts.slice(1).join(" ");
|
|
1333
|
+
if (!connections[connName]) { printError("Unknown connection: " + connName + ". Use /ssh to list."); return; }
|
|
1334
|
+
if (!remoteCmd) { printError("Usage: /ssh " + connName + " <command>"); return; }
|
|
1335
|
+
|
|
1336
|
+
var conn = connections[connName];
|
|
1337
|
+
var sshCmd = "ssh";
|
|
1338
|
+
if (conn.key) sshCmd += " -i " + conn.key;
|
|
1339
|
+
sshCmd += " -o ConnectTimeout=10 -o StrictHostKeyChecking=no";
|
|
1340
|
+
sshCmd += " -p " + conn.port;
|
|
1341
|
+
sshCmd += " " + conn.user + "@" + conn.host;
|
|
1342
|
+
sshCmd += ' "' + remoteCmd.replace(/"/g, '\\"') + '"';
|
|
1343
|
+
|
|
1344
|
+
printInfo("SSH " + connName + ": " + remoteCmd);
|
|
1345
|
+
cmdShell(sshCmd);
|
|
108
1346
|
}
|
|
109
1347
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1348
|
+
// ── Deploy Command ──
|
|
1349
|
+
async function cmdDeploy(args) {
|
|
1350
|
+
if (!args) {
|
|
1351
|
+
console.log("");
|
|
1352
|
+
console.log(C.bold + "Deploy Options:" + C.reset);
|
|
1353
|
+
console.log(" /deploy git — git add + commit + push");
|
|
1354
|
+
console.log(" /deploy ssh <name> <cmd> — Run deploy command via SSH");
|
|
1355
|
+
console.log(" /deploy sync <name> — rsync workdir to server");
|
|
1356
|
+
console.log(" /deploy pm2 <name> <app> — pm2 restart on server");
|
|
1357
|
+
console.log("");
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
113
1360
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1361
|
+
var parts = args.split(/\s+/);
|
|
1362
|
+
var action = parts[0];
|
|
1363
|
+
|
|
1364
|
+
if (action === "git") {
|
|
1365
|
+
printInfo("Deploying via git...");
|
|
1366
|
+
try {
|
|
1367
|
+
var status = execSync("git status --porcelain", { cwd: config.workdir, encoding: "utf8" }).trim();
|
|
1368
|
+
if (status) {
|
|
1369
|
+
cmdShell("git add .");
|
|
1370
|
+
var msg = parts.slice(1).join(" ") || "deploy: " + new Date().toISOString().slice(0, 16);
|
|
1371
|
+
cmdShell('git commit -m "' + msg + '"');
|
|
1372
|
+
}
|
|
1373
|
+
var branch = execSync("git branch --show-current", { cwd: config.workdir, encoding: "utf8" }).trim();
|
|
1374
|
+
cmdShell("git push origin " + branch);
|
|
1375
|
+
printSuccess("Deployed via git push!");
|
|
1376
|
+
} catch(e) { printError("Deploy failed: " + e.message); }
|
|
1377
|
+
|
|
1378
|
+
} else if (action === "ssh" && parts[1]) {
|
|
1379
|
+
cmdSsh(parts.slice(1).join(" "));
|
|
1380
|
+
|
|
1381
|
+
} else if (action === "sync" && parts[1]) {
|
|
1382
|
+
var sshFile = path.join(CONFIG_DIR, "ssh.json");
|
|
1383
|
+
if (!fs.existsSync(sshFile)) { printError("No SSH connections. Use /ssh add first."); return; }
|
|
1384
|
+
var connections = JSON.parse(fs.readFileSync(sshFile, "utf8"));
|
|
1385
|
+
var conn = connections[parts[1]];
|
|
1386
|
+
if (!conn) { printError("Unknown connection: " + parts[1]); return; }
|
|
1387
|
+
var dest = parts[2] || "/root/" + path.basename(config.workdir);
|
|
1388
|
+
var rsyncCmd = "rsync -avz --exclude node_modules --exclude .git";
|
|
1389
|
+
if (conn.key) rsyncCmd += " -e 'ssh -i " + conn.key + " -p " + conn.port + "'";
|
|
1390
|
+
rsyncCmd += " " + config.workdir + "/ " + conn.user + "@" + conn.host + ":" + dest + "/";
|
|
1391
|
+
printInfo("Syncing to " + parts[1] + ":" + dest);
|
|
1392
|
+
cmdShell(rsyncCmd);
|
|
1393
|
+
|
|
1394
|
+
} else if (action === "pm2" && parts[1] && parts[2]) {
|
|
1395
|
+
cmdSsh(parts[1] + " pm2 restart " + parts[2]);
|
|
1396
|
+
|
|
1397
|
+
} else {
|
|
1398
|
+
printError("Unknown deploy action. Use /deploy for help.");
|
|
122
1399
|
}
|
|
1400
|
+
}
|
|
123
1401
|
|
|
124
|
-
|
|
1402
|
+
// ── Read/Write Files ──
|
|
1403
|
+
function cmdRead(filePath) {
|
|
1404
|
+
if (!filePath) { printError("Usage: /read <file>"); return; }
|
|
1405
|
+
var full = path.resolve(config.workdir, filePath);
|
|
1406
|
+
if (!fs.existsSync(full)) { printError("File not found: " + full); return; }
|
|
1407
|
+
var content = fs.readFileSync(full, "utf8");
|
|
1408
|
+
console.log("");
|
|
1409
|
+
console.log(C.bold + "File: " + full + C.reset);
|
|
1410
|
+
console.log(C.dim + "─".repeat(50) + C.reset);
|
|
1411
|
+
var lines = content.split("\n");
|
|
1412
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1413
|
+
console.log(C.dim + String(i + 1).padStart(4) + " │ " + C.reset + lines[i]);
|
|
1414
|
+
}
|
|
1415
|
+
console.log("");
|
|
125
1416
|
}
|
|
126
1417
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1418
|
+
function cmdWrite(args) {
|
|
1419
|
+
if (!args) { printError("Usage: /write <file> <content> or /write <file> (then type content, end with EOF)"); return; }
|
|
1420
|
+
var parts = args.split(/\s+/);
|
|
1421
|
+
var filePath = parts[0];
|
|
1422
|
+
var content = parts.slice(1).join(" ");
|
|
1423
|
+
var full = path.resolve(config.workdir, filePath);
|
|
1424
|
+
|
|
1425
|
+
if (content) {
|
|
1426
|
+
fs.writeFileSync(full, content);
|
|
1427
|
+
printSuccess("Written: " + full + " (" + content.length + " bytes)");
|
|
1428
|
+
} else {
|
|
1429
|
+
printInfo("Type content (end with line 'EOF'):");
|
|
1430
|
+
// This will be handled differently in interactive mode
|
|
1431
|
+
printError("For multi-line write, use: /generate instead");
|
|
1432
|
+
}
|
|
132
1433
|
}
|
|
133
1434
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1435
|
+
// ── Project Init ──
|
|
1436
|
+
function cmdInit(args) {
|
|
1437
|
+
var name = args || path.basename(config.workdir);
|
|
1438
|
+
var projectDir = args ? path.join(config.workdir, args) : config.workdir;
|
|
1439
|
+
|
|
1440
|
+
if (args && !fs.existsSync(projectDir)) {
|
|
1441
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
140
1442
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1443
|
+
|
|
1444
|
+
// Create project memory file
|
|
1445
|
+
var memFile = path.join(projectDir, "AGENT.md");
|
|
1446
|
+
if (!fs.existsSync(memFile)) {
|
|
1447
|
+
var agentContent = "# " + name + "\n\n" +
|
|
1448
|
+
"## Projekt\n" +
|
|
1449
|
+
"Beschreibung: \n\n" +
|
|
1450
|
+
"## Stack\n" +
|
|
1451
|
+
"- \n\n" +
|
|
1452
|
+
"## Regeln\n" +
|
|
1453
|
+
"- \n\n" +
|
|
1454
|
+
"## Status\n" +
|
|
1455
|
+
"Erstellt: " + new Date().toISOString().split("T")[0] + "\n";
|
|
1456
|
+
fs.writeFileSync(memFile, agentContent);
|
|
1457
|
+
printSuccess("AGENT.md erstellt in " + projectDir);
|
|
1458
|
+
} else {
|
|
1459
|
+
printInfo("AGENT.md existiert bereits.");
|
|
144
1460
|
}
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
1461
|
+
|
|
1462
|
+
// Git init if not already
|
|
1463
|
+
try {
|
|
1464
|
+
execSync("git rev-parse --git-dir", { cwd: projectDir, stdio: "pipe" });
|
|
1465
|
+
printInfo("Git repo already initialized.");
|
|
1466
|
+
} catch(e) {
|
|
1467
|
+
execSync("git init", { cwd: projectDir, stdio: "pipe" });
|
|
1468
|
+
printSuccess("Git repo initialized.");
|
|
148
1469
|
}
|
|
149
|
-
|
|
150
|
-
if
|
|
151
|
-
|
|
1470
|
+
|
|
1471
|
+
// Create .gitignore if missing
|
|
1472
|
+
var gitignore = path.join(projectDir, ".gitignore");
|
|
1473
|
+
if (!fs.existsSync(gitignore)) {
|
|
1474
|
+
fs.writeFileSync(gitignore, "node_modules/\n.env\n*.log\n.blun/\n");
|
|
1475
|
+
printSuccess(".gitignore erstellt.");
|
|
152
1476
|
}
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
1477
|
+
|
|
1478
|
+
if (args) {
|
|
1479
|
+
config.workdir = projectDir;
|
|
1480
|
+
saveConfig(config);
|
|
1481
|
+
printSuccess("Workdir set to: " + projectDir);
|
|
159
1482
|
}
|
|
160
|
-
return { category: "general", content: raw };
|
|
161
1483
|
}
|
|
162
1484
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1485
|
+
// ── Plugins (MCP-style) ──
|
|
1486
|
+
const PLUGINS_FILE = path.join(CONFIG_DIR, "plugins.json");
|
|
1487
|
+
const PERMISSIONS_FILE = path.join(CONFIG_DIR, "permissions.json");
|
|
1488
|
+
|
|
1489
|
+
function loadPlugins() {
|
|
1490
|
+
if (fs.existsSync(PLUGINS_FILE)) {
|
|
1491
|
+
try { return JSON.parse(fs.readFileSync(PLUGINS_FILE, "utf8")); } catch(e) {}
|
|
1492
|
+
}
|
|
1493
|
+
return {};
|
|
166
1494
|
}
|
|
167
1495
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
1496
|
+
function savePlugins(p) { fs.writeFileSync(PLUGINS_FILE, JSON.stringify(p, null, 2)); }
|
|
1497
|
+
|
|
1498
|
+
function loadPermissions() {
|
|
1499
|
+
if (fs.existsSync(PERMISSIONS_FILE)) {
|
|
1500
|
+
try { return JSON.parse(fs.readFileSync(PERMISSIONS_FILE, "utf8")); } catch(e) {}
|
|
1501
|
+
}
|
|
1502
|
+
return { mode: "ask", allowed: [], denied: [] };
|
|
171
1503
|
}
|
|
172
1504
|
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
1505
|
+
function savePermissions(p) { fs.writeFileSync(PERMISSIONS_FILE, JSON.stringify(p, null, 2)); }
|
|
1506
|
+
|
|
1507
|
+
// Built-in plugin templates
|
|
1508
|
+
var PLUGIN_TEMPLATES = {
|
|
1509
|
+
telegram: {
|
|
1510
|
+
name: "telegram",
|
|
1511
|
+
description: "Telegram Bot Bridge — send/receive messages",
|
|
1512
|
+
type: "builtin",
|
|
1513
|
+
config: { bot_token: "", chat_id: "" },
|
|
1514
|
+
commands: ["/tg send <msg>", "/tg status"],
|
|
1515
|
+
setup: ["bot_token", "chat_id"]
|
|
1516
|
+
},
|
|
1517
|
+
github: {
|
|
1518
|
+
name: "github",
|
|
1519
|
+
description: "GitHub Integration — repos, issues, PRs",
|
|
1520
|
+
type: "builtin",
|
|
1521
|
+
config: { token: "", default_repo: "" },
|
|
1522
|
+
commands: ["/gh repos", "/gh issues", "/gh pr"],
|
|
1523
|
+
setup: ["token"]
|
|
1524
|
+
},
|
|
1525
|
+
browser: {
|
|
1526
|
+
name: "browser",
|
|
1527
|
+
description: "Playwright Browser — screenshots, rendering, scraping",
|
|
1528
|
+
type: "builtin",
|
|
1529
|
+
config: {},
|
|
1530
|
+
commands: ["/screenshot <url>", "/render <url>"],
|
|
1531
|
+
setup: []
|
|
1532
|
+
},
|
|
1533
|
+
slack: {
|
|
1534
|
+
name: "slack",
|
|
1535
|
+
description: "Slack Webhook — send messages to channels",
|
|
1536
|
+
type: "builtin",
|
|
1537
|
+
config: { webhook_url: "" },
|
|
1538
|
+
commands: ["/slack send <msg>"],
|
|
1539
|
+
setup: ["webhook_url"]
|
|
1540
|
+
},
|
|
1541
|
+
docker: {
|
|
1542
|
+
name: "docker",
|
|
1543
|
+
description: "Docker Management — containers, images, logs",
|
|
1544
|
+
type: "builtin",
|
|
1545
|
+
config: { host: "localhost" },
|
|
1546
|
+
commands: ["/docker ps", "/docker logs <container>"],
|
|
1547
|
+
setup: []
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
function cmdPlugin(args) {
|
|
1552
|
+
var plugins = loadPlugins();
|
|
1553
|
+
var parts = (args || "").split(/\s+/);
|
|
1554
|
+
var action = parts[0] || "list";
|
|
1555
|
+
|
|
1556
|
+
if (action === "list") {
|
|
1557
|
+
console.log("");
|
|
1558
|
+
console.log(C.bold + " " + BOX.bot + " Installed Plugins:" + C.reset);
|
|
1559
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
1560
|
+
var names = Object.keys(plugins);
|
|
1561
|
+
if (names.length === 0) {
|
|
1562
|
+
console.log(C.gray + " (none installed)" + C.reset);
|
|
1563
|
+
} else {
|
|
1564
|
+
names.forEach(function(name) {
|
|
1565
|
+
var p = plugins[name];
|
|
1566
|
+
var status = p.running ? C.green + BOX.dot + " running" : C.gray + BOX.dot + " stopped";
|
|
1567
|
+
console.log(" " + C.brightCyan + name + C.reset + " — " + (p.description || "") + " " + status + C.reset);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
console.log("");
|
|
1571
|
+
console.log(C.gray + " Available: " + Object.keys(PLUGIN_TEMPLATES).join(", ") + C.reset);
|
|
1572
|
+
console.log(C.gray + " Or add custom: /plugin add <npm-package> or /plugin add <path>" + C.reset);
|
|
1573
|
+
console.log("");
|
|
1574
|
+
|
|
1575
|
+
} else if (action === "add" && parts[1]) {
|
|
1576
|
+
var pluginName = parts[1];
|
|
1577
|
+
if (PLUGIN_TEMPLATES[pluginName]) {
|
|
1578
|
+
// Built-in plugin
|
|
1579
|
+
var tmpl = PLUGIN_TEMPLATES[pluginName];
|
|
1580
|
+
plugins[pluginName] = {
|
|
1581
|
+
name: tmpl.name,
|
|
1582
|
+
description: tmpl.description,
|
|
1583
|
+
type: tmpl.type,
|
|
1584
|
+
config: Object.assign({}, tmpl.config),
|
|
1585
|
+
commands: tmpl.commands,
|
|
1586
|
+
installed: new Date().toISOString(),
|
|
1587
|
+
running: false
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
// If setup fields needed, prompt
|
|
1591
|
+
if (tmpl.setup && tmpl.setup.length > 0) {
|
|
1592
|
+
printInfo("Plugin '" + pluginName + "' needs configuration:");
|
|
1593
|
+
tmpl.setup.forEach(function(field) {
|
|
1594
|
+
printInfo(" Set with: /set plugin." + pluginName + "." + field + " <value>");
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
savePlugins(plugins);
|
|
1599
|
+
printSuccess("Plugin '" + pluginName + "' installed!");
|
|
1600
|
+
} else {
|
|
1601
|
+
// Custom: npm package or local path
|
|
1602
|
+
printInfo("Installing custom plugin: " + pluginName);
|
|
1603
|
+
try {
|
|
1604
|
+
execSync("npm install " + pluginName, { cwd: CONFIG_DIR, encoding: "utf8", stdio: "pipe" });
|
|
1605
|
+
plugins[pluginName] = {
|
|
1606
|
+
name: pluginName,
|
|
1607
|
+
description: "Custom MCP server",
|
|
1608
|
+
type: "npm",
|
|
1609
|
+
config: {},
|
|
1610
|
+
commands: [],
|
|
1611
|
+
installed: new Date().toISOString(),
|
|
1612
|
+
running: false
|
|
1613
|
+
};
|
|
1614
|
+
savePlugins(plugins);
|
|
1615
|
+
printSuccess("Plugin '" + pluginName + "' installed via npm!");
|
|
1616
|
+
} catch(e) {
|
|
1617
|
+
printError("Install failed: " + e.message.slice(0, 200));
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
} else if (action === "remove" && parts[1]) {
|
|
1622
|
+
delete plugins[parts[1]];
|
|
1623
|
+
savePlugins(plugins);
|
|
1624
|
+
printSuccess("Plugin '" + parts[1] + "' removed.");
|
|
1625
|
+
|
|
1626
|
+
} else if (action === "run" && parts[1]) {
|
|
1627
|
+
if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
|
|
1628
|
+
plugins[parts[1]].running = true;
|
|
1629
|
+
savePlugins(plugins);
|
|
1630
|
+
printSuccess("Plugin '" + parts[1] + "' started.");
|
|
1631
|
+
|
|
1632
|
+
} else if (action === "stop" && parts[1]) {
|
|
1633
|
+
if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
|
|
1634
|
+
plugins[parts[1]].running = false;
|
|
1635
|
+
savePlugins(plugins);
|
|
1636
|
+
printSuccess("Plugin '" + parts[1] + "' stopped.");
|
|
1637
|
+
|
|
1638
|
+
} else {
|
|
1639
|
+
printError("Usage: /plugin list|add|remove|run|stop <name>");
|
|
1640
|
+
}
|
|
176
1641
|
}
|
|
177
1642
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1643
|
+
function cmdPermissions(args) {
|
|
1644
|
+
var perms = loadPermissions();
|
|
1645
|
+
if (!args) {
|
|
1646
|
+
console.log("");
|
|
1647
|
+
console.log(C.bold + " Permissions:" + C.reset);
|
|
1648
|
+
console.log(" Mode: " + C.brightCyan + perms.mode + C.reset);
|
|
1649
|
+
console.log(" Allowed: " + (perms.allowed.length > 0 ? perms.allowed.join(", ") : C.gray + "(none)" + C.reset));
|
|
1650
|
+
console.log(" Denied: " + (perms.denied.length > 0 ? perms.denied.join(", ") : C.gray + "(none)" + C.reset));
|
|
1651
|
+
console.log("");
|
|
1652
|
+
console.log(C.gray + " /permissions allow-all — Skip all permission prompts" + C.reset);
|
|
1653
|
+
console.log(C.gray + " /permissions ask — Ask before each tool call" + C.reset);
|
|
1654
|
+
console.log("");
|
|
1655
|
+
} else if (args === "allow-all") {
|
|
1656
|
+
perms.mode = "allow-all";
|
|
1657
|
+
savePermissions(perms);
|
|
1658
|
+
printSuccess("Permission mode: allow-all (no prompts)");
|
|
1659
|
+
} else if (args === "ask") {
|
|
1660
|
+
perms.mode = "ask";
|
|
1661
|
+
savePermissions(perms);
|
|
1662
|
+
printSuccess("Permission mode: ask (prompt before each tool call)");
|
|
1663
|
+
}
|
|
182
1664
|
}
|
|
183
1665
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
1666
|
+
// ── Agent Loop (CLI wrapper) ──
|
|
1667
|
+
async function cmdAgent(args) {
|
|
1668
|
+
if (!args) { printError("Usage: /agent <goal> — runs autonomously until done"); return; }
|
|
1669
|
+
try {
|
|
1670
|
+
printUserMessage(args);
|
|
1671
|
+
var maxLoops = 20;
|
|
1672
|
+
var loop = 0;
|
|
1673
|
+
var totalFiles = [];
|
|
1674
|
+
var lastAnswer = "";
|
|
1675
|
+
var goal = args;
|
|
1676
|
+
var context = [];
|
|
1677
|
+
var workdir = config.workdir;
|
|
1678
|
+
|
|
1679
|
+
// Show thinking status on single line
|
|
1680
|
+
console.log("");
|
|
1681
|
+
var thinkTimer = setInterval(function() {
|
|
1682
|
+
var phase = loop === 0 ? "planning" : "step " + loop + "/" + maxLoops;
|
|
1683
|
+
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " [" + phase + "] working..." + C.reset);
|
|
1684
|
+
}, 300);
|
|
1685
|
+
|
|
1686
|
+
while (loop < maxLoops) {
|
|
1687
|
+
loop++;
|
|
1688
|
+
|
|
1689
|
+
var resp = await apiCall("POST", "/agent", {
|
|
1690
|
+
goal: goal,
|
|
1691
|
+
workdir: workdir,
|
|
1692
|
+
context: context.slice(-5),
|
|
1693
|
+
verbose: false
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
if (resp.status !== 200) {
|
|
1697
|
+
clearInterval(thinkTimer);
|
|
1698
|
+
process.stdout.write("\r\x1b[2K");
|
|
1699
|
+
printError("Step " + loop + " failed: " + (resp.data.error || "Error"));
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
var d = resp.data;
|
|
1704
|
+
lastAnswer = d.answer || "";
|
|
1705
|
+
|
|
1706
|
+
// Save files locally to workspace — API now returns file contents
|
|
1707
|
+
if (d.files && d.files.length > 0) {
|
|
1708
|
+
for (var fi = 0; fi < d.files.length; fi++) {
|
|
1709
|
+
var f = d.files[fi];
|
|
1710
|
+
if (f.content && f.name) {
|
|
1711
|
+
var localPath = path.join(workdir, f.name);
|
|
1712
|
+
var localDir = path.dirname(localPath);
|
|
1713
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
1714
|
+
fs.writeFileSync(localPath, f.content, "utf8");
|
|
1715
|
+
f.localPath = localPath;
|
|
1716
|
+
}
|
|
1717
|
+
totalFiles.push(f);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// If answer contains code blocks with filenames, extract and save them
|
|
1722
|
+
var codeBlockRe = /```(?:\w+)?\s*\n\/\/\s*(.+\.(?:html|css|js|json|md|txt|py))\n([\s\S]*?)```/g;
|
|
1723
|
+
var cbMatch;
|
|
1724
|
+
while ((cbMatch = codeBlockRe.exec(lastAnswer)) !== null) {
|
|
1725
|
+
var cbFile = cbMatch[1].trim();
|
|
1726
|
+
var cbContent = cbMatch[2];
|
|
1727
|
+
var cbPath = path.join(workdir, cbFile);
|
|
1728
|
+
var cbDir = path.dirname(cbPath);
|
|
1729
|
+
if (!fs.existsSync(cbDir)) fs.mkdirSync(cbDir, { recursive: true });
|
|
1730
|
+
fs.writeFileSync(cbPath, cbContent, "utf8");
|
|
1731
|
+
totalFiles.push({ name: cbFile, localPath: cbPath });
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Also extract filename: pattern like "**index.html**:" or "Datei: index.html"
|
|
1735
|
+
var fileHeaderRe = /(?:\*\*|Datei:\s*)([a-zA-Z0-9_\-/.]+\.(?:html|css|js|json|md|txt|py))\*?\*?:?\s*\n```(?:\w+)?\n([\s\S]*?)```/g;
|
|
1736
|
+
var fhMatch;
|
|
1737
|
+
while ((fhMatch = fileHeaderRe.exec(lastAnswer)) !== null) {
|
|
1738
|
+
var fhFile = fhMatch[1].trim();
|
|
1739
|
+
var fhContent = fhMatch[2];
|
|
1740
|
+
var fhPath = path.join(workdir, fhFile);
|
|
1741
|
+
var fhDir = path.dirname(fhPath);
|
|
1742
|
+
if (!fs.existsSync(fhDir)) fs.mkdirSync(fhDir, { recursive: true });
|
|
1743
|
+
if (!totalFiles.some(function(tf) { return tf.name === fhFile; })) {
|
|
1744
|
+
fs.writeFileSync(fhPath, fhContent, "utf8");
|
|
1745
|
+
totalFiles.push({ name: fhFile, localPath: fhPath });
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
context.push({ step: loop, answer: lastAnswer.slice(0, 500), files: totalFiles.length, workdir: workdir });
|
|
1750
|
+
|
|
1751
|
+
// Check if the agent says it's done
|
|
1752
|
+
var doneSignals = ["done", "complete", "finished", "fertig", "erledigt", "all steps", "nothing left"];
|
|
1753
|
+
var isDone = doneSignals.some(function(s) { return lastAnswer.toLowerCase().includes(s); });
|
|
1754
|
+
|
|
1755
|
+
if (isDone) break;
|
|
1756
|
+
|
|
1757
|
+
// Feed result back as next goal
|
|
1758
|
+
goal = "Continue with the original goal: " + args +
|
|
1759
|
+
"\nWorkspace: " + workdir +
|
|
1760
|
+
"\nFiles created so far: " + totalFiles.map(function(f) { return f.name; }).join(", ") +
|
|
1761
|
+
"\n\nLast step result: " + lastAnswer.slice(0, 1000) +
|
|
1762
|
+
"\n\nWrite the actual file contents as code blocks. If everything is done, say 'done'.";
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
clearInterval(thinkTimer);
|
|
1766
|
+
process.stdout.write("\r\x1b[2K");
|
|
1767
|
+
|
|
1768
|
+
// Final summary
|
|
1769
|
+
console.log("");
|
|
1770
|
+
var savedCount = totalFiles.filter(function(f) { return f.localPath; }).length;
|
|
1771
|
+
var meta = loop + " loops" + (totalFiles.length > 0 ? " " + BOX.dot + " " + savedCount + " files saved" : "");
|
|
1772
|
+
|
|
1773
|
+
// Token tracking
|
|
1774
|
+
var inTok = Math.ceil(args.length / 3.5);
|
|
1775
|
+
var outTok = Math.ceil(lastAnswer.length / 3.5);
|
|
1776
|
+
sessionCost.requests += loop;
|
|
1777
|
+
sessionCost.inputTokensEst += inTok * loop;
|
|
1778
|
+
sessionCost.outputTokensEst += outTok;
|
|
1779
|
+
|
|
1780
|
+
await streamAnswer(lastAnswer, meta);
|
|
1781
|
+
|
|
1782
|
+
if (totalFiles.length > 0) {
|
|
1783
|
+
console.log(C.green + " 📁 Saved to workspace:" + C.reset);
|
|
1784
|
+
totalFiles.forEach(function(f) {
|
|
1785
|
+
if (f.localPath) console.log(" " + C.brightCyan + f.localPath + C.reset);
|
|
1786
|
+
else console.log(" " + C.dim + f.name + " (not saved)" + C.reset);
|
|
1787
|
+
});
|
|
1788
|
+
console.log("");
|
|
1789
|
+
}
|
|
1790
|
+
} catch(e) {
|
|
1791
|
+
if (typeof thinkTimer !== "undefined") clearInterval(thinkTimer);
|
|
1792
|
+
printError(e.message);
|
|
1793
|
+
}
|
|
198
1794
|
}
|
|
199
1795
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
1796
|
+
// ── Screenshot/Render (CLI wrapper) ──
|
|
1797
|
+
async function cmdScreenshot(args) {
|
|
1798
|
+
if (!args) { printError("Usage: /screenshot <url>"); return; }
|
|
1799
|
+
try {
|
|
1800
|
+
printInfo("Taking screenshot of " + args + "...");
|
|
1801
|
+
var resp = await apiCall("POST", "/screenshot", { url: args });
|
|
1802
|
+
if (resp.status !== 200) { printError(resp.data.error || "Error"); return; }
|
|
1803
|
+
printSuccess("Screenshot: " + resp.data.title);
|
|
1804
|
+
console.log(" Download: " + C.brightCyan + config.api.base_url + resp.data.screenshot + C.reset);
|
|
1805
|
+
console.log("");
|
|
1806
|
+
} catch(e) { printError(e.message); }
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
async function cmdRender(args) {
|
|
1810
|
+
if (!args) { printError("Usage: /render <url>"); return; }
|
|
1811
|
+
try {
|
|
1812
|
+
printInfo("Rendering " + args + " with Playwright...");
|
|
1813
|
+
var resp = await apiCall("POST", "/render", { url: args });
|
|
1814
|
+
if (resp.status !== 200) { printError(resp.data.error || "Error"); return; }
|
|
1815
|
+
printSuccess("Rendered: " + resp.data.title + " (" + resp.data.html_length + " chars)");
|
|
1816
|
+
if (resp.data.screenshot) console.log(" Screenshot: " + C.brightCyan + resp.data.screenshot + C.reset);
|
|
1817
|
+
console.log("");
|
|
1818
|
+
} catch(e) { printError(e.message); }
|
|
1819
|
+
}
|
|
209
1820
|
|
|
210
|
-
|
|
211
|
-
|
|
1821
|
+
// ══════════════════════════════════════════════════
|
|
1822
|
+
// ── 1. SETTINGS REGISTRY (Multi-Scope) ──
|
|
1823
|
+
// ══════════════════════════════════════════════════
|
|
1824
|
+
// Scopes: Managed > CLI Args > Local > Project > User
|
|
1825
|
+
const SETTINGS_SCOPES = ["managed", "cli", "local", "project", "user"];
|
|
1826
|
+
const USER_SETTINGS_FILE = path.join(CONFIG_DIR, "settings.json");
|
|
1827
|
+
|
|
1828
|
+
function findProjectRoot() {
|
|
1829
|
+
var dir = config.workdir;
|
|
1830
|
+
for (var i = 0; i < 10; i++) {
|
|
1831
|
+
if (fs.existsSync(path.join(dir, ".blun"))) return dir;
|
|
1832
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
1833
|
+
var parent = path.dirname(dir);
|
|
1834
|
+
if (parent === dir) break;
|
|
1835
|
+
dir = parent;
|
|
212
1836
|
}
|
|
1837
|
+
return config.workdir;
|
|
1838
|
+
}
|
|
213
1839
|
|
|
214
|
-
|
|
215
|
-
|
|
1840
|
+
function loadScopedSettings(scope) {
|
|
1841
|
+
var file;
|
|
1842
|
+
if (scope === "user") file = USER_SETTINGS_FILE;
|
|
1843
|
+
else if (scope === "project") file = path.join(findProjectRoot(), ".blun", "settings.json");
|
|
1844
|
+
else if (scope === "local") file = path.join(findProjectRoot(), ".blun", "settings.local.json");
|
|
1845
|
+
else if (scope === "managed") file = path.join(CONFIG_DIR, "managed-settings.json");
|
|
1846
|
+
else return {};
|
|
1847
|
+
if (file && fs.existsSync(file)) {
|
|
1848
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch(e) {}
|
|
1849
|
+
}
|
|
1850
|
+
return {};
|
|
216
1851
|
}
|
|
217
1852
|
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1853
|
+
function saveScopedSettings(scope, data) {
|
|
1854
|
+
var file;
|
|
1855
|
+
if (scope === "user") file = USER_SETTINGS_FILE;
|
|
1856
|
+
else if (scope === "project") {
|
|
1857
|
+
var projDir = path.join(findProjectRoot(), ".blun");
|
|
1858
|
+
if (!fs.existsSync(projDir)) fs.mkdirSync(projDir, { recursive: true });
|
|
1859
|
+
file = path.join(projDir, "settings.json");
|
|
1860
|
+
} else if (scope === "local") {
|
|
1861
|
+
var projDir2 = path.join(findProjectRoot(), ".blun");
|
|
1862
|
+
if (!fs.existsSync(projDir2)) fs.mkdirSync(projDir2, { recursive: true });
|
|
1863
|
+
file = path.join(projDir2, "settings.local.json");
|
|
1864
|
+
} else if (scope === "managed") file = path.join(CONFIG_DIR, "managed-settings.json");
|
|
1865
|
+
else return;
|
|
1866
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Resolve setting: highest priority scope wins
|
|
1870
|
+
function getSetting(key) {
|
|
1871
|
+
for (var i = 0; i < SETTINGS_SCOPES.length; i++) {
|
|
1872
|
+
var s = loadScopedSettings(SETTINGS_SCOPES[i]);
|
|
1873
|
+
if (s[key] !== undefined) return { value: s[key], scope: SETTINGS_SCOPES[i] };
|
|
1874
|
+
}
|
|
1875
|
+
return { value: undefined, scope: null };
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// All settings keys with defaults
|
|
1879
|
+
var SETTINGS_DEFAULTS = {
|
|
1880
|
+
model: "blun-king-v100", language: "de", theme: "dark", outputStyle: "markdown",
|
|
1881
|
+
effortLevel: "normal", fastMode: false, alwaysThinkingEnabled: false,
|
|
1882
|
+
showThinkingSummaries: false, verbose: false, autoMode: false,
|
|
1883
|
+
respectGitignore: true, includeCoAuthoredBy: true, cleanupPeriodDays: 30,
|
|
1884
|
+
prefersReducedMotion: false, channelsEnabled: true,
|
|
1885
|
+
"sandbox.enabled": false, "sandbox.autoAllowBashIfSandboxed": true,
|
|
1886
|
+
"permissions.defaultMode": "ask",
|
|
1887
|
+
"attribution.commit": true, "attribution.pr": true
|
|
1888
|
+
};
|
|
1889
|
+
|
|
1890
|
+
function cmdConfig(args) {
|
|
1891
|
+
var parts = (args || "").split(/\s+/);
|
|
1892
|
+
var action = parts[0] || "list";
|
|
1893
|
+
var scope = "user";
|
|
1894
|
+
|
|
1895
|
+
// Check for --global flag
|
|
1896
|
+
var globalIdx = parts.indexOf("--global");
|
|
1897
|
+
if (globalIdx !== -1) { scope = "user"; parts.splice(globalIdx, 1); }
|
|
1898
|
+
var projectIdx = parts.indexOf("--project");
|
|
1899
|
+
if (projectIdx !== -1) { scope = "project"; parts.splice(projectIdx, 1); }
|
|
1900
|
+
var localIdx = parts.indexOf("--local");
|
|
1901
|
+
if (localIdx !== -1) { scope = "local"; parts.splice(localIdx, 1); }
|
|
1902
|
+
|
|
1903
|
+
action = parts[0] || "list";
|
|
1904
|
+
var key = parts[1];
|
|
1905
|
+
var value = parts.slice(2).join(" ");
|
|
1906
|
+
|
|
1907
|
+
if (action === "list") {
|
|
1908
|
+
console.log("");
|
|
1909
|
+
console.log(C.bold + " Settings Registry:" + C.reset);
|
|
1910
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(50) + C.reset);
|
|
1911
|
+
var allKeys = Object.keys(SETTINGS_DEFAULTS);
|
|
1912
|
+
allKeys.forEach(function(k) {
|
|
1913
|
+
var resolved = getSetting(k);
|
|
1914
|
+
var val = resolved.value !== undefined ? resolved.value : SETTINGS_DEFAULTS[k];
|
|
1915
|
+
var src = resolved.scope || "default";
|
|
1916
|
+
console.log(" " + C.gray + k + C.reset + " = " + C.brightCyan + JSON.stringify(val) + C.reset + C.dim + " (" + src + ")" + C.reset);
|
|
1917
|
+
});
|
|
1918
|
+
// Also show custom keys from all scopes
|
|
1919
|
+
SETTINGS_SCOPES.forEach(function(sc) {
|
|
1920
|
+
var s = loadScopedSettings(sc);
|
|
1921
|
+
Object.keys(s).forEach(function(k) {
|
|
1922
|
+
if (!SETTINGS_DEFAULTS.hasOwnProperty(k)) {
|
|
1923
|
+
console.log(" " + C.yellow + k + C.reset + " = " + C.brightCyan + JSON.stringify(s[k]) + C.reset + C.dim + " (" + sc + ")" + C.reset);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
});
|
|
1927
|
+
console.log("");
|
|
1928
|
+
|
|
1929
|
+
} else if (action === "get" && key) {
|
|
1930
|
+
var resolved = getSetting(key);
|
|
1931
|
+
if (resolved.value !== undefined) {
|
|
1932
|
+
console.log(C.brightCyan + JSON.stringify(resolved.value) + C.reset + C.dim + " (from: " + resolved.scope + ")" + C.reset);
|
|
1933
|
+
} else if (SETTINGS_DEFAULTS[key] !== undefined) {
|
|
1934
|
+
console.log(C.brightCyan + JSON.stringify(SETTINGS_DEFAULTS[key]) + C.reset + C.dim + " (default)" + C.reset);
|
|
1935
|
+
} else {
|
|
1936
|
+
printInfo("Not set: " + key);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
} else if (action === "set" && key) {
|
|
1940
|
+
var s = loadScopedSettings(scope);
|
|
1941
|
+
// Auto-parse value
|
|
1942
|
+
var parsed = value;
|
|
1943
|
+
if (value === "true") parsed = true;
|
|
1944
|
+
else if (value === "false") parsed = false;
|
|
1945
|
+
else if (/^\d+$/.test(value)) parsed = parseInt(value);
|
|
1946
|
+
s[key] = parsed;
|
|
1947
|
+
saveScopedSettings(scope, s);
|
|
1948
|
+
printSuccess(key + " = " + JSON.stringify(parsed) + " (scope: " + scope + ")");
|
|
1949
|
+
|
|
1950
|
+
} else if (action === "add" && key && value) {
|
|
1951
|
+
var s2 = loadScopedSettings(scope);
|
|
1952
|
+
if (!Array.isArray(s2[key])) s2[key] = [];
|
|
1953
|
+
s2[key].push(value);
|
|
1954
|
+
saveScopedSettings(scope, s2);
|
|
1955
|
+
printSuccess("Added '" + value + "' to " + key);
|
|
1956
|
+
|
|
1957
|
+
} else if (action === "remove" && key && value) {
|
|
1958
|
+
var s3 = loadScopedSettings(scope);
|
|
1959
|
+
if (Array.isArray(s3[key])) {
|
|
1960
|
+
s3[key] = s3[key].filter(function(v) { return v !== value; });
|
|
1961
|
+
saveScopedSettings(scope, s3);
|
|
1962
|
+
printSuccess("Removed '" + value + "' from " + key);
|
|
1963
|
+
} else {
|
|
1964
|
+
delete s3[key];
|
|
1965
|
+
saveScopedSettings(scope, s3);
|
|
1966
|
+
printSuccess("Removed " + key);
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
printError("Usage: /config list|get|set|add|remove <key> [value] [--global|--project|--local]");
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// ══════════════════════════════════════════════════
|
|
1974
|
+
// ── 2. SANDBOX ENGINE ──
|
|
1975
|
+
// ══════════════════════════════════════════════════
|
|
1976
|
+
function checkSandbox(action, target) {
|
|
1977
|
+
var sandbox = getSetting("sandbox.enabled").value;
|
|
1978
|
+
if (!sandbox) return { allowed: true };
|
|
229
1979
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
1980
|
+
var perms = loadPermissions();
|
|
1981
|
+
|
|
1982
|
+
if (action === "write") {
|
|
1983
|
+
var denyWrite = getSetting("sandbox.filesystem.denyWrite").value || [];
|
|
1984
|
+
var allowWrite = getSetting("sandbox.filesystem.allowWrite").value || [];
|
|
1985
|
+
for (var i = 0; i < denyWrite.length; i++) {
|
|
1986
|
+
if (target.includes(denyWrite[i])) return { allowed: false, reason: "denyWrite: " + denyWrite[i] };
|
|
1987
|
+
}
|
|
1988
|
+
if (allowWrite.length > 0) {
|
|
1989
|
+
var ok = false;
|
|
1990
|
+
for (var j = 0; j < allowWrite.length; j++) {
|
|
1991
|
+
if (target.startsWith(allowWrite[j])) { ok = true; break; }
|
|
1992
|
+
}
|
|
1993
|
+
if (!ok) return { allowed: false, reason: "Not in allowWrite list" };
|
|
1994
|
+
}
|
|
233
1995
|
}
|
|
234
1996
|
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
for (
|
|
238
|
-
|
|
239
|
-
const detail = [
|
|
240
|
-
summary.taskType || "",
|
|
241
|
-
typeof summary.fileCount === "number" && summary.fileCount ? `${summary.fileCount} file` : "",
|
|
242
|
-
typeof summary.referenceCount === "number" && summary.referenceCount ? `${summary.referenceCount} ref` : ""
|
|
243
|
-
].filter(Boolean).join(" | ");
|
|
244
|
-
lines.push(` - ${job.id} ${job.status} ${job.endpoint}${detail ? ` ${detail}` : ""}`);
|
|
1997
|
+
if (action === "read") {
|
|
1998
|
+
var denyRead = getSetting("sandbox.filesystem.denyRead").value || [];
|
|
1999
|
+
for (var k = 0; k < denyRead.length; k++) {
|
|
2000
|
+
if (target.includes(denyRead[k])) return { allowed: false, reason: "denyRead: " + denyRead[k] };
|
|
245
2001
|
}
|
|
246
2002
|
}
|
|
247
2003
|
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
2004
|
+
if (action === "network") {
|
|
2005
|
+
var allowedDomains = getSetting("sandbox.network.allowedDomains").value || [];
|
|
2006
|
+
if (allowedDomains.length > 0) {
|
|
2007
|
+
var domainOk = false;
|
|
2008
|
+
for (var l = 0; l < allowedDomains.length; l++) {
|
|
2009
|
+
if (target.includes(allowedDomains[l])) { domainOk = true; break; }
|
|
2010
|
+
}
|
|
2011
|
+
if (!domainOk) return { allowed: false, reason: "Domain not in allowedDomains" };
|
|
2012
|
+
}
|
|
251
2013
|
}
|
|
252
2014
|
|
|
253
|
-
|
|
254
|
-
return lines;
|
|
2015
|
+
return { allowed: true };
|
|
255
2016
|
}
|
|
256
2017
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
2018
|
+
// ══════════════════════════════════════════════════
|
|
2019
|
+
// ── 3. HOOKS ENGINE ──
|
|
2020
|
+
// ══════════════════════════════════════════════════
|
|
2021
|
+
const HOOKS_FILE = path.join(CONFIG_DIR, "hooks.json");
|
|
260
2022
|
|
|
261
|
-
|
|
262
|
-
|
|
2023
|
+
function loadHooks() {
|
|
2024
|
+
if (fs.existsSync(HOOKS_FILE)) {
|
|
2025
|
+
try { return JSON.parse(fs.readFileSync(HOOKS_FILE, "utf8")); } catch(e) {}
|
|
263
2026
|
}
|
|
2027
|
+
return { pre: {}, post: {} };
|
|
2028
|
+
}
|
|
264
2029
|
|
|
265
|
-
|
|
266
|
-
|
|
2030
|
+
function saveHooks(h) { fs.writeFileSync(HOOKS_FILE, JSON.stringify(h, null, 2)); }
|
|
2031
|
+
|
|
2032
|
+
function runHook(phase, command) {
|
|
2033
|
+
if (getSetting("disableAllHooks").value) return;
|
|
2034
|
+
var hooks = loadHooks();
|
|
2035
|
+
var list = (hooks[phase] || {})[command] || [];
|
|
2036
|
+
for (var i = 0; i < list.length; i++) {
|
|
2037
|
+
try {
|
|
2038
|
+
if (list[i].type === "command") {
|
|
2039
|
+
execSync(list[i].run, { cwd: config.workdir, encoding: "utf8", timeout: 10000, stdio: "pipe" });
|
|
2040
|
+
} else if (list[i].type === "http") {
|
|
2041
|
+
var allowed = getSetting("allowedHttpHookUrls").value || [];
|
|
2042
|
+
if (allowed.length > 0 && !allowed.some(function(u) { return list[i].url.startsWith(u); })) continue;
|
|
2043
|
+
execSync("curl -sX POST " + JSON.stringify(list[i].url) + " -d " + JSON.stringify(JSON.stringify({ event: phase + ":" + command })), { timeout: 5000, stdio: "pipe" });
|
|
2044
|
+
}
|
|
2045
|
+
} catch(e) { if (config.verbose) log("Hook error: " + e.message); }
|
|
267
2046
|
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function cmdHooks(args) {
|
|
2050
|
+
var hooks = loadHooks();
|
|
2051
|
+
var parts = (args || "").split(/\s+/);
|
|
2052
|
+
var action = parts[0] || "list";
|
|
2053
|
+
|
|
2054
|
+
if (action === "list") {
|
|
2055
|
+
console.log("");
|
|
2056
|
+
console.log(C.bold + " Hooks:" + C.reset);
|
|
2057
|
+
["pre", "post"].forEach(function(phase) {
|
|
2058
|
+
var cmds = Object.keys(hooks[phase] || {});
|
|
2059
|
+
if (cmds.length === 0) return;
|
|
2060
|
+
console.log(C.yellow + " " + phase + ":" + C.reset);
|
|
2061
|
+
cmds.forEach(function(cmd) {
|
|
2062
|
+
hooks[phase][cmd].forEach(function(h) {
|
|
2063
|
+
console.log(" " + cmd + " → " + C.brightCyan + (h.run || h.url) + C.reset + C.dim + " (" + h.type + ")" + C.reset);
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
if (Object.keys(hooks.pre).length === 0 && Object.keys(hooks.post).length === 0) {
|
|
2068
|
+
console.log(C.gray + " (none)" + C.reset);
|
|
2069
|
+
}
|
|
2070
|
+
console.log("");
|
|
268
2071
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
2072
|
+
} else if (action === "add" && parts[1] && parts[2] && parts[3]) {
|
|
2073
|
+
// /hooks add pre:chat "echo hello"
|
|
2074
|
+
var trigger = parts[1].split(":");
|
|
2075
|
+
var phase = trigger[0]; // pre or post
|
|
2076
|
+
var cmd = trigger[1];
|
|
2077
|
+
var run = parts.slice(2).join(" ");
|
|
2078
|
+
if (!hooks[phase]) hooks[phase] = {};
|
|
2079
|
+
if (!hooks[phase][cmd]) hooks[phase][cmd] = [];
|
|
2080
|
+
hooks[phase][cmd].push({ type: "command", run: run });
|
|
2081
|
+
saveHooks(hooks);
|
|
2082
|
+
printSuccess("Hook added: " + phase + ":" + cmd + " → " + run);
|
|
2083
|
+
|
|
2084
|
+
} else if (action === "remove" && parts[1]) {
|
|
2085
|
+
var trigger2 = parts[1].split(":");
|
|
2086
|
+
if (hooks[trigger2[0]] && hooks[trigger2[0]][trigger2[1]]) {
|
|
2087
|
+
delete hooks[trigger2[0]][trigger2[1]];
|
|
2088
|
+
saveHooks(hooks);
|
|
2089
|
+
printSuccess("Hook removed: " + parts[1]);
|
|
2090
|
+
} else {
|
|
2091
|
+
printError("Hook not found: " + parts[1]);
|
|
273
2092
|
}
|
|
2093
|
+
} else {
|
|
2094
|
+
printError("Usage: /hooks list|add|remove — /hooks add pre:chat \"echo hello\"");
|
|
274
2095
|
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// ══════════════════════════════════════════════════
|
|
2099
|
+
// ── 4. SUBAGENTS ──
|
|
2100
|
+
// ══════════════════════════════════════════════════
|
|
2101
|
+
const AGENTS_DIR_USER = path.join(CONFIG_DIR, "agents");
|
|
2102
|
+
if (!fs.existsSync(AGENTS_DIR_USER)) fs.mkdirSync(AGENTS_DIR_USER, { recursive: true });
|
|
2103
|
+
|
|
2104
|
+
function loadSubagents() {
|
|
2105
|
+
var agents = [];
|
|
2106
|
+
// User-level agents
|
|
2107
|
+
[AGENTS_DIR_USER, path.join(findProjectRoot(), ".blun", "agents")].forEach(function(dir) {
|
|
2108
|
+
if (!fs.existsSync(dir)) return;
|
|
2109
|
+
fs.readdirSync(dir).forEach(function(f) {
|
|
2110
|
+
if (!f.endsWith(".md")) return;
|
|
2111
|
+
var content = fs.readFileSync(path.join(dir, f), "utf8");
|
|
2112
|
+
var meta = {};
|
|
2113
|
+
// Parse YAML frontmatter
|
|
2114
|
+
var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2115
|
+
if (fmMatch) {
|
|
2116
|
+
fmMatch[1].split("\n").forEach(function(line) {
|
|
2117
|
+
var kv = line.match(/^(\w+):\s*(.+)/);
|
|
2118
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
agents.push({
|
|
2122
|
+
name: meta.name || f.replace(".md", ""),
|
|
2123
|
+
description: meta.description || "",
|
|
2124
|
+
role: meta.role || "general",
|
|
2125
|
+
tools: (meta.tools || "").split(",").map(function(t) { return t.trim(); }).filter(Boolean),
|
|
2126
|
+
file: path.join(dir, f),
|
|
2127
|
+
prompt: content.replace(/^---[\s\S]*?---\n*/, "").trim(),
|
|
2128
|
+
scope: dir.includes(CONFIG_DIR) ? "user" : "project"
|
|
2129
|
+
});
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
return agents;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function cmdAgents(args) {
|
|
2136
|
+
var parts = (args || "").split(/\s+/);
|
|
2137
|
+
var action = parts[0] || "list";
|
|
2138
|
+
|
|
2139
|
+
if (action === "list") {
|
|
2140
|
+
var agents = loadSubagents();
|
|
2141
|
+
console.log("");
|
|
2142
|
+
console.log(C.bold + " " + BOX.bot + " Subagents:" + C.reset);
|
|
2143
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
2144
|
+
if (agents.length === 0) {
|
|
2145
|
+
console.log(C.gray + " (none) — Create .md files in ~/.blun/agents/ or .blun/agents/" + C.reset);
|
|
2146
|
+
}
|
|
2147
|
+
agents.forEach(function(a) {
|
|
2148
|
+
console.log(" " + C.brightCyan + a.name + C.reset + " — " + a.description + C.dim + " (" + a.scope + ")" + C.reset);
|
|
2149
|
+
});
|
|
2150
|
+
console.log("");
|
|
2151
|
+
|
|
2152
|
+
} else if (action === "create" && parts[1]) {
|
|
2153
|
+
var name = parts[1];
|
|
2154
|
+
var desc = parts.slice(2).join(" ") || "Custom agent";
|
|
2155
|
+
var agentFile = path.join(AGENTS_DIR_USER, name + ".md");
|
|
2156
|
+
var content = "---\nname: " + name + "\ndescription: " + desc + "\nrole: general\ntools: chat,read,write\n---\n\nDu bist " + name + ", ein spezialisierter Agent bei BLUN King.\n\nDeine Aufgabe: " + desc + "\n";
|
|
2157
|
+
fs.writeFileSync(agentFile, content);
|
|
2158
|
+
printSuccess("Agent '" + name + "' created at " + agentFile);
|
|
2159
|
+
|
|
2160
|
+
} else if (action === "run" && parts[1]) {
|
|
2161
|
+
var agents2 = loadSubagents();
|
|
2162
|
+
var agent = agents2.find(function(a) { return a.name === parts[1]; });
|
|
2163
|
+
if (!agent) { printError("Agent not found: " + parts[1]); return; }
|
|
2164
|
+
var task = parts.slice(2).join(" ");
|
|
2165
|
+
if (!task) { printError("Usage: /agents run <name> <task>"); return; }
|
|
2166
|
+
// Run agent via API
|
|
2167
|
+
printInfo("Running agent '" + agent.name + "'...");
|
|
2168
|
+
apiCall("POST", "/agent", { goal: agent.prompt + "\n\nAKTUELLE AUFGABE: " + task }).then(function(resp) {
|
|
2169
|
+
if (resp.status === 200) {
|
|
2170
|
+
printAnswer(resp.data.answer, agent.name + " " + BOX.dot + " " + resp.data.steps_executed + " steps");
|
|
2171
|
+
} else {
|
|
2172
|
+
printError(resp.data.error || "Error");
|
|
2173
|
+
}
|
|
2174
|
+
}).catch(function(e) { printError(e.message); });
|
|
275
2175
|
|
|
276
|
-
if (
|
|
277
|
-
|
|
2176
|
+
} else if (action === "info" && parts[1]) {
|
|
2177
|
+
var agents3 = loadSubagents();
|
|
2178
|
+
var a = agents3.find(function(a) { return a.name === parts[1]; });
|
|
2179
|
+
if (!a) { printError("Agent not found: " + parts[1]); return; }
|
|
2180
|
+
console.log("");
|
|
2181
|
+
console.log(C.bold + " Agent: " + a.name + C.reset);
|
|
2182
|
+
console.log(" Description: " + a.description);
|
|
2183
|
+
console.log(" Role: " + a.role);
|
|
2184
|
+
console.log(" Tools: " + a.tools.join(", "));
|
|
2185
|
+
console.log(" Scope: " + a.scope);
|
|
2186
|
+
console.log(" File: " + a.file);
|
|
2187
|
+
console.log(C.dim + " " + BOX.h.repeat(30) + C.reset);
|
|
2188
|
+
console.log(C.gray + a.prompt.slice(0, 300) + C.reset);
|
|
2189
|
+
console.log("");
|
|
2190
|
+
|
|
2191
|
+
} else {
|
|
2192
|
+
printError("Usage: /agents list|create|run|info <name>");
|
|
278
2193
|
}
|
|
2194
|
+
}
|
|
279
2195
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
2196
|
+
// ══════════════════════════════════════════════════
|
|
2197
|
+
// ── 5. SESSION HISTORY (per workdir) ──
|
|
2198
|
+
// ══════════════════════════════════════════════════
|
|
2199
|
+
const SESSIONS_DIR = path.join(CONFIG_DIR, "sessions");
|
|
2200
|
+
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
2201
|
+
|
|
2202
|
+
function getSessionFile() {
|
|
2203
|
+
var hash = require("crypto").createHash("md5").update(config.workdir).digest("hex").slice(0, 8);
|
|
2204
|
+
return path.join(SESSIONS_DIR, hash + ".json");
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function loadSessionHistory() {
|
|
2208
|
+
var f = getSessionFile();
|
|
2209
|
+
if (fs.existsSync(f)) {
|
|
2210
|
+
try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch(e) {}
|
|
283
2211
|
}
|
|
2212
|
+
return { workdir: config.workdir, history: [], inputHistory: [] };
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
function saveSessionHistory(session) {
|
|
2216
|
+
fs.writeFileSync(getSessionFile(), JSON.stringify(session, null, 2));
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ══════════════════════════════════════════════════
|
|
2220
|
+
// ── 6. COST TRACKING ──
|
|
2221
|
+
// ══════════════════════════════════════════════════
|
|
2222
|
+
var sessionCost = { requests: 0, inputTokensEst: 0, outputTokensEst: 0 };
|
|
284
2223
|
|
|
285
|
-
|
|
286
|
-
|
|
2224
|
+
function cmdCost() {
|
|
2225
|
+
console.log("");
|
|
2226
|
+
console.log(C.bold + " Session Cost Estimate:" + C.reset);
|
|
2227
|
+
console.log(" Requests: " + C.brightCyan + sessionCost.requests + C.reset);
|
|
2228
|
+
console.log(" Est. Input Tokens: " + C.brightCyan + sessionCost.inputTokensEst + C.reset);
|
|
2229
|
+
console.log(" Est. Output Tokens: " + C.brightCyan + sessionCost.outputTokensEst + C.reset);
|
|
2230
|
+
console.log("");
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ══════════════════════════════════════════════════
|
|
2234
|
+
// ── 7. /doctor — Diagnostics ──
|
|
2235
|
+
// ══════════════════════════════════════════════════
|
|
2236
|
+
async function cmdDoctor() {
|
|
2237
|
+
console.log("");
|
|
2238
|
+
console.log(C.bold + " " + BOX.bot + " BLUN Doctor — System Check" + C.reset);
|
|
2239
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
2240
|
+
|
|
2241
|
+
// Config
|
|
2242
|
+
var hasKey = config.auth.api_key && config.auth.api_key.length > 10;
|
|
2243
|
+
console.log(" Config: " + (fs.existsSync(CONFIG_FILE) ? C.green + "OK" : C.red + "MISSING") + C.reset);
|
|
2244
|
+
console.log(" API Key: " + (hasKey ? C.green + "OK" : C.red + "NOT SET") + C.reset);
|
|
2245
|
+
|
|
2246
|
+
// API
|
|
2247
|
+
try {
|
|
2248
|
+
var h = await apiCall("GET", "/health");
|
|
2249
|
+
console.log(" API: " + (h.status === 200 ? C.green + "OK (" + h.data.model + ")" : C.red + "ERROR " + h.status) + C.reset);
|
|
2250
|
+
} catch(e) {
|
|
2251
|
+
console.log(" API: " + C.red + "UNREACHABLE" + C.reset);
|
|
287
2252
|
}
|
|
288
2253
|
|
|
289
|
-
|
|
290
|
-
|
|
2254
|
+
// Git
|
|
2255
|
+
try {
|
|
2256
|
+
execSync("git --version", { stdio: "pipe" });
|
|
2257
|
+
console.log(" Git: " + C.green + "OK" + C.reset);
|
|
2258
|
+
} catch(e) {
|
|
2259
|
+
console.log(" Git: " + C.red + "NOT FOUND" + C.reset);
|
|
291
2260
|
}
|
|
292
2261
|
|
|
293
|
-
|
|
294
|
-
|
|
2262
|
+
// SSH
|
|
2263
|
+
try {
|
|
2264
|
+
execSync("ssh -V 2>&1", { stdio: "pipe" });
|
|
2265
|
+
console.log(" SSH: " + C.green + "OK" + C.reset);
|
|
2266
|
+
} catch(e) {
|
|
2267
|
+
console.log(" SSH: " + C.red + "NOT FOUND" + C.reset);
|
|
295
2268
|
}
|
|
296
2269
|
|
|
297
|
-
|
|
298
|
-
|
|
2270
|
+
// Node
|
|
2271
|
+
console.log(" Node: " + C.green + process.version + C.reset);
|
|
2272
|
+
console.log(" Platform: " + C.green + process.platform + C.reset);
|
|
2273
|
+
|
|
2274
|
+
// Plugins
|
|
2275
|
+
var plugins = loadPlugins();
|
|
2276
|
+
console.log(" Plugins: " + C.brightCyan + Object.keys(plugins).length + " installed" + C.reset);
|
|
2277
|
+
|
|
2278
|
+
// Agents
|
|
2279
|
+
var agents = loadSubagents();
|
|
2280
|
+
console.log(" Agents: " + C.brightCyan + agents.length + " loaded" + C.reset);
|
|
2281
|
+
|
|
2282
|
+
// Hooks
|
|
2283
|
+
var hooks = loadHooks();
|
|
2284
|
+
var hookCount = Object.keys(hooks.pre || {}).length + Object.keys(hooks.post || {}).length;
|
|
2285
|
+
console.log(" Hooks: " + C.brightCyan + hookCount + " registered" + C.reset);
|
|
2286
|
+
|
|
2287
|
+
// Memory
|
|
2288
|
+
var memFiles = fs.existsSync(MEMORY_DIR) ? fs.readdirSync(MEMORY_DIR).length : 0;
|
|
2289
|
+
console.log(" Memory: " + C.brightCyan + memFiles + " entries" + C.reset);
|
|
2290
|
+
|
|
2291
|
+
// Disk
|
|
2292
|
+
console.log(" Workdir: " + C.brightCyan + config.workdir + C.reset);
|
|
2293
|
+
console.log("");
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// ══════════════════════════════════════════════════
|
|
2297
|
+
// ── 8. /model — Model switcher ──
|
|
2298
|
+
// ══════════════════════════════════════════════════
|
|
2299
|
+
function cmdModel(args) {
|
|
2300
|
+
if (!args) {
|
|
2301
|
+
console.log("");
|
|
2302
|
+
console.log(C.bold + " Current Model: " + C.brightCyan + config.model + C.reset);
|
|
2303
|
+
console.log(C.gray + " /model <name> — Switch model" + C.reset);
|
|
2304
|
+
console.log("");
|
|
2305
|
+
return;
|
|
299
2306
|
}
|
|
2307
|
+
config.model = args.trim();
|
|
2308
|
+
saveConfig(config);
|
|
2309
|
+
printSuccess("Model switched to: " + config.model);
|
|
2310
|
+
}
|
|
300
2311
|
|
|
301
|
-
|
|
302
|
-
|
|
2312
|
+
// ══════════════════════════════════════════════════
|
|
2313
|
+
// ── 9. /login /logout ──
|
|
2314
|
+
// ══════════════════════════════════════════════════
|
|
2315
|
+
function cmdLogin(args) {
|
|
2316
|
+
if (args && args.startsWith("key ")) {
|
|
2317
|
+
config.auth.api_key = args.slice(4).trim();
|
|
2318
|
+
config.auth.type = "api_key";
|
|
2319
|
+
saveConfig(config);
|
|
2320
|
+
printSuccess("Logged in with API key.");
|
|
2321
|
+
} else if (args === "oauth") {
|
|
2322
|
+
printInfo("OAuth login not yet implemented. Use API key for now:");
|
|
2323
|
+
printInfo(" /login key <your-api-key>");
|
|
2324
|
+
} else {
|
|
2325
|
+
console.log("");
|
|
2326
|
+
console.log(C.bold + " Login:" + C.reset);
|
|
2327
|
+
console.log(" /login key <api-key> — Login with API key");
|
|
2328
|
+
console.log(" /login oauth — Login with OAuth (coming soon)");
|
|
2329
|
+
console.log("");
|
|
2330
|
+
}
|
|
303
2331
|
}
|
|
304
2332
|
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
` status: ${job.status}`,
|
|
313
|
-
` session: ${job.sessionId}`
|
|
314
|
-
];
|
|
2333
|
+
function cmdLogout() {
|
|
2334
|
+
config.auth.api_key = "";
|
|
2335
|
+
config.auth.oauth_token = "";
|
|
2336
|
+
config.auth.oauth_expires = null;
|
|
2337
|
+
saveConfig(config);
|
|
2338
|
+
printSuccess("Logged out.");
|
|
2339
|
+
}
|
|
315
2340
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (job.result?.answer) lines.push(formatAnswerBlock(" ", job.result.answer));
|
|
324
|
-
lines.push("");
|
|
325
|
-
return lines;
|
|
2341
|
+
// ══════════════════════════════════════════════════
|
|
2342
|
+
// ── 10. /compact — Clear context ──
|
|
2343
|
+
// ══════════════════════════════════════════════════
|
|
2344
|
+
function cmdCompact() {
|
|
2345
|
+
var before = chatHistory.length;
|
|
2346
|
+
chatHistory = chatHistory.slice(-4);
|
|
2347
|
+
printSuccess("Context compacted: " + before + " → " + chatHistory.length + " messages");
|
|
326
2348
|
}
|
|
327
2349
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
2350
|
+
// ══════════════════════════════════════════════════
|
|
2351
|
+
// ── 11. /review — Review current diff ──
|
|
2352
|
+
// ══════════════════════════════════════════════════
|
|
2353
|
+
async function cmdReview() {
|
|
2354
|
+
try {
|
|
2355
|
+
var diff = execSync("git diff", { cwd: config.workdir, encoding: "utf8", timeout: 10000 });
|
|
2356
|
+
if (!diff.trim()) { printInfo("No changes to review."); return; }
|
|
2357
|
+
printInfo("Sending diff for review...");
|
|
2358
|
+
var resp = await apiCall("POST", "/chat", {
|
|
2359
|
+
message: "Review diesen Git-Diff. Finde Bugs, Sicherheitsprobleme, Verbesserungen:\n\n```diff\n" + diff.slice(0, 8000) + "\n```"
|
|
334
2360
|
});
|
|
2361
|
+
if (resp.status === 200) printAnswer(resp.data.answer, "code-review");
|
|
2362
|
+
else printError(resp.data.error || "Error");
|
|
2363
|
+
} catch(e) { printError(e.message); }
|
|
2364
|
+
}
|
|
335
2365
|
|
|
336
|
-
|
|
337
|
-
|
|
2366
|
+
// ══════════════════════════════════════════════════
|
|
2367
|
+
// ── AUTO MEMORY DREAM MODE ──
|
|
2368
|
+
// ══════════════════════════════════════════════════
|
|
2369
|
+
// After every 10 messages or on exit, BLUN analyzes the conversation
|
|
2370
|
+
// and saves key learnings to ~/.blun/memory/ automatically.
|
|
2371
|
+
var dreamCounter = 0;
|
|
2372
|
+
const DREAM_INTERVAL = 10; // messages between dreams
|
|
2373
|
+
const DREAM_DIR = path.join(MEMORY_DIR, "dreams");
|
|
2374
|
+
if (!fs.existsSync(DREAM_DIR)) fs.mkdirSync(DREAM_DIR, { recursive: true });
|
|
338
2375
|
|
|
339
|
-
|
|
340
|
-
|
|
2376
|
+
async function dreamMode() {
|
|
2377
|
+
if (chatHistory.length < 4) return; // not enough to dream about
|
|
2378
|
+
|
|
2379
|
+
try {
|
|
2380
|
+
// Ask BLUN to extract key learnings
|
|
2381
|
+
var dreamPrompt = [
|
|
2382
|
+
{ role: "system", content: "Du bist der BLUN King Memory Manager. Analysiere diese Konversation und extrahiere die wichtigsten Fakten, Entscheidungen und Erkenntnisse. Antworte NUR mit einem JSON-Objekt:\n{\"learnings\": [\"...\", \"...\"], \"user_preferences\": [\"...\"], \"project_facts\": [\"...\"], \"summary\": \"1 Satz Zusammenfassung\"}\nKein Markdown, NUR JSON." },
|
|
2383
|
+
{ role: "user", content: "Konversation:\n" + chatHistory.slice(-10).map(function(m) { return m.role + ": " + m.content.slice(0, 200); }).join("\n") }
|
|
2384
|
+
];
|
|
2385
|
+
|
|
2386
|
+
var resp = await apiCall("POST", "/chat", { message: dreamPrompt[1].content, history: [dreamPrompt[0]] });
|
|
2387
|
+
if (resp.status !== 200) return;
|
|
2388
|
+
|
|
2389
|
+
var answer = resp.data.answer;
|
|
2390
|
+
var dreamData;
|
|
2391
|
+
try {
|
|
2392
|
+
var jsonMatch = answer.match(/\{[\s\S]*\}/);
|
|
2393
|
+
if (jsonMatch) dreamData = JSON.parse(jsonMatch[0]);
|
|
2394
|
+
} catch(e) { return; }
|
|
2395
|
+
|
|
2396
|
+
if (!dreamData) return;
|
|
2397
|
+
|
|
2398
|
+
// Save dream
|
|
2399
|
+
var dreamFile = path.join(DREAM_DIR, new Date().toISOString().slice(0, 10) + ".json");
|
|
2400
|
+
var existingDreams = [];
|
|
2401
|
+
if (fs.existsSync(dreamFile)) {
|
|
2402
|
+
try { existingDreams = JSON.parse(fs.readFileSync(dreamFile, "utf8")); } catch(e) {}
|
|
2403
|
+
}
|
|
2404
|
+
existingDreams.push({
|
|
2405
|
+
timestamp: new Date().toISOString(),
|
|
2406
|
+
workdir: config.workdir,
|
|
2407
|
+
learnings: dreamData.learnings || [],
|
|
2408
|
+
user_preferences: dreamData.user_preferences || [],
|
|
2409
|
+
project_facts: dreamData.project_facts || [],
|
|
2410
|
+
summary: dreamData.summary || ""
|
|
341
2411
|
});
|
|
342
|
-
|
|
343
|
-
|
|
2412
|
+
fs.writeFileSync(dreamFile, JSON.stringify(existingDreams, null, 2));
|
|
2413
|
+
|
|
2414
|
+
// Also save learnings to flat memory for quick recall
|
|
2415
|
+
if (dreamData.learnings && dreamData.learnings.length > 0) {
|
|
2416
|
+
var memContent = dreamData.learnings.join("\n");
|
|
2417
|
+
var memFile = path.join(MEMORY_DIR, "auto_" + Date.now() + ".txt");
|
|
2418
|
+
fs.writeFileSync(memFile, memContent);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
if (config.verbose) printInfo("Dream Mode: " + (dreamData.learnings || []).length + " learnings saved.");
|
|
2422
|
+
} catch(e) {
|
|
2423
|
+
if (config.verbose) log("Dream error: " + e.message);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// Load dream context at startup
|
|
2428
|
+
function loadDreamContext() {
|
|
2429
|
+
var context = [];
|
|
2430
|
+
try {
|
|
2431
|
+
var files = fs.readdirSync(DREAM_DIR).sort().reverse().slice(0, 3); // last 3 days
|
|
2432
|
+
files.forEach(function(f) {
|
|
2433
|
+
var dreams = JSON.parse(fs.readFileSync(path.join(DREAM_DIR, f), "utf8"));
|
|
2434
|
+
dreams.forEach(function(d) {
|
|
2435
|
+
if (d.learnings) context = context.concat(d.learnings);
|
|
2436
|
+
if (d.project_facts) context = context.concat(d.project_facts);
|
|
2437
|
+
});
|
|
344
2438
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
2439
|
+
} catch(e) {}
|
|
2440
|
+
return context.slice(0, 20); // max 20 facts
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// Auto-compact: summarize every DREAM_INTERVAL messages
|
|
2444
|
+
async function autoCompact() {
|
|
2445
|
+
if (chatHistory.length < DREAM_INTERVAL) return;
|
|
2446
|
+
|
|
2447
|
+
// Dream first
|
|
2448
|
+
await dreamMode();
|
|
2449
|
+
|
|
2450
|
+
// Then compact
|
|
2451
|
+
var oldLen = chatHistory.length;
|
|
2452
|
+
chatHistory = chatHistory.slice(-6);
|
|
2453
|
+
if (config.verbose) printInfo("Auto-compact: " + oldLen + " → " + chatHistory.length);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// ══════════════════════════════════════════════════
|
|
2457
|
+
// ── SLASH COMMAND MENU (Claude Code style) ──
|
|
2458
|
+
// ══════════════════════════════════════════════════
|
|
2459
|
+
var COMMAND_DESCRIPTIONS = [
|
|
2460
|
+
{ cmd: "/help", desc: "Show all commands and shortcuts" },
|
|
2461
|
+
{ cmd: "/clear", desc: "Clear chat history" },
|
|
2462
|
+
{ cmd: "/compact", desc: "Reduce context window, keep recent messages" },
|
|
2463
|
+
{ cmd: "/config", desc: "Manage settings (list, get, set, add, remove)" },
|
|
2464
|
+
{ cmd: "/model", desc: "Show or switch the active AI model" },
|
|
2465
|
+
{ cmd: "/permissions", desc: "View and manage tool permissions" },
|
|
2466
|
+
{ cmd: "/doctor", desc: "Run full system diagnostics (bundled)" },
|
|
2467
|
+
{ cmd: "/status", desc: "Show runtime status from API" },
|
|
2468
|
+
{ cmd: "/health", desc: "Quick API health check" },
|
|
2469
|
+
{ cmd: "/cost", desc: "Show estimated session cost" },
|
|
2470
|
+
{ cmd: "/review", desc: "Review current git diff with AI" },
|
|
2471
|
+
{ cmd: "/skills", desc: "List all available AI skills/roles" },
|
|
2472
|
+
{ cmd: "/search", desc: "Search the web via DuckDuckGo" },
|
|
2473
|
+
{ cmd: "/learn", desc: "Teach BLUN King new knowledge" },
|
|
2474
|
+
{ cmd: "/generate", desc: "Generate a file from description" },
|
|
2475
|
+
{ cmd: "/analyze", desc: "Analyze a website or HTML" },
|
|
2476
|
+
{ cmd: "/agent", desc: "Run autonomous multi-step agent loop" },
|
|
2477
|
+
{ cmd: "/agents", desc: "Manage subagents (list, create, run, info)" },
|
|
2478
|
+
{ cmd: "/screenshot", desc: "Take a Playwright screenshot of a URL" },
|
|
2479
|
+
{ cmd: "/render", desc: "Full JS-rendered page capture (Playwright)" },
|
|
2480
|
+
{ cmd: "/plugin", desc: "Manage MCP plugins (list, add, remove, run)" },
|
|
2481
|
+
{ cmd: "/mcp", desc: "Alias for /plugin" },
|
|
2482
|
+
{ cmd: "/hooks", desc: "Manage pre/post command hooks" },
|
|
2483
|
+
{ cmd: "/git", desc: "Git commands (status, log, diff, add, commit, push...)" },
|
|
2484
|
+
{ cmd: "/ssh", desc: "Manage SSH connections and run remote commands" },
|
|
2485
|
+
{ cmd: "/deploy", desc: "Deploy via git, ssh, rsync, or pm2" },
|
|
2486
|
+
{ cmd: "/sh", desc: "Run a shell command directly" },
|
|
2487
|
+
{ cmd: "/read", desc: "Read a file with line numbers" },
|
|
2488
|
+
{ cmd: "/write", desc: "Write or create a file" },
|
|
2489
|
+
{ cmd: "/init", desc: "Initialize project (AGENT.md, .gitignore, git)" },
|
|
2490
|
+
{ cmd: "/memory", desc: "View, save, or delete local memory entries" },
|
|
2491
|
+
{ cmd: "/files", desc: "List files in current workdir" },
|
|
2492
|
+
{ cmd: "/settings", desc: "Show current configuration" },
|
|
2493
|
+
{ cmd: "/versions", desc: "Show prompt registry versions" },
|
|
2494
|
+
{ cmd: "/login", desc: "Login with API key or OAuth" },
|
|
2495
|
+
{ cmd: "/logout", desc: "Clear stored credentials" },
|
|
2496
|
+
{ cmd: "/eval", desc: "Run the eval test suite" },
|
|
2497
|
+
{ cmd: "/watchdog", desc: "Show or toggle watchdog status" },
|
|
2498
|
+
{ cmd: "/exit", desc: "Exit the CLI" }
|
|
2499
|
+
];
|
|
2500
|
+
|
|
2501
|
+
function showSlashMenu(filter) {
|
|
2502
|
+
var filtered = COMMAND_DESCRIPTIONS;
|
|
2503
|
+
if (filter && filter.length > 1) {
|
|
2504
|
+
var f = filter.toLowerCase();
|
|
2505
|
+
filtered = COMMAND_DESCRIPTIONS.filter(function(c) {
|
|
2506
|
+
return c.cmd.includes(f) || c.desc.toLowerCase().includes(f);
|
|
349
2507
|
});
|
|
2508
|
+
}
|
|
2509
|
+
if (filtered.length === 0) return;
|
|
2510
|
+
|
|
2511
|
+
// Show max 8 entries
|
|
2512
|
+
var show = filtered.slice(0, 8);
|
|
2513
|
+
// Move cursor up and print menu
|
|
2514
|
+
var menuLines = [];
|
|
2515
|
+
show.forEach(function(c) {
|
|
2516
|
+
var cmdPad = (c.cmd + " ".repeat(20)).slice(0, 18);
|
|
2517
|
+
menuLines.push(" " + C.green + cmdPad + C.white + c.desc.slice(0, 55) + C.reset);
|
|
350
2518
|
});
|
|
2519
|
+
if (filtered.length > 8) {
|
|
2520
|
+
menuLines.push(C.dim + " ... " + (filtered.length - 8) + " more" + C.reset);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// Print below prompt
|
|
2524
|
+
console.log("");
|
|
2525
|
+
menuLines.forEach(function(l) { console.log(l); });
|
|
351
2526
|
}
|
|
352
2527
|
|
|
2528
|
+
// ── Main Loop ──
|
|
353
2529
|
async function main() {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
2530
|
+
// Handle CLI args
|
|
2531
|
+
var cliArgs = process.argv.slice(2);
|
|
2532
|
+
if (cliArgs.length > 0) {
|
|
2533
|
+
// Non-interactive mode
|
|
2534
|
+
if (cliArgs[0] === "setup") {
|
|
2535
|
+
printInfo("BLUN King CLI Setup");
|
|
2536
|
+
var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2537
|
+
rl.question("API Base URL [" + config.api.base_url + "]: ", function(url) {
|
|
2538
|
+
if (url.trim()) config.api.base_url = url.trim();
|
|
2539
|
+
rl.question("API Key: ", function(key) {
|
|
2540
|
+
if (key.trim()) { config.auth.api_key = key.trim(); config.auth.type = "api_key"; }
|
|
2541
|
+
saveConfig(config);
|
|
2542
|
+
printSuccess("Config saved to " + CONFIG_FILE);
|
|
2543
|
+
rl.close();
|
|
2544
|
+
});
|
|
2545
|
+
});
|
|
2546
|
+
return;
|
|
2547
|
+
} else if (cliArgs[0] === "chat") {
|
|
2548
|
+
// One-shot chat
|
|
2549
|
+
var msg = cliArgs.slice(1).join(" ");
|
|
2550
|
+
if (msg) {
|
|
2551
|
+
var resp = await apiCall("POST", "/chat", { message: msg });
|
|
2552
|
+
if (resp.status === 200) console.log(resp.data.answer);
|
|
2553
|
+
else console.error("Error: " + (resp.data.error || resp.status));
|
|
2554
|
+
}
|
|
2555
|
+
return;
|
|
2556
|
+
} else if (cliArgs[0] === "health") {
|
|
2557
|
+
await cmdHealth();
|
|
2558
|
+
return;
|
|
2559
|
+
} else if (cliArgs[0] === "search") {
|
|
2560
|
+
await cmdSearch(cliArgs.slice(1).join(" "));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
372
2564
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
readline.clearLine(process.stdout, 0);
|
|
387
|
-
readline.cursorTo(process.stdout, 0);
|
|
388
|
-
}
|
|
389
|
-
process.stdout.write(`${normalizedLines.filter(Boolean).join("\n")}\n`);
|
|
390
|
-
if (!cliClosed && !rl.closed) {
|
|
391
|
-
renderPrompt();
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function showHelp() {
|
|
396
|
-
writeLines([
|
|
397
|
-
"",
|
|
398
|
-
" Commands:",
|
|
399
|
-
" /chat use chat endpoint",
|
|
400
|
-
" /agent use agent endpoint",
|
|
401
|
-
" /status show runtime status",
|
|
402
|
-
" /health show health",
|
|
403
|
-
" /artifacts list artifacts",
|
|
404
|
-
" /identities list available identities",
|
|
405
|
-
" /identity <name|auto> set a fixed identity or auto",
|
|
406
|
-
" /mode show current mode, identity and queue state",
|
|
407
|
-
" /queue show running job and pending queue",
|
|
408
|
-
" /cancel-pending drop all pending jobs but keep the running one",
|
|
409
|
-
" /bg <message> submit a background job to the API",
|
|
410
|
-
" /jobs list background jobs for this session",
|
|
411
|
-
" /watch <jobId> poll a background job until it finishes",
|
|
412
|
-
" /resume <jobId> restart an old background job and queue it again",
|
|
413
|
-
" /board show a compact taskboard",
|
|
414
|
-
" /learn [cat:] <text> learn aliases/preferences/workflows/guardrails",
|
|
415
|
-
" /btw <message> send an immediate side message without waiting for queue",
|
|
416
|
-
" /install <source> install plugin/skill/package locally",
|
|
417
|
-
" /browser-open <url> open a page in the local browser session",
|
|
418
|
-
" /browser-click <sel> click a selector in the local browser session",
|
|
419
|
-
" /browser-type <sel> <text> type into a selector",
|
|
420
|
-
" /browser-press <key> press a key in the local browser session",
|
|
421
|
-
" /browser-screenshot [path] save a screenshot from the local browser session",
|
|
422
|
-
" /browser-status show current browser snapshot",
|
|
423
|
-
" /browser-close close the local browser session",
|
|
424
|
-
" /local <command> run a local shell command",
|
|
425
|
-
" /ssh <user@host cmd> run a local SSH deployment command",
|
|
426
|
-
" /exit quit",
|
|
427
|
-
""
|
|
428
|
-
]);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
async function processQueue() {
|
|
432
|
-
if (processing) return;
|
|
433
|
-
processing = true;
|
|
2565
|
+
// Interactive mode
|
|
2566
|
+
// Workdir selection (like Claude Code — trust prompt)
|
|
2567
|
+
config.workdir = process.cwd();
|
|
2568
|
+
|
|
2569
|
+
// --dir flag support
|
|
2570
|
+
var dirIdx = process.argv.indexOf("--dir");
|
|
2571
|
+
if (dirIdx !== -1 && process.argv[dirIdx + 1]) {
|
|
2572
|
+
var customDir = process.argv[dirIdx + 1];
|
|
2573
|
+
if (fs.existsSync(customDir)) {
|
|
2574
|
+
config.workdir = path.resolve(customDir);
|
|
2575
|
+
process.chdir(config.workdir);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
434
2578
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
workdir,
|
|
448
|
-
identity: selectedIdentity
|
|
449
|
-
}, {
|
|
450
|
-
onMeta(payload) {
|
|
451
|
-
if (headerPrinted) return;
|
|
452
|
-
headerPrinted = true;
|
|
453
|
-
writeLines([
|
|
454
|
-
"",
|
|
455
|
-
` BLUN King ${job.label} ${payload.task_type}/${payload.role} ${payload.identity || "auto"}`,
|
|
456
|
-
` ${line()}`,
|
|
457
|
-
` routing: ${payload.routing?.task?.type || payload.task_type || "-"} confidence: ${typeof payload.routing?.confidence === "number" ? `${Math.round(payload.routing.confidence * 100)}%` : "-"}`
|
|
458
|
-
]);
|
|
459
|
-
},
|
|
460
|
-
onToken(payload) {
|
|
461
|
-
const text = String(payload.text || "");
|
|
462
|
-
if (!text) return;
|
|
463
|
-
if (process.stdout.isTTY) {
|
|
464
|
-
readline.clearLine(process.stdout, 0);
|
|
465
|
-
readline.cursorTo(process.stdout, 0);
|
|
466
|
-
}
|
|
467
|
-
process.stdout.write(` ${text}`);
|
|
468
|
-
if (!cliClosed && !rl.closed) renderPrompt();
|
|
469
|
-
},
|
|
470
|
-
onDone(payload) {
|
|
471
|
-
finalPayload = payload;
|
|
472
|
-
},
|
|
473
|
-
onError(payload) {
|
|
474
|
-
throw new Error(payload.error || "Stream failed");
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
process.stdout.write("\n");
|
|
478
|
-
if (finalPayload) {
|
|
479
|
-
writeLines([
|
|
480
|
-
` quality: ${finalPayload.quality?.score ?? "-"} flags: ${formatFlags(finalPayload.quality?.reasons || finalPayload.quality?.flags || [])}${finalPayload.quality?.retried ? " retried" : ""}`,
|
|
481
|
-
""
|
|
482
|
-
]);
|
|
2579
|
+
// Restore last workdir if still in home/default dir
|
|
2580
|
+
var lastWdFile = path.join(CONFIG_DIR, "last-workdir.json");
|
|
2581
|
+
if (!dirIdx || dirIdx === -1) {
|
|
2582
|
+
try {
|
|
2583
|
+
if (fs.existsSync(lastWdFile)) {
|
|
2584
|
+
var lastWd = JSON.parse(fs.readFileSync(lastWdFile, "utf8"));
|
|
2585
|
+
if (lastWd.dir && fs.existsSync(lastWd.dir) && config.workdir === process.cwd()) {
|
|
2586
|
+
// Only auto-restore if user didn't explicitly cd somewhere
|
|
2587
|
+
var homeDir = require("os").homedir();
|
|
2588
|
+
if (config.workdir === homeDir || config.workdir === homeDir.replace(/\\/g, "/")) {
|
|
2589
|
+
config.workdir = lastWd.dir;
|
|
2590
|
+
process.chdir(config.workdir);
|
|
483
2591
|
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
} catch(e) {}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
if (!process.argv.includes("--no-workdir-prompt") && !process.argv.includes("--trust")) {
|
|
2598
|
+
var trustedDirs = [];
|
|
2599
|
+
var trustFile = path.join(CONFIG_DIR, "trusted.json");
|
|
2600
|
+
if (fs.existsSync(trustFile)) {
|
|
2601
|
+
try { trustedDirs = JSON.parse(fs.readFileSync(trustFile, "utf8")); } catch(e) {}
|
|
2602
|
+
}
|
|
2603
|
+
var isTrusted = trustedDirs.includes(process.cwd());
|
|
2604
|
+
|
|
2605
|
+
if (!isTrusted) {
|
|
2606
|
+
console.log("");
|
|
2607
|
+
console.log(C.yellow + " " + BOX.bot + " Working in: " + C.brightWhite + process.cwd() + C.reset);
|
|
2608
|
+
console.log(C.gray + " Do you trust this folder? BLUN King will read/write files here." + C.reset);
|
|
2609
|
+
console.log("");
|
|
2610
|
+
|
|
2611
|
+
var trustOptions = [
|
|
2612
|
+
{ label: "Yes, trust this folder", action: "trust" },
|
|
2613
|
+
{ label: "Always trust this folder", action: "always" },
|
|
2614
|
+
{ label: "Choose another folder", action: "choose" }
|
|
2615
|
+
];
|
|
2616
|
+
var trustSel = 0;
|
|
2617
|
+
|
|
2618
|
+
// Arrow key menu for trust selection
|
|
2619
|
+
var trustChoice = await new Promise(function(resolve) {
|
|
2620
|
+
function renderTrustMenu() {
|
|
2621
|
+
// Move up and clear previous menu
|
|
2622
|
+
if (trustSel >= 0) process.stdout.write("\x1b[3A\r");
|
|
2623
|
+
trustOptions.forEach(function(opt, i) {
|
|
2624
|
+
var prefix = i === trustSel ? C.green + C.bold + " \u276F " : " ";
|
|
2625
|
+
var color = i === trustSel ? C.brightWhite + C.bold : C.gray;
|
|
2626
|
+
process.stdout.write("\x1b[2K" + prefix + color + (i + 1) + ". " + opt.label + C.reset + "\n");
|
|
491
2627
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
2628
|
+
}
|
|
2629
|
+
// Initial render
|
|
2630
|
+
console.log(""); console.log(""); console.log("");
|
|
2631
|
+
renderTrustMenu();
|
|
2632
|
+
|
|
2633
|
+
process.stdin.setRawMode(true);
|
|
2634
|
+
process.stdin.resume();
|
|
2635
|
+
process.stdin.setEncoding("utf8");
|
|
2636
|
+
function onKey(key) {
|
|
2637
|
+
if (key === "\x1b[A") { trustSel = Math.max(0, trustSel - 1); renderTrustMenu(); return; }
|
|
2638
|
+
if (key === "\x1b[B") { trustSel = Math.min(2, trustSel + 1); renderTrustMenu(); return; }
|
|
2639
|
+
if (key === "1") { trustSel = 0; process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve(trustOptions[0].action); return; }
|
|
2640
|
+
if (key === "2") { trustSel = 1; process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve(trustOptions[1].action); return; }
|
|
2641
|
+
if (key === "3") { trustSel = 2; process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve(trustOptions[2].action); return; }
|
|
2642
|
+
if (key === "\r" || key === "\n") {
|
|
2643
|
+
process.stdin.removeListener("data", onKey);
|
|
2644
|
+
process.stdin.setRawMode(false);
|
|
2645
|
+
resolve(trustOptions[trustSel].action);
|
|
2646
|
+
return;
|
|
495
2647
|
}
|
|
496
|
-
writeLines(formatJobResultLines(data, job, workdir, locallyWritten));
|
|
497
2648
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
2649
|
+
process.stdin.on("data", onKey);
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
if (trustChoice === "always") {
|
|
2653
|
+
trustedDirs.push(process.cwd());
|
|
2654
|
+
fs.writeFileSync(trustFile, JSON.stringify(trustedDirs, null, 2));
|
|
2655
|
+
printSuccess("Folder trusted permanently.");
|
|
2656
|
+
} else if (trustChoice === "choose") {
|
|
2657
|
+
// Open Windows folder picker if available
|
|
2658
|
+
var newDir = null;
|
|
2659
|
+
if (process.platform === "win32") {
|
|
2660
|
+
try {
|
|
2661
|
+
printInfo("Opening folder picker...");
|
|
2662
|
+
var psCmd = 'powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.FolderBrowserDialog; $f.Description = \'Choose BLUN King workspace\'; $f.ShowNewFolderButton = $true; if($f.ShowDialog() -eq \'OK\'){ Write-Output $f.SelectedPath } else { Write-Output \'CANCELLED\' }"';
|
|
2663
|
+
newDir = require("child_process").execSync(psCmd, { encoding: "utf8", timeout: 60000 }).trim();
|
|
2664
|
+
if (newDir === "CANCELLED") newDir = null;
|
|
2665
|
+
} catch(e) { newDir = null; }
|
|
2666
|
+
}
|
|
2667
|
+
// Fallback to text input
|
|
2668
|
+
if (!newDir) {
|
|
2669
|
+
var rlDir = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2670
|
+
newDir = await new Promise(function(resolve) {
|
|
2671
|
+
rlDir.question(C.brightBlue + " Enter path: " + C.reset, resolve);
|
|
2672
|
+
});
|
|
2673
|
+
rlDir.close();
|
|
2674
|
+
newDir = newDir ? newDir.trim() : null;
|
|
2675
|
+
}
|
|
2676
|
+
if (newDir && fs.existsSync(newDir)) {
|
|
2677
|
+
config.workdir = newDir;
|
|
2678
|
+
try { fs.writeFileSync(path.join(CONFIG_DIR, "last-workdir.json"), JSON.stringify({ dir: config.workdir, ts: new Date().toISOString() })); } catch(e) {}
|
|
2679
|
+
printSuccess("Workspace: " + config.workdir);
|
|
2680
|
+
} else if (newDir) {
|
|
2681
|
+
printError("Invalid path, using current directory.");
|
|
2682
|
+
}
|
|
502
2683
|
}
|
|
2684
|
+
// trust = trust for this session only
|
|
2685
|
+
console.log("");
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
printHeader();
|
|
2690
|
+
checkForUpdates();
|
|
2691
|
+
|
|
2692
|
+
// API is open — no key required
|
|
2693
|
+
if (false) {
|
|
2694
|
+
var apiKeyAnswer = "";
|
|
2695
|
+
if (apiKeyAnswer) {
|
|
2696
|
+
config.api.key = apiKeyAnswer;
|
|
2697
|
+
var cfgFile = path.join(CONFIG_DIR, "config.json");
|
|
2698
|
+
var cfgData = {};
|
|
2699
|
+
if (!cfgData.api) cfgData.api = {};
|
|
2700
|
+
cfgData.api.key = apiKeyAnswer;
|
|
2701
|
+
fs.writeFileSync(cfgFile, JSON.stringify(cfgData, null, 2));
|
|
2702
|
+
printSuccess("API key saved.");
|
|
503
2703
|
}
|
|
2704
|
+
}
|
|
504
2705
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
2706
|
+
// Load session history for this workdir
|
|
2707
|
+
var session = loadSessionHistory();
|
|
2708
|
+
chatHistory = session.history.slice(-20);
|
|
2709
|
+
|
|
2710
|
+
// Health check
|
|
2711
|
+
try {
|
|
2712
|
+
var h = await apiCall("GET", "/health");
|
|
2713
|
+
if (h.status === 200) printSuccess("Connected to BLUN King API");
|
|
2714
|
+
else printError("API returned " + h.status);
|
|
2715
|
+
} catch(e) {
|
|
2716
|
+
printError("Cannot reach API at " + config.api.base_url);
|
|
2717
|
+
printInfo("Run: blun setup — to configure");
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// Mode selection at startup
|
|
2721
|
+
var sessionMode = "chat"; // chat or agent
|
|
2722
|
+
if (!process.argv.includes("--agent") && !process.argv.includes("--chat")) {
|
|
2723
|
+
console.log("");
|
|
2724
|
+
console.log(C.brightWhite + C.bold + " How do you want to work?" + C.reset);
|
|
2725
|
+
console.log("");
|
|
2726
|
+
var modeOptions = [
|
|
2727
|
+
{ label: "Chat — talk back and forth", mode: "chat" },
|
|
2728
|
+
{ label: "Agent — give a goal, I do the rest autonomously", mode: "agent" }
|
|
2729
|
+
];
|
|
2730
|
+
var modeSel = 0;
|
|
2731
|
+
var modeChoice = await new Promise(function(resolve) {
|
|
2732
|
+
function renderModeMenu() {
|
|
2733
|
+
process.stdout.write("\x1b[2A\r");
|
|
2734
|
+
modeOptions.forEach(function(opt, i) {
|
|
2735
|
+
var prefix = i === modeSel ? C.green + C.bold + " \u276F " : " ";
|
|
2736
|
+
var color = i === modeSel ? C.brightWhite + C.bold : C.gray;
|
|
2737
|
+
process.stdout.write("\x1b[2K" + prefix + color + (i + 1) + ". " + opt.label + C.reset + "\n");
|
|
519
2738
|
});
|
|
520
2739
|
}
|
|
521
|
-
|
|
522
|
-
|
|
2740
|
+
console.log(""); console.log("");
|
|
2741
|
+
renderModeMenu();
|
|
523
2742
|
process.stdin.setRawMode(true);
|
|
524
2743
|
process.stdin.resume();
|
|
525
2744
|
process.stdin.setEncoding("utf8");
|
|
526
2745
|
function onKey(key) {
|
|
527
|
-
if (key === "\x1b[A") {
|
|
528
|
-
if (key === "\x1b[B") {
|
|
529
|
-
|
|
530
|
-
|
|
2746
|
+
if (key === "\x1b[A") { modeSel = 0; renderModeMenu(); return; }
|
|
2747
|
+
if (key === "\x1b[B") { modeSel = 1; renderModeMenu(); return; }
|
|
2748
|
+
if (key === "1") { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve("chat"); return; }
|
|
2749
|
+
if (key === "2") { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve("agent"); return; }
|
|
2750
|
+
if (key === "\r" || key === "\n") {
|
|
2751
|
+
process.stdin.removeListener("data", onKey);
|
|
2752
|
+
process.stdin.setRawMode(false);
|
|
2753
|
+
resolve(modeOptions[modeSel].mode);
|
|
2754
|
+
return;
|
|
531
2755
|
}
|
|
532
|
-
if (key === "\r" || key === "\n") { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve(options[sel]); return; }
|
|
533
|
-
if (key === "\x03") { process.exit(0); }
|
|
534
2756
|
}
|
|
535
2757
|
process.stdin.on("data", onKey);
|
|
536
2758
|
});
|
|
2759
|
+
sessionMode = modeChoice;
|
|
2760
|
+
printSuccess("Mode: " + (sessionMode === "agent" ? "Agent (autonomous)" : "Chat (interactive)"));
|
|
2761
|
+
console.log("");
|
|
2762
|
+
} else {
|
|
2763
|
+
sessionMode = process.argv.includes("--agent") ? "agent" : "chat";
|
|
537
2764
|
}
|
|
538
2765
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
2766
|
+
var ALL_COMMANDS = [
|
|
2767
|
+
"/help", "/clear", "/skills", "/skill", "/search", "/learn", "/learn-url",
|
|
2768
|
+
"/generate", "/analyze", "/score", "/eval", "/settings", "/set",
|
|
2769
|
+
"/status", "/versions", "/health", "/watchdog", "/memory", "/files",
|
|
2770
|
+
"/sh", "/git", "/ssh", "/deploy", "/read", "/write", "/init",
|
|
2771
|
+
"/screenshot", "/render", "/agent", "/plugin", "/mcp", "/permissions",
|
|
2772
|
+
"/config", "/hooks", "/agents", "/doctor", "/model", "/login", "/logout",
|
|
2773
|
+
"/compact", "/review", "/cost", "/exit"
|
|
2774
|
+
];
|
|
546
2775
|
|
|
547
|
-
// ──
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
2776
|
+
// ── Bordered Input Box + Live Slash Menu ──
|
|
2777
|
+
var inputBuffer = "";
|
|
2778
|
+
var inputHistory = [];
|
|
2779
|
+
var historyIdx = -1;
|
|
2780
|
+
var menuVisible = false;
|
|
2781
|
+
var menuItems = [];
|
|
2782
|
+
var menuSelected = 0;
|
|
2783
|
+
var cursorPos = 0;
|
|
2784
|
+
var processing = false;
|
|
2785
|
+
var uiStartRow = -1; // row where UI starts on screen
|
|
2786
|
+
|
|
2787
|
+
function getTermWidth() { return process.stdout.columns || 80; }
|
|
2788
|
+
|
|
2789
|
+
// Get current cursor row via sync trick
|
|
2790
|
+
function getCursorRow() {
|
|
2791
|
+
// Fallback: track manually
|
|
2792
|
+
return -1;
|
|
553
2793
|
}
|
|
554
|
-
console.log("");
|
|
555
2794
|
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
2795
|
+
// Erase UI using readline module (Windows-safe)
|
|
2796
|
+
function eraseUI() {
|
|
2797
|
+
if (uiStartRow < 0) return;
|
|
2798
|
+
// Move cursor to where UI started
|
|
2799
|
+
readline.cursorTo(process.stdout, 0, uiStartRow);
|
|
2800
|
+
// Clear everything from here down
|
|
2801
|
+
readline.clearScreenDown(process.stdout);
|
|
2802
|
+
uiStartRow = -1;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// Build UI lines as array, then write all at once
|
|
2806
|
+
function buildUILines() {
|
|
2807
|
+
var w = Math.min(getTermWidth() - 4, 76);
|
|
2808
|
+
var displayText = inputBuffer;
|
|
2809
|
+
if (displayText.length > w - 4) displayText = displayText.slice(-(w - 4));
|
|
2810
|
+
var inner = w - 2;
|
|
2811
|
+
var textPad = Math.max(0, inner - 2 - displayText.length);
|
|
2812
|
+
var lines = [];
|
|
2813
|
+
|
|
2814
|
+
// Box
|
|
2815
|
+
lines.push(C.brightBlue + " \u256D" + "\u2500".repeat(inner) + "\u256E" + C.reset);
|
|
2816
|
+
lines.push(C.brightBlue + " \u2502 " + C.reset + C.brightWhite + displayText + C.reset + " ".repeat(textPad) + " " + C.brightBlue + "\u2502" + C.reset);
|
|
2817
|
+
lines.push(C.brightBlue + " \u2570" + "\u2500".repeat(inner) + "\u256F" + C.reset);
|
|
2818
|
+
|
|
2819
|
+
// Status bar
|
|
2820
|
+
var permData = loadPermissions();
|
|
2821
|
+
var permMode = permData.mode || "ask";
|
|
2822
|
+
var isDangerous = process.argv.includes("--dangerously-skip-permissions") || permMode === "allow" || permMode === "allow-all";
|
|
2823
|
+
var permLabel = isDangerous ? "GOD MODE" : permMode === "deny" ? "LOCKED" : "ask permission";
|
|
2824
|
+
var permIcon = isDangerous ? "\u26A1" : permMode === "deny" ? "\u2718" : "\u2753";
|
|
2825
|
+
var permColor = isDangerous ? C.red + C.bold : permMode === "deny" ? C.red : C.yellow;
|
|
2826
|
+
var modelName = typeof config.model === "string" ? config.model : (config.model && config.model.name ? config.model.name : "default");
|
|
2827
|
+
var wdShort = config.workdir ? path.basename(config.workdir) : "~";
|
|
2828
|
+
var totalTok = sessionCost.inputTokensEst + sessionCost.outputTokensEst;
|
|
2829
|
+
var tokStr = totalTok > 1000 ? (totalTok / 1000).toFixed(1) + "k" : String(totalTok);
|
|
2830
|
+
lines.push(permColor + " " + permIcon + " " + permLabel + C.reset + C.dim + " \u2502 " + C.reset + C.cyan + modelName + C.reset + C.dim + " \u2502 " + C.reset + C.dim + wdShort + C.reset + C.dim + " \u2502 " + C.reset + C.yellow + tokStr + " tok" + C.reset);
|
|
2831
|
+
|
|
2832
|
+
// Menu
|
|
2833
|
+
if (menuVisible && menuItems.length > 0) {
|
|
2834
|
+
var show = menuItems.slice(0, 8);
|
|
2835
|
+
show.forEach(function(item, i) {
|
|
2836
|
+
var prefix = i === menuSelected ? C.green + C.bold + " \u276F " : " ";
|
|
2837
|
+
var cmdStr = (item.cmd + " ".repeat(20)).slice(0, 18);
|
|
2838
|
+
var descStr = item.desc.slice(0, getTermWidth() - 28);
|
|
2839
|
+
var cmdColor = i === menuSelected ? C.green + C.bold : C.green;
|
|
2840
|
+
var descColor = i === menuSelected ? C.brightWhite : C.white;
|
|
2841
|
+
lines.push(prefix + cmdColor + cmdStr + descColor + descStr + C.reset);
|
|
2842
|
+
});
|
|
2843
|
+
if (menuItems.length > 8) {
|
|
2844
|
+
lines.push(C.dim + " ... " + (menuItems.length - 8) + " more" + C.reset);
|
|
587
2845
|
}
|
|
588
2846
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
console.log(` \x1b[32m\u2713\x1b[0m Workspace: \x1b[97m${workdir}\x1b[0m (trusted)`);
|
|
592
|
-
console.log("");
|
|
2847
|
+
|
|
2848
|
+
return { lines: lines, cursorLine: 1, cursorCol: 4 + displayText.length };
|
|
593
2849
|
}
|
|
594
2850
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
2851
|
+
function drawUI() {
|
|
2852
|
+
process.stdout.write("\x1b[?25l"); // hide cursor
|
|
2853
|
+
// Remember where we start drawing
|
|
2854
|
+
uiStartRow = (process.stdout.rows || 24) - 1; // approximate
|
|
2855
|
+
// Use getCursorPosition trick: write lines, then position cursor
|
|
2856
|
+
var ui = buildUILines();
|
|
2857
|
+
|
|
2858
|
+
// Save absolute position before drawing
|
|
2859
|
+
// Write a marker to know our row
|
|
2860
|
+
process.stdout.write("\x1b[6n"); // request cursor position (async, but we don't wait)
|
|
2861
|
+
|
|
2862
|
+
// Actually just track rows manually
|
|
2863
|
+
uiStartRow = -1; // will be set below
|
|
2864
|
+
|
|
2865
|
+
// Clear any previous content and write fresh
|
|
2866
|
+
var output = ui.lines.join("\n") + "\n";
|
|
2867
|
+
process.stdout.write(output);
|
|
2868
|
+
|
|
2869
|
+
// Now cursor is at bottom. Calculate how many lines we wrote.
|
|
2870
|
+
var totalLines = ui.lines.length;
|
|
2871
|
+
|
|
2872
|
+
// Move cursor back to input position (line index 1 = content line)
|
|
2873
|
+
// We're at totalLines below start, need to go up (totalLines - 1 - cursorLine) from current minus 1
|
|
2874
|
+
var upMoves = totalLines - ui.cursorLine;
|
|
2875
|
+
process.stdout.write("\x1b[" + upMoves + "A");
|
|
2876
|
+
process.stdout.write("\r\x1b[" + ui.cursorCol + "C");
|
|
2877
|
+
|
|
2878
|
+
// Track for eraseUI: store how far up the start is from cursor
|
|
2879
|
+
uiStartRow = totalLines; // repurpose as "total lines drawn"
|
|
2880
|
+
_globalUILines = totalLines;
|
|
2881
|
+
|
|
2882
|
+
process.stdout.write("\x1b[?25h"); // show cursor
|
|
606
2883
|
}
|
|
607
|
-
if (process.argv.includes("--chat")) mode = "chat";
|
|
608
|
-
renderPrompt();
|
|
609
2884
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
2885
|
+
// Override eraseUI to use line count
|
|
2886
|
+
_globalEraseUI = eraseUI = function() {
|
|
2887
|
+
if (uiStartRow <= 0) return;
|
|
2888
|
+
process.stdout.write("\x1b[?25l");
|
|
2889
|
+
// Cursor is on content line (line 1). Move up 1 to reach top.
|
|
2890
|
+
process.stdout.write("\x1b[1A\r");
|
|
2891
|
+
// Clear all lines
|
|
2892
|
+
for (var i = 0; i < uiStartRow + 2; i++) {
|
|
2893
|
+
process.stdout.write("\x1b[2K\x1b[1B");
|
|
2894
|
+
}
|
|
2895
|
+
// Move back up
|
|
2896
|
+
process.stdout.write("\x1b[" + (uiStartRow + 2) + "A\r");
|
|
2897
|
+
process.stdout.write("\x1b[?25h");
|
|
2898
|
+
uiStartRow = -1;
|
|
2899
|
+
};
|
|
2900
|
+
|
|
2901
|
+
function refreshUI() {
|
|
2902
|
+
if (uiStartRow > 0) eraseUI();
|
|
2903
|
+
// Compute menu items
|
|
2904
|
+
if (inputBuffer.startsWith("/")) {
|
|
2905
|
+
var filter = inputBuffer.toLowerCase();
|
|
2906
|
+
menuItems = COMMAND_DESCRIPTIONS.filter(function(c) {
|
|
2907
|
+
return c.cmd.includes(filter) || c.desc.toLowerCase().includes(filter.slice(1));
|
|
2908
|
+
});
|
|
2909
|
+
menuVisible = menuItems.length > 0;
|
|
2910
|
+
if (menuVisible) menuSelected = Math.min(menuSelected, menuItems.length - 1);
|
|
2911
|
+
} else {
|
|
2912
|
+
menuVisible = false;
|
|
2913
|
+
menuItems = [];
|
|
2914
|
+
}
|
|
2915
|
+
drawUI();
|
|
2916
|
+
}
|
|
613
2917
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
2918
|
+
function drawPrompt() {
|
|
2919
|
+
_globalDrawPrompt = drawPrompt; // expose globally
|
|
2920
|
+
if (processing) return;
|
|
2921
|
+
console.log(""); // spacing
|
|
2922
|
+
uiStartRow = -1;
|
|
2923
|
+
refreshUI();
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Intent detection: natural language → slash command
|
|
2927
|
+
var INTENTS = [
|
|
2928
|
+
{ patterns: ["verbinde.*telegram", "connect.*telegram", "telegram.*verbind", "telegram.*setup", "telegram.*einricht"], cmd: "/plugin telegram" },
|
|
2929
|
+
{ patterns: ["verbinde.*github", "connect.*github", "github.*setup"], cmd: "/plugin github" },
|
|
2930
|
+
{ patterns: ["verbinde.*slack", "connect.*slack", "slack.*setup"], cmd: "/plugin slack" },
|
|
2931
|
+
{ patterns: ["verbinde.*docker", "connect.*docker", "docker.*setup"], cmd: "/plugin docker" },
|
|
2932
|
+
{ patterns: ["plugin.*install", "plugin.*hinzuf", "erweiterung"], cmd: "/plugin" },
|
|
2933
|
+
{ patterns: ["mcp.*server", "mcp.*install", "mcp.*einricht"], cmd: "/mcp" },
|
|
2934
|
+
{ patterns: ["welches model", "which model", "modell.*wechsel", "model.*switch", "anderes.*model"], cmd: "/model" },
|
|
2935
|
+
{ patterns: ["permission", "berechtigung", "zugriff", "erlaubnis"], cmd: "/permissions" },
|
|
2936
|
+
{ patterns: ["einstellung", "setting", "config", "konfigur"], cmd: "/config" },
|
|
2937
|
+
{ patterns: ["health.*check", "gesundheit", "status.*check", "alles.*ok", "laeuft.*alles"], cmd: "/health" },
|
|
2938
|
+
{ patterns: ["zeig.*skills", "was kannst", "show.*skills", "faehigkeit"], cmd: "/skills" },
|
|
2939
|
+
{ patterns: ["zeig.*agent", "list.*agent", "welche.*agent"], cmd: "/agents" },
|
|
2940
|
+
{ patterns: ["screenshot.*mach", "screenshot.*von", "take.*screenshot"], cmd: null, extract: function(t) { var m = t.match(/(?:screenshot|bildschirmfoto).*?(https?:\/\/\S+)/i); return m ? "/screenshot " + m[1] : "/screenshot"; } },
|
|
2941
|
+
{ patterns: ["hilfe", "help", "was geht", "befehle"], cmd: "/help" },
|
|
2942
|
+
{ patterns: ["doctor", "diagnose", "problem.*check"], cmd: "/doctor" },
|
|
2943
|
+
{ patterns: ["login", "anmeld", "einlogg"], cmd: "/login" },
|
|
2944
|
+
{ patterns: ["logout", "abmeld", "auslogg"], cmd: "/logout" },
|
|
2945
|
+
{ patterns: ["komprimier", "compact", "zusammenfass"], cmd: "/compact" },
|
|
2946
|
+
{ patterns: ["kosten", "verbrauch", "cost", "tokens.*used"], cmd: "/cost" },
|
|
2947
|
+
{ patterns: ["update.*check", "neue.*version", "aktualisier"], cmd: "/versions" },
|
|
2948
|
+
{ patterns: ["speicher", "memory.*show", "was weisst.*du", "erinnerung"], cmd: "/memory" },
|
|
2949
|
+
{ patterns: ["hook", "webhook"], cmd: "/hooks" },
|
|
2950
|
+
];
|
|
2951
|
+
|
|
2952
|
+
function detectIntent(text) {
|
|
2953
|
+
var lower = text.toLowerCase();
|
|
2954
|
+
// Skip if text is long (probably a real chat message)
|
|
2955
|
+
if (text.length > 120) return null;
|
|
2956
|
+
for (var i = 0; i < INTENTS.length; i++) {
|
|
2957
|
+
var intent = INTENTS[i];
|
|
2958
|
+
for (var j = 0; j < intent.patterns.length; j++) {
|
|
2959
|
+
if (new RegExp(intent.patterns[j], "i").test(lower)) {
|
|
2960
|
+
if (intent.extract) return intent.extract(text);
|
|
2961
|
+
return intent.cmd;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
var inputQueue = [];
|
|
2969
|
+
|
|
2970
|
+
async function processInput(input) {
|
|
2971
|
+
if (processing) {
|
|
2972
|
+
inputQueue.push(input);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
processing = true;
|
|
2976
|
+
eraseUI();
|
|
2977
|
+
|
|
2978
|
+
if (input.startsWith("!")) {
|
|
2979
|
+
cmdShell(input.slice(1).trim());
|
|
2980
|
+
} else if (input.startsWith("#save ")) {
|
|
2981
|
+
var memParts = input.slice(6).split(/\s+/);
|
|
2982
|
+
if (memParts[0] && memParts.length > 1) {
|
|
2983
|
+
fs.writeFileSync(path.join(MEMORY_DIR, memParts[0] + ".txt"), memParts.slice(1).join(" "));
|
|
2984
|
+
printSuccess("Memory saved: " + memParts[0]);
|
|
2985
|
+
}
|
|
2986
|
+
} else if (input.startsWith("#")) {
|
|
2987
|
+
var memKey = input.slice(1).trim();
|
|
2988
|
+
var memFile = path.join(MEMORY_DIR, memKey + ".txt");
|
|
2989
|
+
if (fs.existsSync(memFile)) console.log(C.brightCyan + fs.readFileSync(memFile, "utf8") + C.reset);
|
|
2990
|
+
else printError("Memory not found: " + memKey);
|
|
2991
|
+
} else if (input.startsWith("/")) {
|
|
2992
|
+
runHook("pre", input.split(/\s+/)[0].slice(1));
|
|
2993
|
+
await handleCommand(input);
|
|
2994
|
+
runHook("post", input.split(/\s+/)[0].slice(1));
|
|
2995
|
+
} else {
|
|
2996
|
+
// Intent detection — natural language → command
|
|
2997
|
+
var detected = detectIntent(input);
|
|
2998
|
+
if (detected) {
|
|
2999
|
+
printInfo("→ " + detected);
|
|
3000
|
+
runHook("pre", detected.split(/\s+/)[0].slice(1));
|
|
3001
|
+
await handleCommand(detected);
|
|
3002
|
+
runHook("post", detected.split(/\s+/)[0].slice(1));
|
|
3003
|
+
} else if (sessionMode === "agent") {
|
|
3004
|
+
// Agent mode: detect if question or task
|
|
3005
|
+
var trimmed = input.trim();
|
|
3006
|
+
var isChat = trimmed.length < 15 || // short messages = chat
|
|
3007
|
+
/^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschüss|ciao)/i.test(trimmed) ||
|
|
3008
|
+
/^(wo |was |wie |wer |wann |warum |kannst|hast|ist |sind |where|what|how|who|when|why|can you|do you|is |are )/i.test(trimmed) ||
|
|
3009
|
+
/\?$/.test(trimmed);
|
|
3010
|
+
if (isChat) {
|
|
3011
|
+
await sendChat(input);
|
|
3012
|
+
} else {
|
|
3013
|
+
await cmdAgent(input);
|
|
3014
|
+
}
|
|
3015
|
+
} else {
|
|
3016
|
+
await sendChat(input);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
inputBuffer = "";
|
|
3021
|
+
cursorPos = 0;
|
|
3022
|
+
menuSelected = 0;
|
|
3023
|
+
uiStartRow = -1;
|
|
3024
|
+
processing = false;
|
|
3025
|
+
drawPrompt();
|
|
3026
|
+
|
|
3027
|
+
// Process queued inputs
|
|
3028
|
+
if (inputQueue.length > 0) {
|
|
3029
|
+
var next = inputQueue.shift();
|
|
3030
|
+
inputHistory.unshift(next);
|
|
3031
|
+
processInput(next);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// ── Raw Input Handler ──
|
|
3036
|
+
process.stdin.setRawMode(true);
|
|
3037
|
+
process.stdin.resume();
|
|
3038
|
+
process.stdin.setEncoding("utf8");
|
|
3039
|
+
|
|
3040
|
+
drawPrompt();
|
|
3041
|
+
|
|
3042
|
+
process.stdin.on("data", function(key) {
|
|
3043
|
+
// Ctrl+C / Ctrl+D — always allow exit
|
|
3044
|
+
if (key === "\x03" || key === "\x04") {
|
|
3045
|
+
session.history = chatHistory.slice(-50);
|
|
3046
|
+
saveSessionHistory(session);
|
|
3047
|
+
// Save last workdir for next session
|
|
3048
|
+
try { fs.writeFileSync(path.join(CONFIG_DIR, "last-workdir.json"), JSON.stringify({ dir: config.workdir, ts: new Date().toISOString() })); } catch(e) {}
|
|
3049
|
+
eraseUI();
|
|
3050
|
+
console.log(C.dim + "\nBye." + C.reset);
|
|
3051
|
+
process.exit(0);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Enter
|
|
3055
|
+
if (key === "\r" || key === "\n") {
|
|
3056
|
+
var input = inputBuffer.trim();
|
|
3057
|
+
if (menuVisible && menuItems.length > 0 && inputBuffer.startsWith("/") && !inputBuffer.includes(" ")) {
|
|
3058
|
+
input = menuItems[menuSelected].cmd;
|
|
3059
|
+
}
|
|
3060
|
+
if (!input) return;
|
|
3061
|
+
inputHistory.unshift(input);
|
|
3062
|
+
historyIdx = -1;
|
|
3063
|
+
processInput(input);
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Ctrl+V — paste (check clipboard for image)
|
|
3068
|
+
if (key === "\x16") {
|
|
3069
|
+
(async function() {
|
|
3070
|
+
try {
|
|
3071
|
+
var imgPath = path.join(os.tmpdir(), "blun_paste_" + Date.now() + ".png");
|
|
3072
|
+
var isWin = process.platform === "win32";
|
|
3073
|
+
if (isWin) {
|
|
3074
|
+
var psCmd = "powershell -NoProfile -Command \"$img = Get-Clipboard -Format Image; if($img){ $img.Save('" + imgPath.replace(/\\/g, "\\\\") + "'); Write-Output 'OK' } else { Write-Output 'NO' }\"";
|
|
3075
|
+
var psResult = require("child_process").execSync(psCmd, { encoding: "utf8", timeout: 5000 }).trim();
|
|
3076
|
+
if (psResult === "OK" && fs.existsSync(imgPath)) {
|
|
3077
|
+
eraseUI();
|
|
3078
|
+
printInfo("Image pasted from clipboard");
|
|
3079
|
+
var imgData = fs.readFileSync(imgPath).toString("base64");
|
|
3080
|
+
var prompt = inputBuffer.trim() || "Describe this image";
|
|
3081
|
+
processing = true;
|
|
3082
|
+
var resp = await apiCall("POST", "/chat", {
|
|
3083
|
+
message: prompt,
|
|
3084
|
+
image: imgData,
|
|
3085
|
+
history: chatHistory.slice(-10)
|
|
3086
|
+
});
|
|
3087
|
+
if (resp.status === 200) {
|
|
3088
|
+
chatHistory.push({ role: "user", content: "[image] " + prompt });
|
|
3089
|
+
chatHistory.push({ role: "assistant", content: resp.data.answer });
|
|
3090
|
+
printAnswer(resp.data.answer, "vision");
|
|
3091
|
+
} else {
|
|
3092
|
+
printError(resp.data.error || "Vision error");
|
|
3093
|
+
}
|
|
3094
|
+
inputBuffer = "";
|
|
3095
|
+
cursorPos = 0;
|
|
3096
|
+
processing = false;
|
|
3097
|
+
drawPrompt();
|
|
3098
|
+
try { fs.unlinkSync(imgPath); } catch(e) {}
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
683
3101
|
}
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
3102
|
+
// No image — just paste text from clipboard
|
|
3103
|
+
if (isWin) {
|
|
3104
|
+
var clipText = require("child_process").execSync("powershell -NoProfile -Command Get-Clipboard", { encoding: "utf8", timeout: 3000 }).trim();
|
|
3105
|
+
if (clipText) {
|
|
3106
|
+
inputBuffer += clipText;
|
|
3107
|
+
cursorPos = inputBuffer.length;
|
|
3108
|
+
refreshUI();
|
|
3109
|
+
}
|
|
688
3110
|
}
|
|
689
|
-
|
|
3111
|
+
} catch(e) {
|
|
3112
|
+
// Fallback: ignore paste errors
|
|
690
3113
|
}
|
|
691
|
-
})()
|
|
692
|
-
return
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
if (message.startsWith("/learn ")) {
|
|
717
|
-
const payload = parseLearnCommand(message);
|
|
718
|
-
apiRequest(cfg, "POST", "/learn", {
|
|
719
|
-
session_id: sessionId,
|
|
720
|
-
user_id: sessionId,
|
|
721
|
-
title: `cli-${Date.now()}`,
|
|
722
|
-
category: payload.category,
|
|
723
|
-
content: payload.content
|
|
724
|
-
})
|
|
725
|
-
.then((data) => writeLines(formatLearnSummary(data)))
|
|
726
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
727
|
-
return renderPrompt();
|
|
728
|
-
}
|
|
729
|
-
if (message.startsWith("/btw ")) {
|
|
730
|
-
const btwMessage = message.slice(5).trim();
|
|
731
|
-
const btwId = nextBtwId++;
|
|
732
|
-
writeLines([` [btw #${btwId}] sent immediately`]);
|
|
733
|
-
apiRequest(cfg, "POST", mode === "agent" ? "/agent" : "/chat", {
|
|
734
|
-
message: btwMessage,
|
|
735
|
-
goal: btwMessage,
|
|
736
|
-
session_id: sessionId,
|
|
737
|
-
workdir,
|
|
738
|
-
identity: selectedIdentity
|
|
739
|
-
})
|
|
740
|
-
.then((data) => writeLines(formatJobResultLines(data, { id: `btw-${btwId}`, label: `BTW#${btwId}` }, workdir, saveReturnedFiles(workdir, data.files))))
|
|
741
|
-
.catch((error) => writeLines([` ERROR [BTW#${btwId}]: ${error.message}`]));
|
|
742
|
-
return renderPrompt();
|
|
743
|
-
}
|
|
744
|
-
if (message.startsWith("/install ")) {
|
|
745
|
-
installSource(message.slice(9).trim())
|
|
746
|
-
.then((result) => writeLines([` installed ${result.kind}: ${result.target}`, ` extensions: ${EXTENSIONS_DIR}`]))
|
|
747
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
748
|
-
return renderPrompt();
|
|
749
|
-
}
|
|
750
|
-
if (message.startsWith("/browser-open ")) {
|
|
751
|
-
apiRequest(cfg, "POST", "/browser/open", { url: message.slice(14).trim() })
|
|
752
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
753
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
754
|
-
return renderPrompt();
|
|
755
|
-
}
|
|
756
|
-
if (message.startsWith("/browser-click ")) {
|
|
757
|
-
apiRequest(cfg, "POST", "/browser/click", { selector: message.slice(15).trim() })
|
|
758
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
759
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
760
|
-
return renderPrompt();
|
|
761
|
-
}
|
|
762
|
-
if (message.startsWith("/browser-type ")) {
|
|
763
|
-
const rest = message.slice(14).trim();
|
|
764
|
-
const firstSpace = rest.indexOf(" ");
|
|
765
|
-
const selector = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
|
|
766
|
-
const text = firstSpace === -1 ? "" : rest.slice(firstSpace + 1);
|
|
767
|
-
apiRequest(cfg, "POST", "/browser/type", { selector, text })
|
|
768
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
769
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
770
|
-
return renderPrompt();
|
|
771
|
-
}
|
|
772
|
-
if (message.startsWith("/browser-press ")) {
|
|
773
|
-
apiRequest(cfg, "POST", "/browser/press", { key: message.slice(15).trim() })
|
|
774
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
775
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
776
|
-
return renderPrompt();
|
|
777
|
-
}
|
|
778
|
-
if (message.startsWith("/browser-screenshot")) {
|
|
779
|
-
const target = message.slice("/browser-screenshot".length).trim();
|
|
780
|
-
apiRequest(cfg, "POST", "/browser/screenshot", target ? { path: target } : {})
|
|
781
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
782
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
783
|
-
return renderPrompt();
|
|
784
|
-
}
|
|
785
|
-
if (message === "/browser-status") {
|
|
786
|
-
apiRequest(cfg, "GET", "/browser/status")
|
|
787
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
788
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
789
|
-
return renderPrompt();
|
|
790
|
-
}
|
|
791
|
-
if (message === "/browser-close") {
|
|
792
|
-
apiRequest(cfg, "POST", "/browser/close", {})
|
|
793
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
794
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
795
|
-
return renderPrompt();
|
|
796
|
-
}
|
|
797
|
-
if (message === "/status") {
|
|
798
|
-
apiRequest(cfg, "GET", "/runtime/status")
|
|
799
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
800
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
801
|
-
return renderPrompt();
|
|
802
|
-
}
|
|
803
|
-
if (message === "/health") {
|
|
804
|
-
fetch(`${cfg.apiUrl}/health`)
|
|
805
|
-
.then((resp) => resp.json())
|
|
806
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
807
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
808
|
-
return renderPrompt();
|
|
809
|
-
}
|
|
810
|
-
if (message === "/artifacts") {
|
|
811
|
-
apiRequest(cfg, "GET", "/artifacts")
|
|
812
|
-
.then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
|
|
813
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
814
|
-
return renderPrompt();
|
|
815
|
-
}
|
|
816
|
-
if (message.startsWith("/local ")) {
|
|
817
|
-
runLocalShell(message.slice(7), workdir)
|
|
818
|
-
.then((result) => writeLines(["", result.stdout || "(no output)", ""]))
|
|
819
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
820
|
-
return renderPrompt();
|
|
821
|
-
}
|
|
822
|
-
if (message.startsWith("/ssh ")) {
|
|
823
|
-
runLocalShell(`ssh ${message.slice(5)}`, workdir)
|
|
824
|
-
.then((result) => writeLines(["", result.stdout || "(no output)", ""]))
|
|
825
|
-
.catch((error) => writeLines([` ERROR: ${error.message}`]));
|
|
826
|
-
return renderPrompt();
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const endpoint = mode === "agent" ? "/agent" : "/chat";
|
|
830
|
-
const job = { id: nextJobId++, label: `#${nextJobId - 1}`, message, endpoint };
|
|
831
|
-
queue.push(job);
|
|
832
|
-
writeLines([` [pending #${job.id}] ${message} queue:${queue.length}`]);
|
|
833
|
-
processQueue();
|
|
834
|
-
});
|
|
835
|
-
}
|
|
3114
|
+
})();
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// Tab — autocomplete from menu
|
|
3119
|
+
if (key === "\t") {
|
|
3120
|
+
if (menuVisible && menuItems.length > 0) {
|
|
3121
|
+
inputBuffer = menuItems[menuSelected].cmd + " ";
|
|
3122
|
+
cursorPos = inputBuffer.length;
|
|
3123
|
+
refreshUI();
|
|
3124
|
+
}
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// Backspace
|
|
3129
|
+
if (key === "\x7f" || key === "\b") {
|
|
3130
|
+
if (inputBuffer.length > 0) {
|
|
3131
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
3132
|
+
cursorPos = inputBuffer.length;
|
|
3133
|
+
menuSelected = 0;
|
|
3134
|
+
refreshUI();
|
|
3135
|
+
}
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
836
3138
|
|
|
837
|
-
|
|
838
|
-
|
|
3139
|
+
// Arrow keys
|
|
3140
|
+
if (key === "\x1b[A") { // Up
|
|
3141
|
+
if (menuVisible && menuItems.length > 0) {
|
|
3142
|
+
menuSelected = Math.max(0, menuSelected - 1);
|
|
3143
|
+
refreshUI();
|
|
3144
|
+
} else if (inputHistory.length > 0) {
|
|
3145
|
+
historyIdx = Math.min(historyIdx + 1, inputHistory.length - 1);
|
|
3146
|
+
inputBuffer = inputHistory[historyIdx];
|
|
3147
|
+
cursorPos = inputBuffer.length;
|
|
3148
|
+
refreshUI();
|
|
3149
|
+
}
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (key === "\x1b[B") { // Down
|
|
3153
|
+
if (menuVisible && menuItems.length > 0) {
|
|
3154
|
+
menuSelected = Math.min(menuItems.length - 1, menuSelected + 1);
|
|
3155
|
+
refreshUI();
|
|
3156
|
+
} else if (historyIdx > 0) {
|
|
3157
|
+
historyIdx--;
|
|
3158
|
+
inputBuffer = inputHistory[historyIdx];
|
|
3159
|
+
cursorPos = inputBuffer.length;
|
|
3160
|
+
refreshUI();
|
|
3161
|
+
}
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// Escape — close menu
|
|
3166
|
+
if (key === "\x1b" || key === "\x1b\x1b") {
|
|
3167
|
+
if (menuVisible) {
|
|
3168
|
+
menuVisible = false;
|
|
3169
|
+
refreshUI();
|
|
3170
|
+
}
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// Ignore other escape sequences
|
|
3175
|
+
if (key.startsWith("\x1b")) return;
|
|
3176
|
+
|
|
3177
|
+
// Regular character
|
|
3178
|
+
inputBuffer += key;
|
|
3179
|
+
cursorPos = inputBuffer.length;
|
|
3180
|
+
menuSelected = 0;
|
|
3181
|
+
refreshUI();
|
|
3182
|
+
});
|
|
839
3183
|
}
|
|
840
3184
|
|
|
841
|
-
|
|
842
|
-
saveReturnedFiles,
|
|
843
|
-
parseLearnCommand,
|
|
844
|
-
parseResumeCommand,
|
|
845
|
-
parseWatchCommand,
|
|
846
|
-
buildQueueSnapshot,
|
|
847
|
-
buildTaskboard
|
|
848
|
-
};
|
|
3185
|
+
main().catch(console.error);
|