blun-king-cli 4.1.1 → 5.0.1

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 (51) hide show
  1. package/api.js +965 -0
  2. package/blun-cli.js +820 -0
  3. package/blunking-api.js +7 -0
  4. package/bot.js +188 -0
  5. package/browser-controller.js +76 -0
  6. package/chat-memory.js +103 -0
  7. package/file-helper.js +63 -0
  8. package/fuzzy-match.js +78 -0
  9. package/identities.js +106 -0
  10. package/installer.js +160 -0
  11. package/job-manager.js +146 -0
  12. package/local-data.js +71 -0
  13. package/message-builder.js +28 -0
  14. package/noisy-evals.js +38 -0
  15. package/package.json +17 -4
  16. package/palace-memory.js +246 -0
  17. package/reference-inspector.js +228 -0
  18. package/runtime.js +555 -0
  19. package/task-executor.js +104 -0
  20. package/tests/browser-controller.test.js +42 -0
  21. package/tests/cli.test.js +93 -0
  22. package/tests/file-helper.test.js +18 -0
  23. package/tests/installer.test.js +39 -0
  24. package/tests/job-manager.test.js +99 -0
  25. package/tests/merge-compat.test.js +77 -0
  26. package/tests/messages.test.js +23 -0
  27. package/tests/noisy-evals.test.js +12 -0
  28. package/tests/noisy-intent-corpus.test.js +45 -0
  29. package/tests/reference-inspector.test.js +36 -0
  30. package/tests/runtime.test.js +119 -0
  31. package/tests/task-executor.test.js +40 -0
  32. package/tests/tools.test.js +23 -0
  33. package/tests/user-profile.test.js +66 -0
  34. package/tests/website-builder.test.js +66 -0
  35. package/tmp-build-smoke/nicrazy-landing/index.html +53 -0
  36. package/tmp-build-smoke/nicrazy-landing/style.css +110 -0
  37. package/tmp-shot-smoke/website-shot-1776006760424.png +0 -0
  38. package/tmp-shot-smoke/website-shot-1776007850007.png +0 -0
  39. package/tmp-shot-smoke/website-shot-1776007886209.png +0 -0
  40. package/tmp-shot-smoke/website-shot-1776007903766.png +0 -0
  41. package/tmp-shot-smoke/website-shot-1776008737117.png +0 -0
  42. package/tmp-shot-smoke/website-shot-1776008988859.png +0 -0
  43. package/tmp-smoke/nicrazy-landing/index.html +66 -0
  44. package/tmp-smoke/nicrazy-landing/style.css +104 -0
  45. package/tools.js +177 -0
  46. package/user-profile.js +395 -0
  47. package/website-builder.js +394 -0
  48. package/website-shot-1776010648230.png +0 -0
  49. package/website_builder.txt +38 -0
  50. package/bin/blun.js +0 -3196
  51. package/setup.js +0 -30
