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