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.
- package/api.js +965 -0
- package/blun-cli.js +820 -0
- package/blunking-api.js +7 -0
- package/bot.js +188 -0
- package/browser-controller.js +76 -0
- package/chat-memory.js +103 -0
- package/file-helper.js +63 -0
- package/fuzzy-match.js +78 -0
- package/identities.js +106 -0
- package/installer.js +160 -0
- package/job-manager.js +146 -0
- package/local-data.js +71 -0
- package/message-builder.js +28 -0
- package/noisy-evals.js +38 -0
- package/package.json +17 -4
- package/palace-memory.js +246 -0
- package/reference-inspector.js +228 -0
- package/runtime.js +555 -0
- package/task-executor.js +104 -0
- package/tests/browser-controller.test.js +42 -0
- package/tests/cli.test.js +93 -0
- package/tests/file-helper.test.js +18 -0
- package/tests/installer.test.js +39 -0
- package/tests/job-manager.test.js +99 -0
- package/tests/merge-compat.test.js +77 -0
- package/tests/messages.test.js +23 -0
- package/tests/noisy-evals.test.js +12 -0
- package/tests/noisy-intent-corpus.test.js +45 -0
- package/tests/reference-inspector.test.js +36 -0
- package/tests/runtime.test.js +119 -0
- package/tests/task-executor.test.js +40 -0
- package/tests/tools.test.js +23 -0
- package/tests/user-profile.test.js +66 -0
- package/tests/website-builder.test.js +66 -0
- package/tmp-build-smoke/nicrazy-landing/index.html +53 -0
- package/tmp-build-smoke/nicrazy-landing/style.css +110 -0
- package/tmp-shot-smoke/website-shot-1776006760424.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007850007.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007886209.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007903766.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008737117.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008988859.png +0 -0
- package/tmp-smoke/nicrazy-landing/index.html +66 -0
- package/tmp-smoke/nicrazy-landing/style.css +104 -0
- package/tools.js +177 -0
- package/user-profile.js +395 -0
- package/website-builder.js +394 -0
- package/website-shot-1776010648230.png +0 -0
- package/website_builder.txt +38 -0
- package/bin/blun.js +0 -3196
- 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);
|