blun-king-cli 5.0.3 → 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.
Files changed (2) hide show
  1. package/blun-cli.js +3039 -699
  2. package/package.json +1 -1
package/blun-cli.js CHANGED
@@ -1,845 +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 { spawn } = require("child_process");
7
- const { listIdentities, getIdentity } = require("./identities");
8
- const { installSource, EXTENSIONS_DIR } = require("./installer");
9
-
10
- const HOME = process.env.BLUN_HOME || path.join(os.homedir(), ".blun");
11
- const CONFIG_FILE = path.join(HOME, "config.json");
12
- if (!fs.existsSync(HOME)) fs.mkdirSync(HOME, { recursive: true });
13
-
14
- const DEFAULT_CONFIG = {
15
- apiUrl: process.env.BLUN_API_URL || "http://176.9.158.30:3200",
16
- token: process.env.BLUN_API_TOKEN || "",
17
- streamLevel: "normal"
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
- if (!fs.existsSync(CONFIG_FILE)) {
22
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
23
- return { ...DEFAULT_CONFIG };
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
+ }
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
+ }
25
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
- return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) };
28
- } catch {
29
- return { ...DEFAULT_CONFIG };
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
+ }
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
+ }
30
293
  }
294
+ console.log("");
31
295
  }
32
296
 
33
- async function apiRequest(cfg, method, endpoint, body) {
34
- const headers = { "Content-Type": "application/json" };
35
- if (cfg.token) headers.Authorization = `Bearer ${cfg.token}`;
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);
301
+ }
36
302
 
37
- const resp = await fetch(`${cfg.apiUrl}${endpoint}`, {
38
- method,
39
- headers,
40
- body: body ? JSON.stringify(body) : undefined
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);
573
+
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
- const text = await resp.text();
43
- let data = {};
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() {
799
+ try {
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);
823
+ }
824
+ }
825
+
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
+ }
856
+
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);
883
+ }
884
+ }
885
+
886
+ async function cmdLearn(input, isUrl) {
887
+ if (!input) { printError("Usage: /learn <text> or /learn-url <url>"); return; }
44
888
  try {
45
- data = JSON.parse(text);
46
- } catch {
47
- data = { raw: text };
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);
48
899
  }
49
- if (!resp.ok) throw new Error(data.error || `HTTP ${resp.status}`);
50
- return data;
51
900
  }
52
901
 
53
- async function apiStreamRequest(cfg, endpoint, body, handlers = {}) {
54
- const headers = { "Content-Type": "application/json", Accept: "text/event-stream" };
55
- if (cfg.token) headers.Authorization = `Bearer ${cfg.token}`;
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";
56
914
 
57
- const resp = await fetch(`${cfg.apiUrl}${endpoint}`, {
58
- method: "POST",
59
- headers,
60
- body: JSON.stringify(body || {})
61
- });
62
- if (!resp.ok) {
63
- const text = await resp.text();
64
- throw new Error(text || `HTTP ${resp.status}`);
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);
65
930
  }
931
+ }
932
+
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
+ }
957
+
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);
969
+ }
970
+ console.log("");
971
+ } else { printError(resp.data.error || "Score failed"); }
972
+ } catch(e) { printError(e.message); }
973
+ }
974
+
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
+ }
66
999
 
67
- const decoder = new TextDecoder();
68
- const reader = resp.body.getReader();
69
- let buffer = "";
70
- let currentEvent = "message";
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
+ }
71
1017
 
72
- while (true) {
73
- const chunk = await reader.read();
74
- if (chunk.done) break;
75
- buffer += decoder.decode(chunk.value, { stream: true });
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
+ }
76
1095
 
77
- while (buffer.includes("\n\n")) {
78
- const index = buffer.indexOf("\n\n");
79
- const raw = buffer.slice(0, index);
80
- buffer = buffer.slice(index + 2);
81
- if (!raw.trim()) continue;
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
+ }
82
1124
 
83
- let event = currentEvent;
84
- const dataLines = [];
85
- for (const line of raw.split("\n")) {
86
- if (line.startsWith("event:")) event = line.slice(6).trim();
87
- if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
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
+ });
88
1135
  }