package/bin/blun.js DELETED
@@ -1,3196 +0,0 @@
1
- #!/usr/bin/env node
2
- // BLUN King CLI — Interactive Console
3
- const readline = require("readline");
4
- const fs = require("fs");
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 ──
27
- function loadConfig() {
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) {}
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
- }
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() {
149
- try {
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
- }
293
- }
294
- console.log("");
295
- }
296
-
297
- function printUserMessage(msg) {
298
- console.log("");
299
- console.log(C.brightBlue + C.bold + " " + BOX.user + " Du" + C.reset);
300
- console.log(C.brightWhite + " " + msg + C.reset);
301
- }
302
-
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();
617
- });
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; }
888
- try {
889
- process.stdout.write(C.dim + "📚 learning..." + C.reset);
890
- var body = isUrl ? { url: input } : { content: input };
891
- var resp = await apiCall("POST", "/learn", body);
892
- process.stdout.write("\r" + " ".repeat(30) + "\r");
893
- if (resp.status === 200) {
894
- printSuccess("Learned! Category: " + resp.data.category + " | File: " + resp.data.file);
895
- } else { printError(resp.data.error || "Learn failed"); }
896
- } catch(e) {
897
- process.stdout.write("\r" + " ".repeat(30) + "\r");
898
- printError(e.message);
899
- }
900
- }
901
-
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";
914
-
915
- try {
916
- process.stdout.write(C.dim + "⚙️ generating " + format + "..." + C.reset);
917
- var resp = await apiCall("POST", "/generate-file", { prompt: desc, format: format });
918
- process.stdout.write("\r" + " ".repeat(40) + "\r");
919
- if (resp.status === 200) {
920
- // Save locally too
921
- var localPath = path.join(config.workdir, resp.data.filename);
922
- fs.writeFileSync(localPath, resp.data.content);
923
- printSuccess("Generated: " + resp.data.filename + " (" + resp.data.size + " bytes)");
924
- printInfo("Local: " + localPath);
925
- printInfo("Remote: " + config.api.base_url + resp.data.download);
926
- } else { printError(resp.data.error || "Generate failed"); }
927
- } catch(e) {
928
- process.stdout.write("\r" + " ".repeat(40) + "\r");
929
- printError(e.message);
930
- }
931
- }
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
- }
999
-
1000
- function cmdSettings() {
1001
- console.log("");
1002
- console.log(C.bold + "BLUN King Settings:" + C.reset);
1003
- console.log(C.dim + "─".repeat(40) + C.reset);
1004
- console.log(" Auth Type: " + config.auth.type);
1005
- console.log(" API Key: " + (config.auth.api_key ? config.auth.api_key.slice(0, 10) + "..." : "(not set)"));
1006
- console.log(" OAuth Token: " + (config.auth.oauth_token ? "set (expires: " + config.auth.oauth_expires + ")" : "(not set)"));
1007
- console.log(" API URL: " + config.api.base_url);
1008
- console.log(" Model: " + config.model);
1009
- console.log(" Workdir: " + config.workdir);
1010
- console.log(" Verbose: " + config.verbose);
1011
- console.log(" Telegram: " + (config.telegram.enabled ? "on (chat: " + config.telegram.chat_id + ")" : "off"));
1012
- console.log(" Watchdog: " + (config.watchdog.enabled ? "on (" + config.watchdog.interval + "s)" : "off"));
1013
- console.log("");
1014
- console.log(C.dim + " Config file: " + CONFIG_FILE + C.reset);
1015
- console.log("");
1016
- }
1017
-
1018
- function cmdSet(args) {
1019
- var parts = args.split(/\s+/);
1020
- var key = parts[0];
1021
- var val = parts.slice(1).join(" ");
1022
-
1023
- switch(key) {
1024
- case "auth":
1025
- if (val === "api" || val === "api_key") {
1026
- config.auth.type = "api_key";
1027
- saveConfig(config);
1028
- printSuccess("Auth switched to API key.");
1029
- } else if (val === "oauth") {
1030
- config.auth.type = "oauth";
1031
- printInfo("Enter OAuth token:");
1032
- // Will be set interactively
1033
- var rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
1034
- rl2.question(" OAuth Token: ", function(token) {
1035
- config.auth.oauth_token = token.trim();
1036
- rl2.question(" Expires (ISO date or 'never'): ", function(exp) {
1037
- config.auth.oauth_expires = exp.trim() === "never" ? null : exp.trim();
1038
- saveConfig(config);
1039
- printSuccess("OAuth configured.");
1040
- rl2.close();
1041
- });
1042
- });
1043
- return;
1044
- } else {
1045
- printError("Usage: /set auth api or /set auth oauth");
1046
- }
1047
- break;
1048
-
1049
- case "key":
1050
- config.auth.api_key = val;
1051
- config.auth.type = "api_key";
1052
- saveConfig(config);
1053
- printSuccess("API key set: " + val.slice(0, 10) + "...");
1054
- break;
1055
-
1056
- case "url":
1057
- config.api.base_url = val;
1058
- saveConfig(config);
1059
- printSuccess("API URL set: " + val);
1060
- break;
1061
-
1062
- case "telegram":
1063
- printInfo("Telegram Bridge Setup:");
1064
- var rl3 = readline.createInterface({ input: process.stdin, output: process.stdout });
1065
- rl3.question(" Bot Token: ", function(token) {
1066
- config.telegram.bot_token = token.trim();
1067
- rl3.question(" Chat ID: ", function(chatId) {
1068
- config.telegram.chat_id = chatId.trim();
1069
- config.telegram.enabled = true;
1070
- saveConfig(config);
1071
- printSuccess("Telegram bridge configured.");
1072
- rl3.close();
1073
- });
1074
- });
1075
- return;
1076
-
1077
- case "verbose":
1078
- config.verbose = !config.verbose;
1079
- saveConfig(config);
1080
- printSuccess("Verbose logging: " + (config.verbose ? "ON" : "OFF"));
1081
- break;
1082
-
1083
- case "workdir":
1084
- if (fs.existsSync(val)) {
1085
- config.workdir = path.resolve(val);
1086
- saveConfig(config);
1087
- printSuccess("Workdir: " + config.workdir);
1088
- } else { printError("Directory not found: " + val); }
1089
- break;
1090
-
1091
- default:
1092
- printError("Unknown setting. Use /settings to see all options.");
1093
- }
1094
- }
1095
-
1096
- async function cmdStatus() {
1097
- try {
1098
- process.stdout.write(C.dim + "checking..." + C.reset);
1099
- var resp = await apiCall("GET", "/runtime/status");
1100
- process.stdout.write("\r" + " ".repeat(20) + "\r");
1101
- if (resp.status === 200) {
1102
- var d = resp.data;
1103
- console.log("");
1104
- console.log(C.bold + "BLUN King Runtime Status:" + C.reset);
1105
- console.log(C.dim + "─".repeat(40) + C.reset);
1106
- console.log(" Status: " + C.green + d.status + C.reset);
1107
- console.log(" Model: " + d.model);
1108
- console.log(" Ollama: " + (d.ollama === "connected" ? C.green : C.red) + d.ollama + C.reset);
1109
- console.log(" Uptime: " + d.uptime_seconds + "s");
1110
- console.log(" Artifacts: " + d.artifacts_count);
1111
- console.log(" Models: " + (d.available_models || []).join(", "));
1112
- if (d.prompt_registry && d.prompt_registry.layers) {
1113
- console.log("");
1114
- console.log(C.bold + " Prompt Registry:" + C.reset);
1115
- d.prompt_registry.layers.forEach(function(l) { console.log(" " + l); });
1116
- }
1117
- console.log("");
1118
- } else { printError(resp.data.error || "Status failed"); }
1119
- } catch(e) {
1120
- process.stdout.write("\r" + " ".repeat(20) + "\r");
1121
- printError(e.message);
1122
- }
1123
- }
1124
-
1125
- async function cmdVersions() {
1126
- try {
1127
- var resp = await apiCall("GET", "/versions");
1128
- if (resp.status === 200) {
1129
- console.log("");
1130
- console.log(C.bold + "Prompt Versions:" + C.reset);
1131
- if (resp.data.versions) {
1132
- resp.data.versions.forEach(function(v) {
1133
- console.log(" " + (v.active ? C.green + "● " : C.dim + "○ ") + v.version + C.reset + " " + v.file);
1134
- });
1135
- }
1136
- console.log("");
1137
- } else { printError(resp.data.error || "Versions failed"); }
1138
- } catch(e) { printError(e.message); }
1139
- }
1140
-
1141
- async function cmdHealth() {
1142
- try {
1143
- var resp = await apiCall("GET", "/health");
1144
- if (resp.status === 200) {
1145
- printSuccess("API healthy — " + resp.data.model + " | uptime: " + Math.round(resp.data.uptime) + "s");
1146
- } else { printError("API unhealthy: " + resp.status); }
1147
- } catch(e) { printError("API unreachable: " + e.message); }
1148
- }
1149
-
1150
- function cmdWatchdog(args) {
1151
- if (args === "on") {
1152
- config.watchdog.enabled = true;
1153
- saveConfig(config);
1154
- printSuccess("Watchdog enabled (interval: " + config.watchdog.interval + "s)");
1155
- } else if (args === "off") {
1156
- config.watchdog.enabled = false;
1157
- saveConfig(config);
1158
- printSuccess("Watchdog disabled.");
1159
- } else {
1160
- console.log("");
1161
- console.log(C.bold + "Watchdog:" + C.reset);
1162
- console.log(" Status: " + (config.watchdog.enabled ? C.green + "ON" : C.red + "OFF") + C.reset);
1163
- console.log(" Interval: " + config.watchdog.interval + "s");
1164
- console.log("");
1165
- console.log(C.dim + " /watchdog on — Enable" + C.reset);
1166
- console.log(C.dim + " /watchdog off — Disable" + C.reset);
1167
- console.log("");
1168
- }
1169
- }
1170
-
1171
- function cmdMemory(args) {
1172
- if (!args) {
1173
- // List memory
1174
- var files = fs.existsSync(MEMORY_DIR) ? fs.readdirSync(MEMORY_DIR) : [];
1175
- console.log("");
1176
- console.log(C.bold + "Local Memory (" + files.length + " entries):" + C.reset);
1177
- files.forEach(function(f) {
1178
- var content = fs.readFileSync(path.join(MEMORY_DIR, f), "utf8").trim();
1179
- console.log(" " + C.yellow + f.replace(".txt", "") + C.reset + " — " + content.slice(0, 60));
1180
- });
1181
- if (files.length === 0) console.log(C.dim + " (empty)" + C.reset);
1182
- console.log("");
1183
- } else {
1184
- var parts = args.split(/\s+/);
1185
- if (parts[0] === "save" && parts.length >= 3) {
1186
- var key = parts[1];
1187
- var value = parts.slice(2).join(" ");
1188
- fs.writeFileSync(path.join(MEMORY_DIR, key + ".txt"), value);
1189
- printSuccess("Saved: " + key);
1190
- } else if (parts[0] === "del" && parts[1]) {
1191
- var file = path.join(MEMORY_DIR, parts[1] + ".txt");
1192
- if (fs.existsSync(file)) { fs.unlinkSync(file); printSuccess("Deleted: " + parts[1]); }
1193
- else { printError("Not found: " + parts[1]); }
1194
- } else {
1195
- printError("Usage: /memory save <key> <value> or /memory del <key>");
1196
- }
1197
- }
1198
- }
1199
-
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
- }
1282
- }
1283
-
1284
- // ── SSH Commands ──
1285
- function cmdSsh(args) {
1286
- if (!args) {
1287
- // Show saved connections
1288
- var sshFile = path.join(CONFIG_DIR, "ssh.json");
1289
- if (fs.existsSync(sshFile)) {
1290
- var connections = JSON.parse(fs.readFileSync(sshFile, "utf8"));
1291
- console.log("");
1292
- console.log(C.bold + "SSH Connections:" + C.reset);
1293
- for (var name in connections) {
1294
- var c = connections[name];
1295
- console.log(" " + C.yellow + name + C.reset + " — " + c.user + "@" + c.host + (c.port !== 22 ? ":" + c.port : ""));
1296
- }
1297
- console.log("");
1298
- console.log(C.dim + " /ssh <name> <command> — Run command" + C.reset);
1299
- console.log(C.dim + " /ssh add <name> <user@host> — Add connection" + C.reset);
1300
- console.log(C.dim + " /ssh del <name> — Remove connection" + C.reset);
1301
- } else {
1302
- printInfo("No SSH connections saved. Use /ssh add <name> <user@host>");
1303
- }
1304
- return;
1305
- }
1306
-
1307
- var parts = args.split(/\s+/);
1308
- var sshFile = path.join(CONFIG_DIR, "ssh.json");
1309
- var connections = {};
1310
- if (fs.existsSync(sshFile)) {
1311
- try { connections = JSON.parse(fs.readFileSync(sshFile, "utf8")); } catch(e) {}
1312
- }
1313
-
1314
- if (parts[0] === "add" && parts[1] && parts[2]) {
1315
- var match = parts[2].match(/^([^@]+)@([^:]+)(?::(\d+))?$/);
1316
- if (!match) { printError("Format: user@host or user@host:port"); return; }
1317
- connections[parts[1]] = { user: match[1], host: match[2], port: parseInt(match[3] || "22"), key: parts[3] || null };
1318
- fs.writeFileSync(sshFile, JSON.stringify(connections, null, 2));
1319
- printSuccess("SSH connection '" + parts[1] + "' saved.");
1320
- return;
1321
- }
1322
-
1323
- if (parts[0] === "del" && parts[1]) {
1324
- delete connections[parts[1]];
1325
- fs.writeFileSync(sshFile, JSON.stringify(connections, null, 2));
1326
- printSuccess("SSH connection '" + parts[1] + "' removed.");
1327
- return;
1328
- }
1329
-
1330
- // Execute command on saved connection
1331
- var connName = parts[0];
1332
- var remoteCmd = parts.slice(1).join(" ");
1333
- if (!connections[connName]) { printError("Unknown connection: " + connName + ". Use /ssh to list."); return; }
1334
- if (!remoteCmd) { printError("Usage: /ssh " + connName + " <command>"); return; }
1335
-
1336
- var conn = connections[connName];
1337
- var sshCmd = "ssh";
1338
- if (conn.key) sshCmd += " -i " + conn.key;
1339
- sshCmd += " -o ConnectTimeout=10 -o StrictHostKeyChecking=no";
1340
- sshCmd += " -p " + conn.port;
1341
- sshCmd += " " + conn.user + "@" + conn.host;
1342
- sshCmd += ' "' + remoteCmd.replace(/"/g, '\\"') + '"';
1343
-
1344
- printInfo("SSH " + connName + ": " + remoteCmd);
1345
- cmdShell(sshCmd);
1346
- }
1347
-
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
- }
1360
-
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.");
1399
- }
1400
- }
1401
-
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("");
1416
- }
1417
-
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
- }
1433
- }
1434
-
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 });
1442
- }
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.");
1460
- }
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.");
1469
- }
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.");
1476
- }
1477
-
1478
- if (args) {
1479
- config.workdir = projectDir;
1480
- saveConfig(config);
1481
- printSuccess("Workdir set to: " + projectDir);
1482
- }
1483
- }
1484
-
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 {};
1494
- }
1495
-
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: [] };
1503
- }
1504
-
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
- }
1641
- }
1642
-
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
- }
1664
- }
1665
-
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
- }
1794
- }
1795
-
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
- }
1820
-
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;
1836
- }
1837
- return config.workdir;
1838
- }
1839
-
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 {};
1851
- }
1852
-
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();
1981
-
1982
- if (action === "write") {
1983
- var denyWrite = getSetting("sandbox.filesystem.denyWrite").value || [];
1984
- var allowWrite = getSetting("sandbox.filesystem.allowWrite").value || [];
1985
- for (var i = 0; i < denyWrite.length; i++) {
1986
- if (target.includes(denyWrite[i])) return { allowed: false, reason: "denyWrite: " + denyWrite[i] };
1987
- }
1988
- if (allowWrite.length > 0) {
1989
- var ok = false;
1990
- for (var j = 0; j < allowWrite.length; j++) {
1991
- if (target.startsWith(allowWrite[j])) { ok = true; break; }
1992
- }
1993
- if (!ok) return { allowed: false, reason: "Not in allowWrite list" };
1994
- }
1995
- }
1996
-
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] };
2001
- }
2002
- }
2003
-
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
- }
2013
- }
2014
-
2015
- return { allowed: true };
2016
- }
2017
-
2018
- // ══════════════════════════════════════════════════
2019
- // ── 3. HOOKS ENGINE ──
2020
- // ══════════════════════════════════════════════════
2021
- const HOOKS_FILE = path.join(CONFIG_DIR, "hooks.json");
2022
-
2023
- function loadHooks() {
2024
- if (fs.existsSync(HOOKS_FILE)) {
2025
- try { return JSON.parse(fs.readFileSync(HOOKS_FILE, "utf8")); } catch(e) {}
2026
- }
2027
- return { pre: {}, post: {} };
2028
- }
2029
-
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); }
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("");
2071
-
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]);
2092
- }
2093
- } else {
2094
- printError("Usage: /hooks list|add|remove — /hooks add pre:chat \"echo hello\"");
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("");
2190
-
2191
- } else {
2192
- printError("Usage: /agents list|create|run|info <name>");
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
- }
2206
-
2207
- function loadSessionHistory() {
2208
- var f = getSessionFile();
2209
- if (fs.existsSync(f)) {
2210
- try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch(e) {}
2211
- }
2212
- return { workdir: config.workdir, history: [], inputHistory: [] };
2213
- }
2214
-
2215
- function saveSessionHistory(session) {
2216
- fs.writeFileSync(getSessionFile(), JSON.stringify(session, null, 2));
2217
- }
2218
-
2219
- // ══════════════════════════════════════════════════
2220
- // ── 6. COST TRACKING ──
2221
- // ══════════════════════════════════════════════════
2222
- var sessionCost = { requests: 0, inputTokensEst: 0, outputTokensEst: 0 };
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);
2252
- }
2253
-
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);
2260
- }
2261
-
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);
2268
- }
2269
-
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;
2306
- }
2307
- config.model = args.trim();
2308
- saveConfig(config);
2309
- printSuccess("Model switched to: " + config.model);
2310
- }
2311
-
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
- }
2331
- }
2332
-
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
- }
2340
-
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");
2348
- }
2349
-
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```"
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 });
2375
-
2376
- async function dreamMode() {
2377
- if (chatHistory.length < 4) return; // not enough to dream about
2378
-
2379
- try {
2380
- // Ask BLUN to extract key learnings
2381
- var dreamPrompt = [
2382
- { role: "system", content: "Du bist der BLUN King Memory Manager. Analysiere diese Konversation und extrahiere die wichtigsten Fakten, Entscheidungen und Erkenntnisse. Antworte NUR mit einem JSON-Objekt:\n{\"learnings\": [\"...\", \"...\"], \"user_preferences\": [\"...\"], \"project_facts\": [\"...\"], \"summary\": \"1 Satz Zusammenfassung\"}\nKein Markdown, NUR JSON." },
2383
- { role: "user", content: "Konversation:\n" + chatHistory.slice(-10).map(function(m) { return m.role + ": " + m.content.slice(0, 200); }).join("\n") }
2384
- ];
2385
-
2386
- var resp = await apiCall("POST", "/chat", { message: dreamPrompt[1].content, history: [dreamPrompt[0]] });
2387
- if (resp.status !== 200) return;
2388
-
2389
- var answer = resp.data.answer;
2390
- var dreamData;
2391
- try {
2392
- var jsonMatch = answer.match(/\{[\s\S]*\}/);
2393
- if (jsonMatch) dreamData = JSON.parse(jsonMatch[0]);
2394
- } catch(e) { return; }
2395
-
2396
- if (!dreamData) return;
2397
-
2398
- // Save dream
2399
- var dreamFile = path.join(DREAM_DIR, new Date().toISOString().slice(0, 10) + ".json");
2400
- var existingDreams = [];
2401
- if (fs.existsSync(dreamFile)) {
2402
- try { existingDreams = JSON.parse(fs.readFileSync(dreamFile, "utf8")); } catch(e) {}
2403
- }
2404
- existingDreams.push({
2405
- timestamp: new Date().toISOString(),
2406
- workdir: config.workdir,
2407
- learnings: dreamData.learnings || [],
2408
- user_preferences: dreamData.user_preferences || [],
2409
- project_facts: dreamData.project_facts || [],
2410
- summary: dreamData.summary || ""
2411
- });
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
- });
2438
- });
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);
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);
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); });
2526
- }
2527
-
2528
- // ── Main Loop ──
2529
- async function main() {
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
- }
2564
-
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
- }
2578
-
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);
2591
- }
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");
2627
- });
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;
2647
- }
2648
- }
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
- }
2683
- }
2684
- // trust = trust for this session only
2685
- console.log("");
2686
- }
2687
- }
2688
-
2689
- printHeader();
2690
- checkForUpdates();
2691
-
2692
- // API Key check — prompt if missing
2693
- if (!config.api.key && !process.argv.includes("--api-key")) {
2694
- console.log("");
2695
- console.log(C.yellow + " " + BOX.bot + " No API key configured." + C.reset);
2696
- console.log(C.gray + " Enter your BLUN King API key (or press Enter to skip):" + C.reset);
2697
- var apiKeyAnswer = await new Promise(function(resolve) {
2698
- var rlKey = readline.createInterface({ input: process.stdin, output: process.stdout });
2699
- rlKey.question(C.brightBlue + " API Key: " + C.reset, function(answer) {
2700
- rlKey.close();
2701
- resolve(answer.trim());
2702
- });
2703
- });
2704
- if (apiKeyAnswer) {
2705
- config.api.key = apiKeyAnswer;
2706
- // Save to config file
2707
- var cfgFile = path.join(CONFIG_DIR, "config.json");
2708
- var cfgData = {};
2709
- if (fs.existsSync(cfgFile)) { try { cfgData = JSON.parse(fs.readFileSync(cfgFile, "utf8")); } catch(e) {} }
2710
- if (!cfgData.api) cfgData.api = {};
2711
- cfgData.api.key = apiKeyAnswer;
2712
- fs.writeFileSync(cfgFile, JSON.stringify(cfgData, null, 2));
2713
- printSuccess("API key saved.");
2714
- }
2715
- }
2716
-
2717
- // Load session history for this workdir
2718
- var session = loadSessionHistory();
2719
- chatHistory = session.history.slice(-20);
2720
-
2721
- // Health check
2722
- try {
2723
- var h = await apiCall("GET", "/health");
2724
- if (h.status === 200) printSuccess("Connected to BLUN King API");
2725
- else printError("API returned " + h.status);
2726
- } catch(e) {
2727
- printError("Cannot reach API at " + config.api.base_url);
2728
- printInfo("Run: blun setup — to configure");
2729
- }
2730
-
2731
- // Mode selection at startup
2732
- var sessionMode = "chat"; // chat or agent
2733
- if (!process.argv.includes("--agent") && !process.argv.includes("--chat")) {
2734
- console.log("");
2735
- console.log(C.brightWhite + C.bold + " How do you want to work?" + C.reset);
2736
- console.log("");
2737
- var modeOptions = [
2738
- { label: "Chat — talk back and forth", mode: "chat" },
2739
- { label: "Agent — give a goal, I do the rest autonomously", mode: "agent" }
2740
- ];
2741
- var modeSel = 0;
2742
- var modeChoice = await new Promise(function(resolve) {
2743
- function renderModeMenu() {
2744
- process.stdout.write("\x1b[2A\r");
2745
- modeOptions.forEach(function(opt, i) {
2746
- var prefix = i === modeSel ? C.green + C.bold + " \u276F " : " ";
2747
- var color = i === modeSel ? C.brightWhite + C.bold : C.gray;
2748
- process.stdout.write("\x1b[2K" + prefix + color + (i + 1) + ". " + opt.label + C.reset + "\n");
2749
- });
2750
- }
2751
- console.log(""); console.log("");
2752
- renderModeMenu();
2753
- process.stdin.setRawMode(true);
2754
- process.stdin.resume();
2755
- process.stdin.setEncoding("utf8");
2756
- function onKey(key) {
2757
- if (key === "\x1b[A") { modeSel = 0; renderModeMenu(); return; }
2758
- if (key === "\x1b[B") { modeSel = 1; renderModeMenu(); return; }
2759
- if (key === "1") { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve("chat"); return; }
2760
- if (key === "2") { process.stdin.removeListener("data", onKey); process.stdin.setRawMode(false); resolve("agent"); return; }
2761
- if (key === "\r" || key === "\n") {
2762
- process.stdin.removeListener("data", onKey);
2763
- process.stdin.setRawMode(false);
2764
- resolve(modeOptions[modeSel].mode);
2765
- return;
2766
- }
2767
- }
2768
- process.stdin.on("data", onKey);
2769
- });
2770
- sessionMode = modeChoice;
2771
- printSuccess("Mode: " + (sessionMode === "agent" ? "Agent (autonomous)" : "Chat (interactive)"));
2772
- console.log("");
2773
- } else {
2774
- sessionMode = process.argv.includes("--agent") ? "agent" : "chat";
2775
- }
2776
-
2777
- var ALL_COMMANDS = [
2778
- "/help", "/clear", "/skills", "/skill", "/search", "/learn", "/learn-url",
2779
- "/generate", "/analyze", "/score", "/eval", "/settings", "/set",
2780
- "/status", "/versions", "/health", "/watchdog", "/memory", "/files",
2781
- "/sh", "/git", "/ssh", "/deploy", "/read", "/write", "/init",
2782
- "/screenshot", "/render", "/agent", "/plugin", "/mcp", "/permissions",
2783
- "/config", "/hooks", "/agents", "/doctor", "/model", "/login", "/logout",
2784
- "/compact", "/review", "/cost", "/exit"
2785
- ];
2786
-
2787
- // ── Bordered Input Box + Live Slash Menu ──
2788
- var inputBuffer = "";
2789
- var inputHistory = [];
2790
- var historyIdx = -1;
2791
- var menuVisible = false;
2792
- var menuItems = [];
2793
- var menuSelected = 0;
2794
- var cursorPos = 0;
2795
- var processing = false;
2796
- var uiStartRow = -1; // row where UI starts on screen
2797
-
2798
- function getTermWidth() { return process.stdout.columns || 80; }
2799
-
2800
- // Get current cursor row via sync trick
2801
- function getCursorRow() {
2802
- // Fallback: track manually
2803
- return -1;
2804
- }
2805
-
2806
- // Erase UI using readline module (Windows-safe)
2807
- function eraseUI() {
2808
- if (uiStartRow < 0) return;
2809
- // Move cursor to where UI started
2810
- readline.cursorTo(process.stdout, 0, uiStartRow);
2811
- // Clear everything from here down
2812
- readline.clearScreenDown(process.stdout);
2813
- uiStartRow = -1;
2814
- }
2815
-
2816
- // Build UI lines as array, then write all at once
2817
- function buildUILines() {
2818
- var w = Math.min(getTermWidth() - 4, 76);
2819
- var displayText = inputBuffer;
2820
- if (displayText.length > w - 4) displayText = displayText.slice(-(w - 4));
2821
- var inner = w - 2;
2822
- var textPad = Math.max(0, inner - 2 - displayText.length);
2823
- var lines = [];
2824
-
2825
- // Box
2826
- lines.push(C.brightBlue + " \u256D" + "\u2500".repeat(inner) + "\u256E" + C.reset);
2827
- lines.push(C.brightBlue + " \u2502 " + C.reset + C.brightWhite + displayText + C.reset + " ".repeat(textPad) + " " + C.brightBlue + "\u2502" + C.reset);
2828
- lines.push(C.brightBlue + " \u2570" + "\u2500".repeat(inner) + "\u256F" + C.reset);
2829
-
2830
- // Status bar
2831
- var permData = loadPermissions();
2832
- var permMode = permData.mode || "ask";
2833
- var isDangerous = process.argv.includes("--dangerously-skip-permissions") || permMode === "allow" || permMode === "allow-all";
2834
- var permLabel = isDangerous ? "GOD MODE" : permMode === "deny" ? "LOCKED" : "ask permission";
2835
- var permIcon = isDangerous ? "\u26A1" : permMode === "deny" ? "\u2718" : "\u2753";
2836
- var permColor = isDangerous ? C.red + C.bold : permMode === "deny" ? C.red : C.yellow;
2837
- var modelName = typeof config.model === "string" ? config.model : (config.model && config.model.name ? config.model.name : "default");
2838
- var wdShort = config.workdir ? path.basename(config.workdir) : "~";
2839
- var totalTok = sessionCost.inputTokensEst + sessionCost.outputTokensEst;
2840
- var tokStr = totalTok > 1000 ? (totalTok / 1000).toFixed(1) + "k" : String(totalTok);
2841
- 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);
2842
-
2843
- // Menu
2844
- if (menuVisible && menuItems.length > 0) {
2845
- var show = menuItems.slice(0, 8);
2846
- show.forEach(function(item, i) {
2847
- var prefix = i === menuSelected ? C.green + C.bold + " \u276F " : " ";
2848
- var cmdStr = (item.cmd + " ".repeat(20)).slice(0, 18);
2849
- var descStr = item.desc.slice(0, getTermWidth() - 28);
2850
- var cmdColor = i === menuSelected ? C.green + C.bold : C.green;
2851
- var descColor = i === menuSelected ? C.brightWhite : C.white;
2852
- lines.push(prefix + cmdColor + cmdStr + descColor + descStr + C.reset);
2853
- });
2854
- if (menuItems.length > 8) {
2855
- lines.push(C.dim + " ... " + (menuItems.length - 8) + " more" + C.reset);
2856
- }
2857
- }
2858
-
2859
- return { lines: lines, cursorLine: 1, cursorCol: 4 + displayText.length };
2860
- }
2861
-
2862
- function drawUI() {
2863
- process.stdout.write("\x1b[?25l"); // hide cursor
2864
- // Remember where we start drawing
2865
- uiStartRow = (process.stdout.rows || 24) - 1; // approximate
2866
- // Use getCursorPosition trick: write lines, then position cursor
2867
- var ui = buildUILines();
2868
-
2869
- // Save absolute position before drawing
2870
- // Write a marker to know our row
2871
- process.stdout.write("\x1b[6n"); // request cursor position (async, but we don't wait)
2872
-
2873
- // Actually just track rows manually
2874
- uiStartRow = -1; // will be set below
2875
-
2876
- // Clear any previous content and write fresh
2877
- var output = ui.lines.join("\n") + "\n";
2878
- process.stdout.write(output);
2879
-
2880
- // Now cursor is at bottom. Calculate how many lines we wrote.
2881
- var totalLines = ui.lines.length;
2882
-
2883
- // Move cursor back to input position (line index 1 = content line)
2884
- // We're at totalLines below start, need to go up (totalLines - 1 - cursorLine) from current minus 1
2885
- var upMoves = totalLines - ui.cursorLine;
2886
- process.stdout.write("\x1b[" + upMoves + "A");
2887
- process.stdout.write("\r\x1b[" + ui.cursorCol + "C");
2888
-
2889
- // Track for eraseUI: store how far up the start is from cursor
2890
- uiStartRow = totalLines; // repurpose as "total lines drawn"
2891
- _globalUILines = totalLines;
2892
-
2893
- process.stdout.write("\x1b[?25h"); // show cursor
2894
- }
2895
-
2896
- // Override eraseUI to use line count
2897
- _globalEraseUI = eraseUI = function() {
2898
- if (uiStartRow <= 0) return;
2899
- process.stdout.write("\x1b[?25l");
2900
- // Cursor is on content line (line 1). Move up 1 to reach top.
2901
- process.stdout.write("\x1b[1A\r");
2902
- // Clear all lines
2903
- for (var i = 0; i < uiStartRow + 2; i++) {
2904
- process.stdout.write("\x1b[2K\x1b[1B");
2905
- }
2906
- // Move back up
2907
- process.stdout.write("\x1b[" + (uiStartRow + 2) + "A\r");
2908
- process.stdout.write("\x1b[?25h");
2909
- uiStartRow = -1;
2910
- };
2911
-
2912
- function refreshUI() {
2913
- if (uiStartRow > 0) eraseUI();
2914
- // Compute menu items
2915
- if (inputBuffer.startsWith("/")) {
2916
- var filter = inputBuffer.toLowerCase();
2917
- menuItems = COMMAND_DESCRIPTIONS.filter(function(c) {
2918
- return c.cmd.includes(filter) || c.desc.toLowerCase().includes(filter.slice(1));
2919
- });
2920
- menuVisible = menuItems.length > 0;
2921
- if (menuVisible) menuSelected = Math.min(menuSelected, menuItems.length - 1);
2922
- } else {
2923
- menuVisible = false;
2924
- menuItems = [];
2925
- }
2926
- drawUI();
2927
- }
2928
-
2929
- function drawPrompt() {
2930
- _globalDrawPrompt = drawPrompt; // expose globally
2931
- if (processing) return;
2932
- console.log(""); // spacing
2933
- uiStartRow = -1;
2934
- refreshUI();
2935
- }
2936
-
2937
- // Intent detection: natural language → slash command
2938
- var INTENTS = [
2939
- { patterns: ["verbinde.*telegram", "connect.*telegram", "telegram.*verbind", "telegram.*setup", "telegram.*einricht"], cmd: "/plugin telegram" },
2940
- { patterns: ["verbinde.*github", "connect.*github", "github.*setup"], cmd: "/plugin github" },
2941
- { patterns: ["verbinde.*slack", "connect.*slack", "slack.*setup"], cmd: "/plugin slack" },
2942
- { patterns: ["verbinde.*docker", "connect.*docker", "docker.*setup"], cmd: "/plugin docker" },
2943
- { patterns: ["plugin.*install", "plugin.*hinzuf", "erweiterung"], cmd: "/plugin" },
2944
- { patterns: ["mcp.*server", "mcp.*install", "mcp.*einricht"], cmd: "/mcp" },
2945
- { patterns: ["welches model", "which model", "modell.*wechsel", "model.*switch", "anderes.*model"], cmd: "/model" },
2946
- { patterns: ["permission", "berechtigung", "zugriff", "erlaubnis"], cmd: "/permissions" },
2947
- { patterns: ["einstellung", "setting", "config", "konfigur"], cmd: "/config" },
2948
- { patterns: ["health.*check", "gesundheit", "status.*check", "alles.*ok", "laeuft.*alles"], cmd: "/health" },
2949
- { patterns: ["zeig.*skills", "was kannst", "show.*skills", "faehigkeit"], cmd: "/skills" },
2950
- { patterns: ["zeig.*agent", "list.*agent", "welche.*agent"], cmd: "/agents" },
2951
- { 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"; } },
2952
- { patterns: ["hilfe", "help", "was geht", "befehle"], cmd: "/help" },
2953
- { patterns: ["doctor", "diagnose", "problem.*check"], cmd: "/doctor" },
2954
- { patterns: ["login", "anmeld", "einlogg"], cmd: "/login" },
2955
- { patterns: ["logout", "abmeld", "auslogg"], cmd: "/logout" },
2956
- { patterns: ["komprimier", "compact", "zusammenfass"], cmd: "/compact" },
2957
- { patterns: ["kosten", "verbrauch", "cost", "tokens.*used"], cmd: "/cost" },
2958
- { patterns: ["update.*check", "neue.*version", "aktualisier"], cmd: "/versions" },
2959
- { patterns: ["speicher", "memory.*show", "was weisst.*du", "erinnerung"], cmd: "/memory" },
2960
- { patterns: ["hook", "webhook"], cmd: "/hooks" },
2961
- ];
2962
-
2963
- function detectIntent(text) {
2964
- var lower = text.toLowerCase();
2965
- // Skip if text is long (probably a real chat message)
2966
- if (text.length > 120) return null;
2967
- for (var i = 0; i < INTENTS.length; i++) {
2968
- var intent = INTENTS[i];
2969
- for (var j = 0; j < intent.patterns.length; j++) {
2970
- if (new RegExp(intent.patterns[j], "i").test(lower)) {
2971
- if (intent.extract) return intent.extract(text);
2972
- return intent.cmd;
2973
- }
2974
- }
2975
- }
2976
- return null;
2977
- }
2978
-
2979
- var inputQueue = [];
2980
-
2981
- async function processInput(input) {
2982
- if (processing) {
2983
- inputQueue.push(input);
2984
- return;
2985
- }
2986
- processing = true;
2987
- eraseUI();
2988
-
2989
- if (input.startsWith("!")) {
2990
- cmdShell(input.slice(1).trim());
2991
- } else if (input.startsWith("#save ")) {
2992
- var memParts = input.slice(6).split(/\s+/);
2993
- if (memParts[0] && memParts.length > 1) {
2994
- fs.writeFileSync(path.join(MEMORY_DIR, memParts[0] + ".txt"), memParts.slice(1).join(" "));
2995
- printSuccess("Memory saved: " + memParts[0]);
2996
- }
2997
- } else if (input.startsWith("#")) {
2998
- var memKey = input.slice(1).trim();
2999
- var memFile = path.join(MEMORY_DIR, memKey + ".txt");
3000
- if (fs.existsSync(memFile)) console.log(C.brightCyan + fs.readFileSync(memFile, "utf8") + C.reset);
3001
- else printError("Memory not found: " + memKey);
3002
- } else if (input.startsWith("/")) {
3003
- runHook("pre", input.split(/\s+/)[0].slice(1));
3004
- await handleCommand(input);
3005
- runHook("post", input.split(/\s+/)[0].slice(1));
3006
- } else {
3007
- // Intent detection — natural language → command
3008
- var detected = detectIntent(input);
3009
- if (detected) {
3010
- printInfo("→ " + detected);
3011
- runHook("pre", detected.split(/\s+/)[0].slice(1));
3012
- await handleCommand(detected);
3013
- runHook("post", detected.split(/\s+/)[0].slice(1));
3014
- } else if (sessionMode === "agent") {
3015
- // Agent mode: detect if question or task
3016
- var trimmed = input.trim();
3017
- var isChat = trimmed.length < 15 || // short messages = chat
3018
- /^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschüss|ciao)/i.test(trimmed) ||
3019
- /^(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) ||
3020
- /\?$/.test(trimmed);
3021
- if (isChat) {
3022
- await sendChat(input);
3023
- } else {
3024
- await cmdAgent(input);
3025
- }
3026
- } else {
3027
- await sendChat(input);
3028
- }
3029
- }
3030
-
3031
- inputBuffer = "";
3032
- cursorPos = 0;
3033
- menuSelected = 0;
3034
- uiStartRow = -1;
3035
- processing = false;
3036
- drawPrompt();
3037
-
3038
- // Process queued inputs
3039
- if (inputQueue.length > 0) {
3040
- var next = inputQueue.shift();
3041
- inputHistory.unshift(next);
3042
- processInput(next);
3043
- }
3044
- }
3045
-
3046
- // ── Raw Input Handler ──
3047
- process.stdin.setRawMode(true);
3048
- process.stdin.resume();
3049
- process.stdin.setEncoding("utf8");
3050
-
3051
- drawPrompt();
3052
-
3053
- process.stdin.on("data", function(key) {
3054
- // Ctrl+C / Ctrl+D — always allow exit
3055
- if (key === "\x03" || key === "\x04") {
3056
- session.history = chatHistory.slice(-50);
3057
- saveSessionHistory(session);
3058
- // Save last workdir for next session
3059
- try { fs.writeFileSync(path.join(CONFIG_DIR, "last-workdir.json"), JSON.stringify({ dir: config.workdir, ts: new Date().toISOString() })); } catch(e) {}
3060
- eraseUI();
3061
- console.log(C.dim + "\nBye." + C.reset);
3062
- process.exit(0);
3063
- }
3064
-
3065
- // Enter
3066
- if (key === "\r" || key === "\n") {
3067
- var input = inputBuffer.trim();
3068
- if (menuVisible && menuItems.length > 0 && inputBuffer.startsWith("/") && !inputBuffer.includes(" ")) {
3069
- input = menuItems[menuSelected].cmd;
3070
- }
3071
- if (!input) return;
3072
- inputHistory.unshift(input);
3073
- historyIdx = -1;
3074
- processInput(input);
3075
- return;
3076
- }
3077
-
3078
- // Ctrl+V — paste (check clipboard for image)
3079
- if (key === "\x16") {
3080
- (async function() {
3081
- try {
3082
- var imgPath = path.join(os.tmpdir(), "blun_paste_" + Date.now() + ".png");
3083
- var isWin = process.platform === "win32";
3084
- if (isWin) {
3085
- var psCmd = "powershell -NoProfile -Command \"$img = Get-Clipboard -Format Image; if($img){ $img.Save('" + imgPath.replace(/\\/g, "\\\\") + "'); Write-Output 'OK' } else { Write-Output 'NO' }\"";
3086
- var psResult = require("child_process").execSync(psCmd, { encoding: "utf8", timeout: 5000 }).trim();
3087
- if (psResult === "OK" && fs.existsSync(imgPath)) {
3088
- eraseUI();
3089
- printInfo("Image pasted from clipboard");
3090
- var imgData = fs.readFileSync(imgPath).toString("base64");
3091
- var prompt = inputBuffer.trim() || "Describe this image";
3092
- processing = true;
3093
- var resp = await apiCall("POST", "/chat", {
3094
- message: prompt,
3095
- image: imgData,
3096
- history: chatHistory.slice(-10)
3097
- });
3098
- if (resp.status === 200) {
3099
- chatHistory.push({ role: "user", content: "[image] " + prompt });
3100
- chatHistory.push({ role: "assistant", content: resp.data.answer });
3101
- printAnswer(resp.data.answer, "vision");
3102
- } else {
3103
- printError(resp.data.error || "Vision error");
3104
- }
3105
- inputBuffer = "";
3106
- cursorPos = 0;
3107
- processing = false;
3108
- drawPrompt();
3109
- try { fs.unlinkSync(imgPath); } catch(e) {}
3110
- return;
3111
- }
3112
- }
3113
- // No image — just paste text from clipboard
3114
- if (isWin) {
3115
- var clipText = require("child_process").execSync("powershell -NoProfile -Command Get-Clipboard", { encoding: "utf8", timeout: 3000 }).trim();
3116
- if (clipText) {
3117
- inputBuffer += clipText;
3118
- cursorPos = inputBuffer.length;
3119
- refreshUI();
3120
- }
3121
- }
3122
- } catch(e) {
3123
- // Fallback: ignore paste errors
3124
- }
3125
- })();
3126
- return;
3127
- }
3128
-
3129
- // Tab — autocomplete from menu
3130
- if (key === "\t") {
3131
- if (menuVisible && menuItems.length > 0) {
3132
- inputBuffer = menuItems[menuSelected].cmd + " ";
3133
- cursorPos = inputBuffer.length;
3134
- refreshUI();
3135
- }
3136
- return;
3137
- }
3138
-
3139
- // Backspace
3140
- if (key === "\x7f" || key === "\b") {
3141
- if (inputBuffer.length > 0) {
3142
- inputBuffer = inputBuffer.slice(0, -1);
3143
- cursorPos = inputBuffer.length;
3144
- menuSelected = 0;
3145
- refreshUI();
3146
- }
3147
- return;
3148
- }
3149
-
3150
- // Arrow keys
3151
- if (key === "\x1b[A") { // Up
3152
- if (menuVisible && menuItems.length > 0) {
3153
- menuSelected = Math.max(0, menuSelected - 1);
3154
- refreshUI();
3155
- } else if (inputHistory.length > 0) {
3156
- historyIdx = Math.min(historyIdx + 1, inputHistory.length - 1);
3157
- inputBuffer = inputHistory[historyIdx];
3158
- cursorPos = inputBuffer.length;
3159
- refreshUI();
3160
- }
3161
- return;
3162
- }
3163
- if (key === "\x1b[B") { // Down
3164
- if (menuVisible && menuItems.length > 0) {
3165
- menuSelected = Math.min(menuItems.length - 1, menuSelected + 1);
3166
- refreshUI();
3167
- } else if (historyIdx > 0) {
3168
- historyIdx--;
3169
- inputBuffer = inputHistory[historyIdx];
3170
- cursorPos = inputBuffer.length;
3171
- refreshUI();
3172
- }
3173
- return;
3174
- }
3175
-
3176
- // Escape — close menu
3177
- if (key === "\x1b" || key === "\x1b\x1b") {
3178
- if (menuVisible) {
3179
- menuVisible = false;
3180
- refreshUI();
3181
- }
3182
- return;
3183
- }
3184
-
3185
- // Ignore other escape sequences
3186
- if (key.startsWith("\x1b")) return;
3187
-
3188
- // Regular character
3189
- inputBuffer += key;
3190
- cursorPos = inputBuffer.length;
3191
- menuSelected = 0;
3192
- refreshUI();
3193
- });
3194
- }
3195
-
3196
- main().catch(console.error);