1136
+ console.log("");
1137
+ } else { printError(resp.data.error || "Versions failed"); }
1138
+ } catch(e) { printError(e.message); }
1139
+ }
89
1140
 
90
- if (!dataLines.length) continue;
91
- const payload = JSON.parse(dataLines.join("\n"));
92
- currentEvent = "message";
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
+ }
93
1149
 
94
- if (event === "meta" && handlers.onMeta) handlers.onMeta(payload);
95
- if (event === "token" && handlers.onToken) handlers.onToken(payload);
96
- if (event === "done" && handlers.onDone) handlers.onDone(payload);
97
- if (event === "error" && handlers.onError) handlers.onError(payload);
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 line(char = "-", len = 48) {
103
- return char.repeat(len);
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); }
1212
+ }
1213
+
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
+ }
104
1282
  }
105
1283
 
106
- function isPlaceholderContent(content) {
107
- return typeof content === "string" && content.length === 0;
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
- function saveReturnedFiles(workdir, files) {
111
- if (!Array.isArray(files)) return [];
112
- const written = [];
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
- for (const file of files) {
115
- if (!file || !file.path || typeof file.content !== "string") continue;
116
- if (isPlaceholderContent(file.content)) continue;
117
- const target = path.resolve(workdir, file.path);
118
- if (!target.startsWith(path.resolve(workdir))) continue;
119
- fs.mkdirSync(path.dirname(target), { recursive: true });
120
- fs.writeFileSync(target, file.content, "utf8");
121
- written.push(target);
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
- return written;
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 formatAnswerBlock(prefix, text) {
128
- return String(text || "")
129
- .split("\n")
130
- .map((entry) => `${prefix}${entry}`)
131
- .join("\n");
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
- function parseLearnCommand(input) {
135
- const raw = String(input || "").replace(/^\/learn\s+/i, "").trim();
136
- if (!raw) return { category: "general", content: "" };
137
- const alias = raw.match(/^alias\s+(.+?)=(.+)$/i);
138
- if (alias) {
139
- return { category: "system", content: `${alias[1].trim()} = ${alias[2].trim()}` };
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
- const prefer = raw.match(/^prefer\s+(.+)$/i);
142
- if (prefer) {
143
- return { category: "system", content: `Ich bevorzuge ${prefer[1].trim()}.` };
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
- const forbid = raw.match(/^forbid\s+(.+)$/i);
146
- if (forbid) {
147
- return { category: "system", content: `Mach niemals ${forbid[1].trim()}.` };
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
- const workflow = raw.match(/^workflow\s+(.+?)=(.+)$/i);
150
- if (workflow) {
151
- return { category: "system", content: `Wenn ich ${workflow[1].trim()} sage, meine ich ${workflow[2].trim()}.` };
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
- const match = raw.match(/^([a-z0-9_-]+)\s*:\s*([\s\S]+)$/i);
154
- if (match) {
155
- return {
156
- category: match[1].toLowerCase(),
157
- content: match[2].trim()
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
- function parseResumeCommand(input) {
164
- const match = String(input || "").match(/^\/resume\s+([a-zA-Z0-9._-]+)/);
165
- return match ? match[1] : "";
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 parseWatchCommand(input) {
169
- const match = String(input || "").match(/^\/watch\s+([a-zA-Z0-9._-]+)/);
170
- return match ? match[1] : "";
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 formatFlags(flags) {
174
- if (!Array.isArray(flags) || flags.length === 0) return "clean";
175
- return flags.join(", ");
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 formatRoutingSummary(data) {
179
- const confidence = data.routing?.confidence ?? data.confidence;
180
- const pct = typeof confidence === "number" ? `${Math.round(confidence * 100)}%` : "-";
181
- return `routing: ${data.task_type || data.type || "-"} confidence: ${pct}`;
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
- function formatLearnSummary(data) {
185
- const learned = data.learned || {};
186
- const types = learned.types || {};
187
- return [
188
- "",
189
- " Learn",
190
- ` ${line()}`,
191
- ` user: ${data.user_id || "-"}`,
192
- ` category: ${data.category || "general"}`,
193
- ` aliases: ${learned.aliasesLearned || 0} snippets: ${learned.snippetsLearned || 0}`,
194
- ` types: pref=${types.preferences || 0} fact=${types.facts || 0} workflow=${types.workflows || 0} guard=${types.guardrails || 0}`,
195
- ` file: ${data.file || "-"}`,
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
- function buildQueueSnapshot({ mode, selectedIdentity, currentJob, queue }) {
201
- const lines = [
202
- "",
203
- " Queue",
204
- ` ${line()}`,
205
- ` mode: ${mode} identity: ${selectedIdentity}`,
206
- ` running: ${currentJob ? `#${currentJob.id} ${currentJob.message}` : "idle"}`,
207
- ` pending: ${Array.isArray(queue) ? queue.length : 0}`
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
- for (const job of queue || []) {
211
- lines.push(` - #${job.id} ${job.message}`);
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
- lines.push("");
215
- return lines;
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 buildTaskboard({ mode, selectedIdentity, currentJob, queue, jobs = [], lastArtifacts = [] }) {
219
- const lines = [
220
- "",
221
- " Taskboard",
222
- ` ${line()}`,
223
- ` mode: ${mode}`,
224
- ` identity: ${selectedIdentity}`,
225
- ` running: ${currentJob ? `#${currentJob.id} ${currentJob.message}` : "idle"}`,
226
- ` pending: ${queue.length}`,
227
- ` remote jobs: ${jobs.length}`
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 };
1979
+
1980
+ var perms = loadPermissions();
229
1981
 
230
- if (queue.length) {
231
- lines.push(" pending queue:");
232
- for (const job of queue.slice(0, 5)) lines.push(` - #${job.id} ${job.message}`);
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 (jobs.length) {
236
- lines.push(" recent remote jobs:");
237
- for (const job of jobs.slice(0, 5)) {
238
- const summary = job.summary || {};
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 (lastArtifacts.length) {
249
- lines.push(" recent artifacts:");
250
- for (const artifact of lastArtifacts.slice(0, 5)) lines.push(` - ${artifact}`);
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
- lines.push("");
254
- return lines;
2015
+ return { allowed: true };
255
2016
  }
256
2017
 
257
- function formatJobResultLines(data, job, workdir, locallyWritten) {
258
- const header = ` BLUN King ${job.label} ${data.task_type}/${data.role} ${data.identity || "auto"}`;
259
- const lines = ["", header, ` ${line()}`, ` ${formatRoutingSummary(data)}`];
2018
+ // ══════════════════════════════════════════════════
2019
+ // ── 3. HOOKS ENGINE ──
2020
+ // ══════════════════════════════════════════════════
2021
+ const HOOKS_FILE = path.join(CONFIG_DIR, "hooks.json");
260
2022
 
261
- if (data.quality) {
262
- lines.push(` quality: ${data.quality.score ?? "-"} flags: ${formatFlags(data.quality.flags)}${data.quality.retried ? " retried" : ""}${data.quality.blocked ? " blocked" : ""}`);
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
- for (const step of data.steps || []) {
266
- lines.push(` [step ${step.step}] ${step.action}${step.detail ? ` -> ${step.detail}` : ""}${step.files ? ` -> ${step.files.join(", ")}` : ""}`);
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 (Array.isArray(data.references) && data.references.length) {
270
- for (const ref of data.references) {
271
- if (ref.error) lines.push(` [reference] ${ref.url} -> ${ref.error}`);
272
- else lines.push(` [reference] ${ref.url} -> ${ref.title || "no title"}${ref.screenshotPath ? ` (${ref.screenshotPath})` : ""}`);
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); });
2175
+
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("");
275
2190
 
276
- if (data.artifact?.filename) {
277
- lines.push(` artifact: ${data.artifact.filename}`);
2191
+ } else {
2192
+ printError("Usage: /agents list|create|run|info <name>");
278
2193
  }
2194
+ }
2195
+
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
+ }
279
2206
 
280
- if (Array.isArray(data.files) && data.files.length) {
281
- const fileList = data.files.map((file) => file.path).filter(Boolean);
282
- if (fileList.length) lines.push(` files: ${fileList.join(", ")}`);
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
+ }
284
2218
 
285
- if (locallyWritten.length) {
286
- lines.push(` local: ${locallyWritten.map((entry) => path.relative(workdir, entry)).join(", ")}`);
2219
+ // ══════════════════════════════════════════════════
2220
+ // ── 6. COST TRACKING ──
2221
+ // ══════════════════════════════════════════════════
2222
+ var sessionCost = { requests: 0, inputTokensEst: 0, outputTokensEst: 0 };
2223
+
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
- if (data.answer) {
290
- lines.push(formatAnswerBlock(" ", data.answer));
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
- if (data.status || typeof data.verified === "boolean") {
294
- lines.push(` status: ${data.status || "completed"} verified: ${data.verified ? "yes" : "no"}`);
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
- if (data.tokens) {
298
- lines.push(` tokens: ${data.tokens.total ?? "-"} total`);
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
- lines.push("");
302
- return lines;
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 formatRemoteJobLines(job) {
306
- const summary = job.summary || {};
307
- const lines = [
308
- "",
309
- ` Job ${job.id}`,
310
- ` ${line()}`,
311
- ` endpoint: ${job.endpoint}`,
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
- if (job.resumedFrom) lines.push(` resumed_from: ${job.resumedFrom}`);
317
- if (summary.taskType) lines.push(` task: ${summary.taskType}`);
318
- if (typeof summary.durationMs === "number") lines.push(` duration_ms: ${summary.durationMs}`);
319
- if (summary.fileCount) lines.push(` files: ${summary.fileCount}`);
320
- if (summary.referenceCount) lines.push(` references: ${summary.referenceCount}`);
321
- if (summary.artifact) lines.push(` artifact: ${summary.artifact}`);
322
- if (job.error) lines.push(` error: ${job.error}`);
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
- function runLocalShell(command, workdir) {
329
- return new Promise((resolve, reject) => {
330
- const child = spawn(command, {
331
- cwd: workdir,
332
- shell: true,
333
- stdio: ["ignore", "pipe", "pipe"]
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
+ }
2365
+
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 });
335
2375
 
336
- let stdout = "";
337
- let stderr = "";
2376
+ async function dreamMode() {
2377
+ if (chatHistory.length < 4) return; // not enough to dream about
338
2378
 
339
- child.stdout.on("data", (chunk) => {
340
- stdout += chunk.toString();
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
- child.stderr.on("data", (chunk) => {
343
- stderr += chunk.toString();
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
- child.on("error", reject);
346
- child.on("close", (code) => {
347
- if (code === 0) resolve({ code, stdout, stderr });
348
- else reject(new Error(stderr.trim() || `Command failed with exit code ${code}`));
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
- const cfg = loadConfig();
355
- const workdir = process.cwd();
356
- const sessionId = `cli_${Date.now()}`;
357
- const queue = [];
358
- let processing = false;
359
- let currentJob = null;
360
- let mode = "agent";
361
- let selectedIdentity = "auto";
362
- let cliClosed = false;
363
- let nextJobId = 1;
364
- let nextBtwId = 1;
365
- const lastArtifacts = [];
366
-
367
- const rl = readline.createInterface({
368
- input: process.stdin,
369
- output: process.stdout,
370
- prompt: ""
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
- function promptLabel() {
374
- return ` You [${mode}/${selectedIdentity}]> `;
375
- }
376
-
377
- function renderPrompt() {
378
- if (cliClosed || rl.closed || !process.stdin.isTTY) return;
379
- rl.setPrompt(promptLabel());
380
- rl.prompt(true);
381
- }
382
-
383
- function writeLines(lines) {
384
- const normalizedLines = Array.isArray(lines) ? lines : [lines];
385
- if (process.stdout.isTTY) {
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
- while (queue.length) {
436
- const job = queue.shift();
437
- currentJob = job;
438
- try {
439
- writeLines([` [running #${job.id}] ${job.message} pending:${queue.length}`]);
440
- if (job.endpoint === "/chat" && cfg.streamLevel !== "off") {
441
- let finalPayload = null;
442
- let headerPrinted = false;
443
- process.stdout.write("\n");
444
- await apiStreamRequest(cfg, "/chat/stream", {
445
- message: job.message,
446
- session_id: sessionId,
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
- } else {
485
- const data = await apiRequest(cfg, "POST", job.endpoint, {
486
- message: job.message,
487
- goal: job.message,
488
- session_id: sessionId,
489
- workdir,
490
- identity: selectedIdentity
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
- const locallyWritten = saveReturnedFiles(workdir, data.files);
493
- for (const file of locallyWritten) {
494
- lastArtifacts.unshift(path.relative(workdir, file));
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
- } catch (error) {
499
- writeLines([` ERROR [#${job.id}]: ${error.message}`]);
500
- } finally {
501
- currentJob = null;
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("");
503
2686
  }
2687
+ }
504
2688
 
505
- processing = false;
506
- renderPrompt();
507
- }
508
-
509
- // ── Arrow-key menu helper ──
510
- function arrowMenu(options, count) {
511
- let sel = 0;
512
- return new Promise((resolve) => {
513
- function render() {
514
- process.stdout.write(`\x1b[${count}A\r`);
515
- options.forEach((opt, i) => {
516
- const prefix = i === sel ? "\x1b[32m\x1b[1m \u276F " : " ";
517
- const color = i === sel ? "\x1b[97m\x1b[1m" : "\x1b[90m";
518
- process.stdout.write(`\x1b[2K${prefix}${color}${i + 1}. ${opt.label}\x1b[0m\n`);
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.");
2703
+ }
2704
+ }
2705
+
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
- for (let i = 0; i < count; i++) console.log("");
522
- render();
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") { sel = Math.max(0, sel - 1); render(); return; }
528
- if (key === "\x1b[B") { sel = Math.min(options.length - 1, sel + 1); render(); return; }
529
- for (let i = 0; i < options.length; i++) {
530
- if (key === String(i + 1)) { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve(options[i]); return; }
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
- // ── Trust Prompt ──
540
- const TRUST_FILE = path.join(HOME, "trusted.json");
541
- let trustedDirs = [];
542
- try { if (fs.existsSync(TRUST_FILE)) trustedDirs = JSON.parse(fs.readFileSync(TRUST_FILE, "utf8")); } catch {}
543
- const isTrusted = trustedDirs.includes(workdir) || process.argv.includes("--trust");
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
+ ];
544
2775
 
545
- const isTTY = process.stdin.isTTY || process.stdout.isTTY || process.platform === "win32";
546
- if (!isTrusted && isTTY) {
547
- console.log("");
548
- console.log(` \x1b[33m\u{1F4C1} Working in: \x1b[97m${workdir}\x1b[0m`);
549
- console.log(" \x1b[90mDo you trust this folder? BLUN King will read/write files here.\x1b[0m");
550
- console.log("");
551
- const trustChoice = await arrowMenu([
552
- { label: "Yes, trust this folder", action: "trust" },
553
- { label: "Always trust this folder", action: "always" },
554
- { label: "Choose another folder", action: "choose" }
555
- ], 3);
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;
2793
+ }
556
2794
 
557
- if (trustChoice.action === "always") {
558
- trustedDirs.push(workdir);
559
- fs.writeFileSync(TRUST_FILE, JSON.stringify(trustedDirs, null, 2));
560
- console.log(" \x1b[32m\u2713 Folder trusted permanently.\x1b[0m");
561
- } else if (trustChoice.action === "choose") {
562
- if (process.platform === "win32") {
563
- try {
564
- const 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\' }"';
565
- const newDir = require("child_process").execSync(psCmd, { encoding: "utf8", timeout: 60000 }).trim();
566
- if (newDir && newDir !== "CANCELLED" && fs.existsSync(newDir)) {
567
- process.chdir(newDir);
568
- console.log(` \x1b[32m\u2713 Workspace: ${newDir}\x1b[0m`);
569
- }
570
- } catch {}
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);
571
2845
  }
572
2846
  }
573
- console.log("");
2847
+
2848
+ return { lines: lines, cursorLine: 1, cursorCol: 4 + displayText.length };
574
2849
  }
575
2850
 
576
- // ── Mode Selection ──
577
- if (!process.argv.includes("--agent") && !process.argv.includes("--chat") && isTTY) {
578
- console.log(" \x1b[97m\x1b[1mHow do you want to work?\x1b[0m");
579
- console.log("");
580
- const modeChoice = await arrowMenu([
581
- { label: "Chat \u2014 talk back and forth", mode: "chat" },
582
- { label: "Agent \u2014 give a goal, I do the rest autonomously", mode: "agent" }
583
- ], 2);
584
- mode = modeChoice.mode;
585
- console.log(` \x1b[32m\u2713 Mode: ${mode === "agent" ? "Agent (autonomous)" : "Chat (interactive)"}\x1b[0m`);
586
- console.log("");
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
587
2883
  }
588
- if (process.argv.includes("--chat")) mode = "chat";
589
2884
 
590
- // ── Startup ──
591
- try {
592
- const health = await fetch(`${cfg.apiUrl}/health`).then((resp) => resp.json());
593
- console.log(` Connected to BLUN King API (${health.model})`);
594
- } catch (error) {
595
- console.error(` API connection failed: ${error.message}`);
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();
596
2916
  }
597
2917
 
598
- console.log("");
599
- console.log(" BLUN KING CLI v5.0.3");
600
- console.log(` API: ${cfg.apiUrl}`);
601
- console.log(` Dir: ${workdir}`);
602
- console.log(" Chat input stays available while jobs run.");
603
- console.log(" Normal input goes into pending. /btw uses an immediate side lane.");
604
- console.log("");
605
- renderPrompt();
2918
+ function drawPrompt() {
2919
+ _globalDrawPrompt = drawPrompt; // expose globally
2920
+ if (processing) return;
2921
+ console.log(""); // spacing
2922
+ uiStartRow = -1;
2923
+ refreshUI();
2924
+ }
606
2925
 
607
- rl.on("close", () => {
608
- cliClosed = true;
609
- });
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
+ ];
610
2951
 
611
- rl.on("line", (input) => {
612
- const message = input.trim();
613
- if (!message) return renderPrompt();
614
-
615
- if (message === "/exit" || message === "/quit") process.exit(0);
616
- if (message === "/help") return showHelp();
617
- if (message === "/chat") {
618
- mode = "chat";
619
- return writeLines([" Mode switched to chat."]);
620
- }
621
- if (message === "/agent") {
622
- mode = "agent";
623
- return writeLines([" Mode switched to agent."]);
624
- }
625
- if (message === "/identities") {
626
- const lines = ["", " Available identities:"];
627
- for (const identity of listIdentities()) {
628
- lines.push(` - ${identity.id}: ${identity.name}`);
629
- }
630
- lines.push("");
631
- return writeLines(lines);
632
- }
633
- if (message.startsWith("/identity")) {
634
- const nextIdentity = message.split(/\s+/)[1] || "auto";
635
- if (nextIdentity !== "auto" && getIdentity(nextIdentity).id !== nextIdentity) {
636
- return writeLines([` Unknown identity: ${nextIdentity}`]);
637
- }
638
- selectedIdentity = nextIdentity;
639
- return writeLines([` Identity set to ${selectedIdentity}.`]);
640
- }
641
- if (message === "/mode" || message === "/queue") {
642
- return writeLines(buildQueueSnapshot({ mode, selectedIdentity, currentJob, queue }));
643
- }
644
- if (message === "/board") {
645
- apiRequest(cfg, "GET", `/jobs?session_id=${encodeURIComponent(sessionId)}`)
646
- .then((data) => writeLines(buildTaskboard({ mode, selectedIdentity, currentJob, queue, jobs: data.jobs || [], lastArtifacts })))
647
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
648
- return renderPrompt();
649
- }
650
- if (message === "/cancel-pending") {
651
- const dropped = queue.length;
652
- queue.splice(0, queue.length);
653
- return writeLines([` Pending queue cleared. Dropped: ${dropped}`]);
654
- }
655
- if (message === "/jobs") {
656
- apiRequest(cfg, "GET", `/jobs?session_id=${encodeURIComponent(sessionId)}`)
657
- .then((data) => {
658
- const lines = ["", " Jobs", ` ${line()}`];
659
- for (const job of data.jobs || []) {
660
- const summary = job.summary || {};
661
- lines.push(` - ${job.id} ${job.status} ${job.endpoint}${summary.taskType ? ` ${summary.taskType}` : ""}${summary.durationMs ? ` ${summary.durationMs}ms` : ""}`);
662
- }
663
- lines.push("");
664
- writeLines(lines);
665
- })
666
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
667
- return renderPrompt();
668
- }
669
- if (message.startsWith("/watch ")) {
670
- const jobId = parseWatchCommand(message);
671
- if (!jobId) return writeLines([" Usage: /watch <jobId>"]);
672
- (async () => {
673
- let seenLogCount = 0;
674
- for (;;) {
675
- const job = await apiRequest(cfg, "GET", `/jobs/${jobId}`);
676
- const logData = await apiRequest(cfg, "GET", `/jobs/${jobId}/logs`);
677
- const newLogs = (logData.logs || []).slice(seenLogCount);
678
- for (const entry of newLogs) {
679
- writeLines([` [job ${jobId}] ${entry.event}${entry.payload?.error ? ` -> ${entry.payload.error}` : ""}`]);
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
+ }
680
3101
  }
681
- seenLogCount = (logData.logs || []).length;
682
- if (job.status === "completed" || job.status === "failed") {
683
- writeLines(formatRemoteJobLines(job));
684
- break;
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
+ }
685
3110
  }
686
- await new Promise((resolve) => setTimeout(resolve, 1000));
3111
+ } catch(e) {
3112
+ // Fallback: ignore paste errors
687
3113
  }
688
- })().catch((error) => writeLines([` ERROR: ${error.message}`]));
689
- return renderPrompt();
690
- }
691
- if (message.startsWith("/resume ")) {
692
- const jobId = parseResumeCommand(message);
693
- if (!jobId) return writeLines([" Usage: /resume <jobId>"]);
694
- apiRequest(cfg, "POST", `/jobs/${jobId}/resume`, {})
695
- .then((data) => writeLines([` resumed job: ${data.job_id} (from ${data.resumed_from})`]))
696
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
697
- return renderPrompt();
698
- }
699
- if (message.startsWith("/bg ")) {
700
- const payloadMessage = message.slice(4).trim();
701
- apiRequest(cfg, "POST", mode === "agent" ? "/agent" : "/chat", {
702
- message: payloadMessage,
703
- goal: payloadMessage,
704
- session_id: sessionId,
705
- workdir,
706
- identity: selectedIdentity,
707
- background: true
708
- })
709
- .then((data) => writeLines([` background job queued: ${data.job_id}`]))
710
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
711
- return renderPrompt();
712
- }
713
- if (message.startsWith("/learn ")) {
714
- const payload = parseLearnCommand(message);
715
- apiRequest(cfg, "POST", "/learn", {
716
- session_id: sessionId,
717
- user_id: sessionId,
718
- title: `cli-${Date.now()}`,
719
- category: payload.category,
720
- content: payload.content
721
- })
722
- .then((data) => writeLines(formatLearnSummary(data)))
723
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
724
- return renderPrompt();
725
- }
726
- if (message.startsWith("/btw ")) {
727
- const btwMessage = message.slice(5).trim();
728
- const btwId = nextBtwId++;
729
- writeLines([` [btw #${btwId}] sent immediately`]);
730
- apiRequest(cfg, "POST", mode === "agent" ? "/agent" : "/chat", {
731
- message: btwMessage,
732
- goal: btwMessage,
733
- session_id: sessionId,
734
- workdir,
735
- identity: selectedIdentity
736
- })
737
- .then((data) => writeLines(formatJobResultLines(data, { id: `btw-${btwId}`, label: `BTW#${btwId}` }, workdir, saveReturnedFiles(workdir, data.files))))
738
- .catch((error) => writeLines([` ERROR [BTW#${btwId}]: ${error.message}`]));
739
- return renderPrompt();
740
- }
741
- if (message.startsWith("/install ")) {
742
- installSource(message.slice(9).trim())
743
- .then((result) => writeLines([` installed ${result.kind}: ${result.target}`, ` extensions: ${EXTENSIONS_DIR}`]))
744
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
745
- return renderPrompt();
746
- }
747
- if (message.startsWith("/browser-open ")) {
748
- apiRequest(cfg, "POST", "/browser/open", { url: message.slice(14).trim() })
749
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
750
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
751
- return renderPrompt();
752
- }
753
- if (message.startsWith("/browser-click ")) {
754
- apiRequest(cfg, "POST", "/browser/click", { selector: message.slice(15).trim() })
755
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
756
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
757
- return renderPrompt();
758
- }
759
- if (message.startsWith("/browser-type ")) {
760
- const rest = message.slice(14).trim();
761
- const firstSpace = rest.indexOf(" ");
762
- const selector = firstSpace === -1 ? rest : rest.slice(0, firstSpace);
763
- const text = firstSpace === -1 ? "" : rest.slice(firstSpace + 1);
764
- apiRequest(cfg, "POST", "/browser/type", { selector, text })
765
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
766
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
767
- return renderPrompt();
768
- }
769
- if (message.startsWith("/browser-press ")) {
770
- apiRequest(cfg, "POST", "/browser/press", { key: message.slice(15).trim() })
771
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
772
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
773
- return renderPrompt();
774
- }
775
- if (message.startsWith("/browser-screenshot")) {
776
- const target = message.slice("/browser-screenshot".length).trim();
777
- apiRequest(cfg, "POST", "/browser/screenshot", target ? { path: target } : {})
778
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
779
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
780
- return renderPrompt();
781
- }
782
- if (message === "/browser-status") {
783
- apiRequest(cfg, "GET", "/browser/status")
784
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
785
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
786
- return renderPrompt();
787
- }
788
- if (message === "/browser-close") {
789
- apiRequest(cfg, "POST", "/browser/close", {})
790
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
791
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
792
- return renderPrompt();
793
- }
794
- if (message === "/status") {
795
- apiRequest(cfg, "GET", "/runtime/status")
796
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
797
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
798
- return renderPrompt();
799
- }
800
- if (message === "/health") {
801
- fetch(`${cfg.apiUrl}/health`)
802
- .then((resp) => resp.json())
803
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
804
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
805
- return renderPrompt();
806
- }
807
- if (message === "/artifacts") {
808
- apiRequest(cfg, "GET", "/artifacts")
809
- .then((data) => writeLines(["", JSON.stringify(data, null, 2), ""]))
810
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
811
- return renderPrompt();
812
- }
813
- if (message.startsWith("/local ")) {
814
- runLocalShell(message.slice(7), workdir)
815
- .then((result) => writeLines(["", result.stdout || "(no output)", ""]))
816
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
817
- return renderPrompt();
818
- }
819
- if (message.startsWith("/ssh ")) {
820
- runLocalShell(`ssh ${message.slice(5)}`, workdir)
821
- .then((result) => writeLines(["", result.stdout || "(no output)", ""]))
822
- .catch((error) => writeLines([` ERROR: ${error.message}`]));
823
- return renderPrompt();
824
- }
825
-
826
- const endpoint = mode === "agent" ? "/agent" : "/chat";
827
- const job = { id: nextJobId++, label: `#${nextJobId - 1}`, message, endpoint };
828
- queue.push(job);
829
- writeLines([` [pending #${job.id}] ${message} queue:${queue.length}`]);
830
- processQueue();
831
- });
832
- }
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
+ }
833
3127
 
834
- if (require.main === module) {
835
- main();
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
+ }
3138
+
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
+ });
836
3183
  }
837
3184
 
838
- module.exports = {
839
- saveReturnedFiles,
840
- parseLearnCommand,
841
- parseResumeCommand,
842
- parseWatchCommand,
843
- buildQueueSnapshot,
844
- buildTaskboard
845
- };
3185
+ main().catch(console.error);