blun-king-cli 1.2.0 → 1.4.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 +938 -7
- package/package.json +1 -1
package/bin/blun.js
CHANGED
|
@@ -108,7 +108,7 @@ function printHeader() {
|
|
|
108
108
|
var line = BOX.h.repeat(w - 4);
|
|
109
109
|
console.log("");
|
|
110
110
|
console.log(C.brightBlue + " " + BOX.tl + line + BOX.tr + C.reset);
|
|
111
|
-
console.log(C.brightBlue + " " + BOX.v + C.reset + C.bold + C.brightWhite + " " + BOX.bot + " BLUN KING CLI" + C.reset + C.dim + " v1.
|
|
111
|
+
console.log(C.brightBlue + " " + BOX.v + C.reset + C.bold + C.brightWhite + " " + BOX.bot + " BLUN KING CLI" + C.reset + C.dim + " v1.4" + C.reset + " ".repeat(Math.max(0, w - 24)) + C.brightBlue + BOX.v + C.reset);
|
|
112
112
|
console.log(C.brightBlue + " " + BOX.v + C.reset + C.gray + " Premium KI — Local First — Autonom" + C.reset + " ".repeat(Math.max(0, w - 41)) + C.brightBlue + BOX.v + C.reset);
|
|
113
113
|
console.log(C.brightBlue + " " + BOX.v + line + BOX.v + C.reset);
|
|
114
114
|
console.log(C.brightBlue + " " + BOX.v + C.reset + C.gray + " API " + C.brightCyan + config.api.base_url + C.reset + " ".repeat(Math.max(0, w - 14 - config.api.base_url.length)) + C.brightBlue + BOX.v + C.reset);
|
|
@@ -121,6 +121,20 @@ function printHeader() {
|
|
|
121
121
|
console.log("");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ── Update Checker ──
|
|
125
|
+
function checkForUpdates() {
|
|
126
|
+
try {
|
|
127
|
+
var currentVersion = "1.4.0";
|
|
128
|
+
try { currentVersion = require(path.join(__dirname, "..", "package.json")).version; } catch(e) {}
|
|
129
|
+
var latest = execSync("npm view blun-king-cli version 2>/dev/null", { encoding: "utf8", timeout: 5000 }).trim();
|
|
130
|
+
if (latest && latest !== currentVersion) {
|
|
131
|
+
console.log(C.yellow + C.bold + " " + BOX.arrow + " Update available: " + currentVersion + " → " + latest + C.reset);
|
|
132
|
+
console.log(C.yellow + " npm update -g blun-king-cli" + C.reset);
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
} catch(e) { /* silent */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
function printAnswer(answer, meta) {
|
|
125
139
|
console.log("");
|
|
126
140
|
console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset + (meta ? C.gray + " " + BOX.dot + " " + meta + C.reset : ""));
|
|
@@ -199,6 +213,16 @@ async function handleCommand(input) {
|
|
|
199
213
|
console.log(" /score <text> Score a response");
|
|
200
214
|
console.log(" /eval Run eval suite");
|
|
201
215
|
console.log("");
|
|
216
|
+
console.log(C.yellow + " PLUGINS (MCP)" + C.reset);
|
|
217
|
+
console.log(" /plugin list List installed plugins");
|
|
218
|
+
console.log(" /plugin add <cmd> Add MCP server (npm pkg or local path)");
|
|
219
|
+
console.log(" /plugin remove <n>Remove a plugin");
|
|
220
|
+
console.log(" /plugin run <n> Start a plugin server");
|
|
221
|
+
console.log(" /plugin stop <n> Stop a plugin server");
|
|
222
|
+
console.log(" /permissions Show permission settings");
|
|
223
|
+
console.log(" /permissions allow-all Allow all tool calls");
|
|
224
|
+
console.log(" /permissions ask Ask before each tool call");
|
|
225
|
+
console.log("");
|
|
202
226
|
console.log(C.yellow + " SETTINGS" + C.reset);
|
|
203
227
|
console.log(" /settings Show all settings");
|
|
204
228
|
console.log(" /set auth api Switch to API key auth");
|
|
@@ -218,18 +242,45 @@ async function handleCommand(input) {
|
|
|
218
242
|
console.log(" /write <file> Write/create a file");
|
|
219
243
|
console.log(" /init Init project (AGENT.md, .gitignore, git init)");
|
|
220
244
|
console.log("");
|
|
245
|
+
console.log(C.yellow + " CONFIG" + C.reset);
|
|
246
|
+
console.log(" /config list List all settings (all scopes)");
|
|
247
|
+
console.log(" /config get <key> Get a setting value");
|
|
248
|
+
console.log(" /config set <k> <v> Set a value [--global|--project|--local]");
|
|
249
|
+
console.log(" /config add <k> <v> Add to array setting");
|
|
250
|
+
console.log(" /config remove <k> Remove setting");
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(C.yellow + " HOOKS" + C.reset);
|
|
253
|
+
console.log(" /hooks list List all hooks");
|
|
254
|
+
console.log(" /hooks add <trigger> <cmd> Add hook (pre:chat, post:deploy...)");
|
|
255
|
+
console.log(" /hooks remove <trigger> Remove hook");
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log(C.yellow + " AGENTS" + C.reset);
|
|
258
|
+
console.log(" /agents list List subagents");
|
|
259
|
+
console.log(" /agents create <n> Create new agent");
|
|
260
|
+
console.log(" /agents run <n> <task> Run agent with task");
|
|
261
|
+
console.log(" /agents info <n> Show agent details");
|
|
262
|
+
console.log("");
|
|
221
263
|
console.log(C.yellow + " SYSTEM" + C.reset);
|
|
222
264
|
console.log(" /status Runtime status");
|
|
223
265
|
console.log(" /versions Prompt versions");
|
|
224
266
|
console.log(" /health Quick health check");
|
|
267
|
+
console.log(" /doctor Full system diagnostics");
|
|
268
|
+
console.log(" /model [name] Show/switch model");
|
|
269
|
+
console.log(" /cost Session cost estimate");
|
|
270
|
+
console.log(" /compact Clear old context");
|
|
271
|
+
console.log(" /review Review current git diff");
|
|
272
|
+
console.log(" /login key <k> Login with API key");
|
|
273
|
+
console.log(" /logout Logout");
|
|
225
274
|
console.log(" /watchdog Watchdog status");
|
|
226
|
-
console.log(" /watchdog on/off Enable/disable watchdog");
|
|
227
275
|
console.log(" /memory Show local memory");
|
|
228
|
-
console.log(" /memory save <k> Save to memory");
|
|
229
|
-
console.log(" /memory del <k> Delete from memory");
|
|
230
276
|
console.log(" /files List workdir files");
|
|
231
277
|
console.log(" /exit Exit CLI");
|
|
232
278
|
console.log("");
|
|
279
|
+
console.log(C.yellow + " SHORTCUTS" + C.reset);
|
|
280
|
+
console.log(" !<cmd> Run bash command directly");
|
|
281
|
+
console.log(" #<key> Read memory entry");
|
|
282
|
+
console.log(" #save <k> <v> Save to memory");
|
|
283
|
+
console.log("");
|
|
233
284
|
break;
|
|
234
285
|
|
|
235
286
|
case "/clear":
|
|
@@ -333,6 +384,67 @@ async function handleCommand(input) {
|
|
|
333
384
|
cmdInit();
|
|
334
385
|
break;
|
|
335
386
|
|
|
387
|
+
case "/plugin":
|
|
388
|
+
case "/mcp":
|
|
389
|
+
cmdPlugin(args);
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
case "/permissions":
|
|
393
|
+
cmdPermissions(args);
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case "/config":
|
|
397
|
+
cmdConfig(args);
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case "/hooks":
|
|
401
|
+
cmdHooks(args);
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case "/agents":
|
|
405
|
+
cmdAgents(args);
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case "/doctor":
|
|
409
|
+
await cmdDoctor();
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case "/model":
|
|
413
|
+
cmdModel(args);
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case "/login":
|
|
417
|
+
cmdLogin(args);
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case "/logout":
|
|
421
|
+
cmdLogout();
|
|
422
|
+
break;
|
|
423
|
+
|
|
424
|
+
case "/compact":
|
|
425
|
+
cmdCompact();
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case "/review":
|
|
429
|
+
await cmdReview();
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case "/cost":
|
|
433
|
+
cmdCost();
|
|
434
|
+
break;
|
|
435
|
+
|
|
436
|
+
case "/agent":
|
|
437
|
+
await cmdAgent(args);
|
|
438
|
+
break;
|
|
439
|
+
|
|
440
|
+
case "/screenshot":
|
|
441
|
+
await cmdScreenshot(args);
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case "/render":
|
|
445
|
+
await cmdRender(args);
|
|
446
|
+
break;
|
|
447
|
+
|
|
336
448
|
case "/exit":
|
|
337
449
|
case "/quit":
|
|
338
450
|
case "/q":
|
|
@@ -1072,6 +1184,777 @@ function cmdInit(args) {
|
|
|
1072
1184
|
}
|
|
1073
1185
|
}
|
|
1074
1186
|
|
|
1187
|
+
// ── Plugins (MCP-style) ──
|
|
1188
|
+
const PLUGINS_FILE = path.join(CONFIG_DIR, "plugins.json");
|
|
1189
|
+
const PERMISSIONS_FILE = path.join(CONFIG_DIR, "permissions.json");
|
|
1190
|
+
|
|
1191
|
+
function loadPlugins() {
|
|
1192
|
+
if (fs.existsSync(PLUGINS_FILE)) {
|
|
1193
|
+
try { return JSON.parse(fs.readFileSync(PLUGINS_FILE, "utf8")); } catch(e) {}
|
|
1194
|
+
}
|
|
1195
|
+
return {};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function savePlugins(p) { fs.writeFileSync(PLUGINS_FILE, JSON.stringify(p, null, 2)); }
|
|
1199
|
+
|
|
1200
|
+
function loadPermissions() {
|
|
1201
|
+
if (fs.existsSync(PERMISSIONS_FILE)) {
|
|
1202
|
+
try { return JSON.parse(fs.readFileSync(PERMISSIONS_FILE, "utf8")); } catch(e) {}
|
|
1203
|
+
}
|
|
1204
|
+
return { mode: "ask", allowed: [], denied: [] };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function savePermissions(p) { fs.writeFileSync(PERMISSIONS_FILE, JSON.stringify(p, null, 2)); }
|
|
1208
|
+
|
|
1209
|
+
// Built-in plugin templates
|
|
1210
|
+
var PLUGIN_TEMPLATES = {
|
|
1211
|
+
telegram: {
|
|
1212
|
+
name: "telegram",
|
|
1213
|
+
description: "Telegram Bot Bridge — send/receive messages",
|
|
1214
|
+
type: "builtin",
|
|
1215
|
+
config: { bot_token: "", chat_id: "" },
|
|
1216
|
+
commands: ["/tg send <msg>", "/tg status"],
|
|
1217
|
+
setup: ["bot_token", "chat_id"]
|
|
1218
|
+
},
|
|
1219
|
+
github: {
|
|
1220
|
+
name: "github",
|
|
1221
|
+
description: "GitHub Integration — repos, issues, PRs",
|
|
1222
|
+
type: "builtin",
|
|
1223
|
+
config: { token: "", default_repo: "" },
|
|
1224
|
+
commands: ["/gh repos", "/gh issues", "/gh pr"],
|
|
1225
|
+
setup: ["token"]
|
|
1226
|
+
},
|
|
1227
|
+
browser: {
|
|
1228
|
+
name: "browser",
|
|
1229
|
+
description: "Playwright Browser — screenshots, rendering, scraping",
|
|
1230
|
+
type: "builtin",
|
|
1231
|
+
config: {},
|
|
1232
|
+
commands: ["/screenshot <url>", "/render <url>"],
|
|
1233
|
+
setup: []
|
|
1234
|
+
},
|
|
1235
|
+
slack: {
|
|
1236
|
+
name: "slack",
|
|
1237
|
+
description: "Slack Webhook — send messages to channels",
|
|
1238
|
+
type: "builtin",
|
|
1239
|
+
config: { webhook_url: "" },
|
|
1240
|
+
commands: ["/slack send <msg>"],
|
|
1241
|
+
setup: ["webhook_url"]
|
|
1242
|
+
},
|
|
1243
|
+
docker: {
|
|
1244
|
+
name: "docker",
|
|
1245
|
+
description: "Docker Management — containers, images, logs",
|
|
1246
|
+
type: "builtin",
|
|
1247
|
+
config: { host: "localhost" },
|
|
1248
|
+
commands: ["/docker ps", "/docker logs <container>"],
|
|
1249
|
+
setup: []
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
function cmdPlugin(args) {
|
|
1254
|
+
var plugins = loadPlugins();
|
|
1255
|
+
var parts = (args || "").split(/\s+/);
|
|
1256
|
+
var action = parts[0] || "list";
|
|
1257
|
+
|
|
1258
|
+
if (action === "list") {
|
|
1259
|
+
console.log("");
|
|
1260
|
+
console.log(C.bold + " " + BOX.bot + " Installed Plugins:" + C.reset);
|
|
1261
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
1262
|
+
var names = Object.keys(plugins);
|
|
1263
|
+
if (names.length === 0) {
|
|
1264
|
+
console.log(C.gray + " (none installed)" + C.reset);
|
|
1265
|
+
} else {
|
|
1266
|
+
names.forEach(function(name) {
|
|
1267
|
+
var p = plugins[name];
|
|
1268
|
+
var status = p.running ? C.green + BOX.dot + " running" : C.gray + BOX.dot + " stopped";
|
|
1269
|
+
console.log(" " + C.brightCyan + name + C.reset + " — " + (p.description || "") + " " + status + C.reset);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
console.log("");
|
|
1273
|
+
console.log(C.gray + " Available: " + Object.keys(PLUGIN_TEMPLATES).join(", ") + C.reset);
|
|
1274
|
+
console.log(C.gray + " Or add custom: /plugin add <npm-package> or /plugin add <path>" + C.reset);
|
|
1275
|
+
console.log("");
|
|
1276
|
+
|
|
1277
|
+
} else if (action === "add" && parts[1]) {
|
|
1278
|
+
var pluginName = parts[1];
|
|
1279
|
+
if (PLUGIN_TEMPLATES[pluginName]) {
|
|
1280
|
+
// Built-in plugin
|
|
1281
|
+
var tmpl = PLUGIN_TEMPLATES[pluginName];
|
|
1282
|
+
plugins[pluginName] = {
|
|
1283
|
+
name: tmpl.name,
|
|
1284
|
+
description: tmpl.description,
|
|
1285
|
+
type: tmpl.type,
|
|
1286
|
+
config: Object.assign({}, tmpl.config),
|
|
1287
|
+
commands: tmpl.commands,
|
|
1288
|
+
installed: new Date().toISOString(),
|
|
1289
|
+
running: false
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// If setup fields needed, prompt
|
|
1293
|
+
if (tmpl.setup && tmpl.setup.length > 0) {
|
|
1294
|
+
printInfo("Plugin '" + pluginName + "' needs configuration:");
|
|
1295
|
+
tmpl.setup.forEach(function(field) {
|
|
1296
|
+
printInfo(" Set with: /set plugin." + pluginName + "." + field + " <value>");
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
savePlugins(plugins);
|
|
1301
|
+
printSuccess("Plugin '" + pluginName + "' installed!");
|
|
1302
|
+
} else {
|
|
1303
|
+
// Custom: npm package or local path
|
|
1304
|
+
printInfo("Installing custom plugin: " + pluginName);
|
|
1305
|
+
try {
|
|
1306
|
+
execSync("npm install " + pluginName, { cwd: CONFIG_DIR, encoding: "utf8", stdio: "pipe" });
|
|
1307
|
+
plugins[pluginName] = {
|
|
1308
|
+
name: pluginName,
|
|
1309
|
+
description: "Custom MCP server",
|
|
1310
|
+
type: "npm",
|
|
1311
|
+
config: {},
|
|
1312
|
+
commands: [],
|
|
1313
|
+
installed: new Date().toISOString(),
|
|
1314
|
+
running: false
|
|
1315
|
+
};
|
|
1316
|
+
savePlugins(plugins);
|
|
1317
|
+
printSuccess("Plugin '" + pluginName + "' installed via npm!");
|
|
1318
|
+
} catch(e) {
|
|
1319
|
+
printError("Install failed: " + e.message.slice(0, 200));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
} else if (action === "remove" && parts[1]) {
|
|
1324
|
+
delete plugins[parts[1]];
|
|
1325
|
+
savePlugins(plugins);
|
|
1326
|
+
printSuccess("Plugin '" + parts[1] + "' removed.");
|
|
1327
|
+
|
|
1328
|
+
} else if (action === "run" && parts[1]) {
|
|
1329
|
+
if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
|
|
1330
|
+
plugins[parts[1]].running = true;
|
|
1331
|
+
savePlugins(plugins);
|
|
1332
|
+
printSuccess("Plugin '" + parts[1] + "' started.");
|
|
1333
|
+
|
|
1334
|
+
} else if (action === "stop" && parts[1]) {
|
|
1335
|
+
if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
|
|
1336
|
+
plugins[parts[1]].running = false;
|
|
1337
|
+
savePlugins(plugins);
|
|
1338
|
+
printSuccess("Plugin '" + parts[1] + "' stopped.");
|
|
1339
|
+
|
|
1340
|
+
} else {
|
|
1341
|
+
printError("Usage: /plugin list|add|remove|run|stop <name>");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function cmdPermissions(args) {
|
|
1346
|
+
var perms = loadPermissions();
|
|
1347
|
+
if (!args) {
|
|
1348
|
+
console.log("");
|
|
1349
|
+
console.log(C.bold + " Permissions:" + C.reset);
|
|
1350
|
+
console.log(" Mode: " + C.brightCyan + perms.mode + C.reset);
|
|
1351
|
+
console.log(" Allowed: " + (perms.allowed.length > 0 ? perms.allowed.join(", ") : C.gray + "(none)" + C.reset));
|
|
1352
|
+
console.log(" Denied: " + (perms.denied.length > 0 ? perms.denied.join(", ") : C.gray + "(none)" + C.reset));
|
|
1353
|
+
console.log("");
|
|
1354
|
+
console.log(C.gray + " /permissions allow-all — Skip all permission prompts" + C.reset);
|
|
1355
|
+
console.log(C.gray + " /permissions ask — Ask before each tool call" + C.reset);
|
|
1356
|
+
console.log("");
|
|
1357
|
+
} else if (args === "allow-all") {
|
|
1358
|
+
perms.mode = "allow-all";
|
|
1359
|
+
savePermissions(perms);
|
|
1360
|
+
printSuccess("Permission mode: allow-all (no prompts)");
|
|
1361
|
+
} else if (args === "ask") {
|
|
1362
|
+
perms.mode = "ask";
|
|
1363
|
+
savePermissions(perms);
|
|
1364
|
+
printSuccess("Permission mode: ask (prompt before each tool call)");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ── Agent Loop (CLI wrapper) ──
|
|
1369
|
+
async function cmdAgent(args) {
|
|
1370
|
+
if (!args) { printError("Usage: /agent <goal>"); return; }
|
|
1371
|
+
try {
|
|
1372
|
+
printUserMessage(args);
|
|
1373
|
+
process.stdout.write(C.dim + " " + BOX.bot + " working autonomously..." + C.reset);
|
|
1374
|
+
var resp = await apiCall("POST", "/agent", { goal: args, verbose: true });
|
|
1375
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
1376
|
+
if (resp.status !== 200) { printError(resp.data.error || "Error"); return; }
|
|
1377
|
+
var d = resp.data;
|
|
1378
|
+
var meta = d.steps_executed + " steps" + (d.files.length > 0 ? " " + BOX.dot + " " + d.files.length + " files" : "");
|
|
1379
|
+
printAnswer(d.answer, meta);
|
|
1380
|
+
if (d.files.length > 0) {
|
|
1381
|
+
console.log(C.green + " Files:" + C.reset);
|
|
1382
|
+
d.files.forEach(function(f) { console.log(" " + C.brightCyan + f.name + C.reset + " → " + config.api.base_url + f.download); });
|
|
1383
|
+
console.log("");
|
|
1384
|
+
}
|
|
1385
|
+
} catch(e) { printError(e.message); }
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ── Screenshot/Render (CLI wrapper) ──
|
|
1389
|
+
async function cmdScreenshot(args) {
|
|
1390
|
+
if (!args) { printError("Usage: /screenshot <url>"); return; }
|
|
1391
|
+
try {
|
|
1392
|
+
printInfo("Taking screenshot of " + args + "...");
|
|
1393
|
+
var resp = await apiCall("POST", "/screenshot", { url: args });
|
|
1394
|
+
if (resp.status !== 200) { printError(resp.data.error || "Error"); return; }
|
|
1395
|
+
printSuccess("Screenshot: " + resp.data.title);
|
|
1396
|
+
console.log(" Download: " + C.brightCyan + config.api.base_url + resp.data.screenshot + C.reset);
|
|
1397
|
+
console.log("");
|
|
1398
|
+
} catch(e) { printError(e.message); }
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function cmdRender(args) {
|
|
1402
|
+
if (!args) { printError("Usage: /render <url>"); return; }
|
|
1403
|
+
try {
|
|
1404
|
+
printInfo("Rendering " + args + " with Playwright...");
|
|
1405
|
+
var resp = await apiCall("POST", "/render", { url: args });
|
|
1406
|
+
if (resp.status !== 200) { printError(resp.data.error || "Error"); return; }
|
|
1407
|
+
printSuccess("Rendered: " + resp.data.title + " (" + resp.data.html_length + " chars)");
|
|
1408
|
+
if (resp.data.screenshot) console.log(" Screenshot: " + C.brightCyan + resp.data.screenshot + C.reset);
|
|
1409
|
+
console.log("");
|
|
1410
|
+
} catch(e) { printError(e.message); }
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// ══════════════════════════════════════════════════
|
|
1414
|
+
// ── 1. SETTINGS REGISTRY (Multi-Scope) ──
|
|
1415
|
+
// ══════════════════════════════════════════════════
|
|
1416
|
+
// Scopes: Managed > CLI Args > Local > Project > User
|
|
1417
|
+
const SETTINGS_SCOPES = ["managed", "cli", "local", "project", "user"];
|
|
1418
|
+
const USER_SETTINGS_FILE = path.join(CONFIG_DIR, "settings.json");
|
|
1419
|
+
|
|
1420
|
+
function findProjectRoot() {
|
|
1421
|
+
var dir = config.workdir;
|
|
1422
|
+
for (var i = 0; i < 10; i++) {
|
|
1423
|
+
if (fs.existsSync(path.join(dir, ".blun"))) return dir;
|
|
1424
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
1425
|
+
var parent = path.dirname(dir);
|
|
1426
|
+
if (parent === dir) break;
|
|
1427
|
+
dir = parent;
|
|
1428
|
+
}
|
|
1429
|
+
return config.workdir;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function loadScopedSettings(scope) {
|
|
1433
|
+
var file;
|
|
1434
|
+
if (scope === "user") file = USER_SETTINGS_FILE;
|
|
1435
|
+
else if (scope === "project") file = path.join(findProjectRoot(), ".blun", "settings.json");
|
|
1436
|
+
else if (scope === "local") file = path.join(findProjectRoot(), ".blun", "settings.local.json");
|
|
1437
|
+
else if (scope === "managed") file = path.join(CONFIG_DIR, "managed-settings.json");
|
|
1438
|
+
else return {};
|
|
1439
|
+
if (file && fs.existsSync(file)) {
|
|
1440
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch(e) {}
|
|
1441
|
+
}
|
|
1442
|
+
return {};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function saveScopedSettings(scope, data) {
|
|
1446
|
+
var file;
|
|
1447
|
+
if (scope === "user") file = USER_SETTINGS_FILE;
|
|
1448
|
+
else if (scope === "project") {
|
|
1449
|
+
var projDir = path.join(findProjectRoot(), ".blun");
|
|
1450
|
+
if (!fs.existsSync(projDir)) fs.mkdirSync(projDir, { recursive: true });
|
|
1451
|
+
file = path.join(projDir, "settings.json");
|
|
1452
|
+
} else if (scope === "local") {
|
|
1453
|
+
var projDir2 = path.join(findProjectRoot(), ".blun");
|
|
1454
|
+
if (!fs.existsSync(projDir2)) fs.mkdirSync(projDir2, { recursive: true });
|
|
1455
|
+
file = path.join(projDir2, "settings.local.json");
|
|
1456
|
+
} else if (scope === "managed") file = path.join(CONFIG_DIR, "managed-settings.json");
|
|
1457
|
+
else return;
|
|
1458
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Resolve setting: highest priority scope wins
|
|
1462
|
+
function getSetting(key) {
|
|
1463
|
+
for (var i = 0; i < SETTINGS_SCOPES.length; i++) {
|
|
1464
|
+
var s = loadScopedSettings(SETTINGS_SCOPES[i]);
|
|
1465
|
+
if (s[key] !== undefined) return { value: s[key], scope: SETTINGS_SCOPES[i] };
|
|
1466
|
+
}
|
|
1467
|
+
return { value: undefined, scope: null };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// All settings keys with defaults
|
|
1471
|
+
var SETTINGS_DEFAULTS = {
|
|
1472
|
+
model: "blun-king-v100", language: "de", theme: "dark", outputStyle: "markdown",
|
|
1473
|
+
effortLevel: "normal", fastMode: false, alwaysThinkingEnabled: false,
|
|
1474
|
+
showThinkingSummaries: false, verbose: false, autoMode: false,
|
|
1475
|
+
respectGitignore: true, includeCoAuthoredBy: true, cleanupPeriodDays: 30,
|
|
1476
|
+
prefersReducedMotion: false, channelsEnabled: true,
|
|
1477
|
+
"sandbox.enabled": false, "sandbox.autoAllowBashIfSandboxed": true,
|
|
1478
|
+
"permissions.defaultMode": "ask",
|
|
1479
|
+
"attribution.commit": true, "attribution.pr": true
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
function cmdConfig(args) {
|
|
1483
|
+
var parts = (args || "").split(/\s+/);
|
|
1484
|
+
var action = parts[0] || "list";
|
|
1485
|
+
var scope = "user";
|
|
1486
|
+
|
|
1487
|
+
// Check for --global flag
|
|
1488
|
+
var globalIdx = parts.indexOf("--global");
|
|
1489
|
+
if (globalIdx !== -1) { scope = "user"; parts.splice(globalIdx, 1); }
|
|
1490
|
+
var projectIdx = parts.indexOf("--project");
|
|
1491
|
+
if (projectIdx !== -1) { scope = "project"; parts.splice(projectIdx, 1); }
|
|
1492
|
+
var localIdx = parts.indexOf("--local");
|
|
1493
|
+
if (localIdx !== -1) { scope = "local"; parts.splice(localIdx, 1); }
|
|
1494
|
+
|
|
1495
|
+
action = parts[0] || "list";
|
|
1496
|
+
var key = parts[1];
|
|
1497
|
+
var value = parts.slice(2).join(" ");
|
|
1498
|
+
|
|
1499
|
+
if (action === "list") {
|
|
1500
|
+
console.log("");
|
|
1501
|
+
console.log(C.bold + " Settings Registry:" + C.reset);
|
|
1502
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(50) + C.reset);
|
|
1503
|
+
var allKeys = Object.keys(SETTINGS_DEFAULTS);
|
|
1504
|
+
allKeys.forEach(function(k) {
|
|
1505
|
+
var resolved = getSetting(k);
|
|
1506
|
+
var val = resolved.value !== undefined ? resolved.value : SETTINGS_DEFAULTS[k];
|
|
1507
|
+
var src = resolved.scope || "default";
|
|
1508
|
+
console.log(" " + C.gray + k + C.reset + " = " + C.brightCyan + JSON.stringify(val) + C.reset + C.dim + " (" + src + ")" + C.reset);
|
|
1509
|
+
});
|
|
1510
|
+
// Also show custom keys from all scopes
|
|
1511
|
+
SETTINGS_SCOPES.forEach(function(sc) {
|
|
1512
|
+
var s = loadScopedSettings(sc);
|
|
1513
|
+
Object.keys(s).forEach(function(k) {
|
|
1514
|
+
if (!SETTINGS_DEFAULTS.hasOwnProperty(k)) {
|
|
1515
|
+
console.log(" " + C.yellow + k + C.reset + " = " + C.brightCyan + JSON.stringify(s[k]) + C.reset + C.dim + " (" + sc + ")" + C.reset);
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
console.log("");
|
|
1520
|
+
|
|
1521
|
+
} else if (action === "get" && key) {
|
|
1522
|
+
var resolved = getSetting(key);
|
|
1523
|
+
if (resolved.value !== undefined) {
|
|
1524
|
+
console.log(C.brightCyan + JSON.stringify(resolved.value) + C.reset + C.dim + " (from: " + resolved.scope + ")" + C.reset);
|
|
1525
|
+
} else if (SETTINGS_DEFAULTS[key] !== undefined) {
|
|
1526
|
+
console.log(C.brightCyan + JSON.stringify(SETTINGS_DEFAULTS[key]) + C.reset + C.dim + " (default)" + C.reset);
|
|
1527
|
+
} else {
|
|
1528
|
+
printInfo("Not set: " + key);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
} else if (action === "set" && key) {
|
|
1532
|
+
var s = loadScopedSettings(scope);
|
|
1533
|
+
// Auto-parse value
|
|
1534
|
+
var parsed = value;
|
|
1535
|
+
if (value === "true") parsed = true;
|
|
1536
|
+
else if (value === "false") parsed = false;
|
|
1537
|
+
else if (/^\d+$/.test(value)) parsed = parseInt(value);
|
|
1538
|
+
s[key] = parsed;
|
|
1539
|
+
saveScopedSettings(scope, s);
|
|
1540
|
+
printSuccess(key + " = " + JSON.stringify(parsed) + " (scope: " + scope + ")");
|
|
1541
|
+
|
|
1542
|
+
} else if (action === "add" && key && value) {
|
|
1543
|
+
var s2 = loadScopedSettings(scope);
|
|
1544
|
+
if (!Array.isArray(s2[key])) s2[key] = [];
|
|
1545
|
+
s2[key].push(value);
|
|
1546
|
+
saveScopedSettings(scope, s2);
|
|
1547
|
+
printSuccess("Added '" + value + "' to " + key);
|
|
1548
|
+
|
|
1549
|
+
} else if (action === "remove" && key && value) {
|
|
1550
|
+
var s3 = loadScopedSettings(scope);
|
|
1551
|
+
if (Array.isArray(s3[key])) {
|
|
1552
|
+
s3[key] = s3[key].filter(function(v) { return v !== value; });
|
|
1553
|
+
saveScopedSettings(scope, s3);
|
|
1554
|
+
printSuccess("Removed '" + value + "' from " + key);
|
|
1555
|
+
} else {
|
|
1556
|
+
delete s3[key];
|
|
1557
|
+
saveScopedSettings(scope, s3);
|
|
1558
|
+
printSuccess("Removed " + key);
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
printError("Usage: /config list|get|set|add|remove <key> [value] [--global|--project|--local]");
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ══════════════════════════════════════════════════
|
|
1566
|
+
// ── 2. SANDBOX ENGINE ──
|
|
1567
|
+
// ══════════════════════════════════════════════════
|
|
1568
|
+
function checkSandbox(action, target) {
|
|
1569
|
+
var sandbox = getSetting("sandbox.enabled").value;
|
|
1570
|
+
if (!sandbox) return { allowed: true };
|
|
1571
|
+
|
|
1572
|
+
var perms = loadPermissions();
|
|
1573
|
+
|
|
1574
|
+
if (action === "write") {
|
|
1575
|
+
var denyWrite = getSetting("sandbox.filesystem.denyWrite").value || [];
|
|
1576
|
+
var allowWrite = getSetting("sandbox.filesystem.allowWrite").value || [];
|
|
1577
|
+
for (var i = 0; i < denyWrite.length; i++) {
|
|
1578
|
+
if (target.includes(denyWrite[i])) return { allowed: false, reason: "denyWrite: " + denyWrite[i] };
|
|
1579
|
+
}
|
|
1580
|
+
if (allowWrite.length > 0) {
|
|
1581
|
+
var ok = false;
|
|
1582
|
+
for (var j = 0; j < allowWrite.length; j++) {
|
|
1583
|
+
if (target.startsWith(allowWrite[j])) { ok = true; break; }
|
|
1584
|
+
}
|
|
1585
|
+
if (!ok) return { allowed: false, reason: "Not in allowWrite list" };
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (action === "read") {
|
|
1590
|
+
var denyRead = getSetting("sandbox.filesystem.denyRead").value || [];
|
|
1591
|
+
for (var k = 0; k < denyRead.length; k++) {
|
|
1592
|
+
if (target.includes(denyRead[k])) return { allowed: false, reason: "denyRead: " + denyRead[k] };
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (action === "network") {
|
|
1597
|
+
var allowedDomains = getSetting("sandbox.network.allowedDomains").value || [];
|
|
1598
|
+
if (allowedDomains.length > 0) {
|
|
1599
|
+
var domainOk = false;
|
|
1600
|
+
for (var l = 0; l < allowedDomains.length; l++) {
|
|
1601
|
+
if (target.includes(allowedDomains[l])) { domainOk = true; break; }
|
|
1602
|
+
}
|
|
1603
|
+
if (!domainOk) return { allowed: false, reason: "Domain not in allowedDomains" };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return { allowed: true };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ══════════════════════════════════════════════════
|
|
1611
|
+
// ── 3. HOOKS ENGINE ──
|
|
1612
|
+
// ══════════════════════════════════════════════════
|
|
1613
|
+
const HOOKS_FILE = path.join(CONFIG_DIR, "hooks.json");
|
|
1614
|
+
|
|
1615
|
+
function loadHooks() {
|
|
1616
|
+
if (fs.existsSync(HOOKS_FILE)) {
|
|
1617
|
+
try { return JSON.parse(fs.readFileSync(HOOKS_FILE, "utf8")); } catch(e) {}
|
|
1618
|
+
}
|
|
1619
|
+
return { pre: {}, post: {} };
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function saveHooks(h) { fs.writeFileSync(HOOKS_FILE, JSON.stringify(h, null, 2)); }
|
|
1623
|
+
|
|
1624
|
+
function runHook(phase, command) {
|
|
1625
|
+
if (getSetting("disableAllHooks").value) return;
|
|
1626
|
+
var hooks = loadHooks();
|
|
1627
|
+
var list = (hooks[phase] || {})[command] || [];
|
|
1628
|
+
for (var i = 0; i < list.length; i++) {
|
|
1629
|
+
try {
|
|
1630
|
+
if (list[i].type === "command") {
|
|
1631
|
+
execSync(list[i].run, { cwd: config.workdir, encoding: "utf8", timeout: 10000, stdio: "pipe" });
|
|
1632
|
+
} else if (list[i].type === "http") {
|
|
1633
|
+
var allowed = getSetting("allowedHttpHookUrls").value || [];
|
|
1634
|
+
if (allowed.length > 0 && !allowed.some(function(u) { return list[i].url.startsWith(u); })) continue;
|
|
1635
|
+
execSync("curl -sX POST " + JSON.stringify(list[i].url) + " -d " + JSON.stringify(JSON.stringify({ event: phase + ":" + command })), { timeout: 5000, stdio: "pipe" });
|
|
1636
|
+
}
|
|
1637
|
+
} catch(e) { if (config.verbose) log("Hook error: " + e.message); }
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function cmdHooks(args) {
|
|
1642
|
+
var hooks = loadHooks();
|
|
1643
|
+
var parts = (args || "").split(/\s+/);
|
|
1644
|
+
var action = parts[0] || "list";
|
|
1645
|
+
|
|
1646
|
+
if (action === "list") {
|
|
1647
|
+
console.log("");
|
|
1648
|
+
console.log(C.bold + " Hooks:" + C.reset);
|
|
1649
|
+
["pre", "post"].forEach(function(phase) {
|
|
1650
|
+
var cmds = Object.keys(hooks[phase] || {});
|
|
1651
|
+
if (cmds.length === 0) return;
|
|
1652
|
+
console.log(C.yellow + " " + phase + ":" + C.reset);
|
|
1653
|
+
cmds.forEach(function(cmd) {
|
|
1654
|
+
hooks[phase][cmd].forEach(function(h) {
|
|
1655
|
+
console.log(" " + cmd + " → " + C.brightCyan + (h.run || h.url) + C.reset + C.dim + " (" + h.type + ")" + C.reset);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
});
|
|
1659
|
+
if (Object.keys(hooks.pre).length === 0 && Object.keys(hooks.post).length === 0) {
|
|
1660
|
+
console.log(C.gray + " (none)" + C.reset);
|
|
1661
|
+
}
|
|
1662
|
+
console.log("");
|
|
1663
|
+
|
|
1664
|
+
} else if (action === "add" && parts[1] && parts[2] && parts[3]) {
|
|
1665
|
+
// /hooks add pre:chat "echo hello"
|
|
1666
|
+
var trigger = parts[1].split(":");
|
|
1667
|
+
var phase = trigger[0]; // pre or post
|
|
1668
|
+
var cmd = trigger[1];
|
|
1669
|
+
var run = parts.slice(2).join(" ");
|
|
1670
|
+
if (!hooks[phase]) hooks[phase] = {};
|
|
1671
|
+
if (!hooks[phase][cmd]) hooks[phase][cmd] = [];
|
|
1672
|
+
hooks[phase][cmd].push({ type: "command", run: run });
|
|
1673
|
+
saveHooks(hooks);
|
|
1674
|
+
printSuccess("Hook added: " + phase + ":" + cmd + " → " + run);
|
|
1675
|
+
|
|
1676
|
+
} else if (action === "remove" && parts[1]) {
|
|
1677
|
+
var trigger2 = parts[1].split(":");
|
|
1678
|
+
if (hooks[trigger2[0]] && hooks[trigger2[0]][trigger2[1]]) {
|
|
1679
|
+
delete hooks[trigger2[0]][trigger2[1]];
|
|
1680
|
+
saveHooks(hooks);
|
|
1681
|
+
printSuccess("Hook removed: " + parts[1]);
|
|
1682
|
+
} else {
|
|
1683
|
+
printError("Hook not found: " + parts[1]);
|
|
1684
|
+
}
|
|
1685
|
+
} else {
|
|
1686
|
+
printError("Usage: /hooks list|add|remove — /hooks add pre:chat \"echo hello\"");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// ══════════════════════════════════════════════════
|
|
1691
|
+
// ── 4. SUBAGENTS ──
|
|
1692
|
+
// ══════════════════════════════════════════════════
|
|
1693
|
+
const AGENTS_DIR_USER = path.join(CONFIG_DIR, "agents");
|
|
1694
|
+
if (!fs.existsSync(AGENTS_DIR_USER)) fs.mkdirSync(AGENTS_DIR_USER, { recursive: true });
|
|
1695
|
+
|
|
1696
|
+
function loadSubagents() {
|
|
1697
|
+
var agents = [];
|
|
1698
|
+
// User-level agents
|
|
1699
|
+
[AGENTS_DIR_USER, path.join(findProjectRoot(), ".blun", "agents")].forEach(function(dir) {
|
|
1700
|
+
if (!fs.existsSync(dir)) return;
|
|
1701
|
+
fs.readdirSync(dir).forEach(function(f) {
|
|
1702
|
+
if (!f.endsWith(".md")) return;
|
|
1703
|
+
var content = fs.readFileSync(path.join(dir, f), "utf8");
|
|
1704
|
+
var meta = {};
|
|
1705
|
+
// Parse YAML frontmatter
|
|
1706
|
+
var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1707
|
+
if (fmMatch) {
|
|
1708
|
+
fmMatch[1].split("\n").forEach(function(line) {
|
|
1709
|
+
var kv = line.match(/^(\w+):\s*(.+)/);
|
|
1710
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
agents.push({
|
|
1714
|
+
name: meta.name || f.replace(".md", ""),
|
|
1715
|
+
description: meta.description || "",
|
|
1716
|
+
role: meta.role || "general",
|
|
1717
|
+
tools: (meta.tools || "").split(",").map(function(t) { return t.trim(); }).filter(Boolean),
|
|
1718
|
+
file: path.join(dir, f),
|
|
1719
|
+
prompt: content.replace(/^---[\s\S]*?---\n*/, "").trim(),
|
|
1720
|
+
scope: dir.includes(CONFIG_DIR) ? "user" : "project"
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
return agents;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function cmdAgents(args) {
|
|
1728
|
+
var parts = (args || "").split(/\s+/);
|
|
1729
|
+
var action = parts[0] || "list";
|
|
1730
|
+
|
|
1731
|
+
if (action === "list") {
|
|
1732
|
+
var agents = loadSubagents();
|
|
1733
|
+
console.log("");
|
|
1734
|
+
console.log(C.bold + " " + BOX.bot + " Subagents:" + C.reset);
|
|
1735
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
1736
|
+
if (agents.length === 0) {
|
|
1737
|
+
console.log(C.gray + " (none) — Create .md files in ~/.blun/agents/ or .blun/agents/" + C.reset);
|
|
1738
|
+
}
|
|
1739
|
+
agents.forEach(function(a) {
|
|
1740
|
+
console.log(" " + C.brightCyan + a.name + C.reset + " — " + a.description + C.dim + " (" + a.scope + ")" + C.reset);
|
|
1741
|
+
});
|
|
1742
|
+
console.log("");
|
|
1743
|
+
|
|
1744
|
+
} else if (action === "create" && parts[1]) {
|
|
1745
|
+
var name = parts[1];
|
|
1746
|
+
var desc = parts.slice(2).join(" ") || "Custom agent";
|
|
1747
|
+
var agentFile = path.join(AGENTS_DIR_USER, name + ".md");
|
|
1748
|
+
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";
|
|
1749
|
+
fs.writeFileSync(agentFile, content);
|
|
1750
|
+
printSuccess("Agent '" + name + "' created at " + agentFile);
|
|
1751
|
+
|
|
1752
|
+
} else if (action === "run" && parts[1]) {
|
|
1753
|
+
var agents2 = loadSubagents();
|
|
1754
|
+
var agent = agents2.find(function(a) { return a.name === parts[1]; });
|
|
1755
|
+
if (!agent) { printError("Agent not found: " + parts[1]); return; }
|
|
1756
|
+
var task = parts.slice(2).join(" ");
|
|
1757
|
+
if (!task) { printError("Usage: /agents run <name> <task>"); return; }
|
|
1758
|
+
// Run agent via API
|
|
1759
|
+
printInfo("Running agent '" + agent.name + "'...");
|
|
1760
|
+
apiCall("POST", "/agent", { goal: agent.prompt + "\n\nAKTUELLE AUFGABE: " + task }).then(function(resp) {
|
|
1761
|
+
if (resp.status === 200) {
|
|
1762
|
+
printAnswer(resp.data.answer, agent.name + " " + BOX.dot + " " + resp.data.steps_executed + " steps");
|
|
1763
|
+
} else {
|
|
1764
|
+
printError(resp.data.error || "Error");
|
|
1765
|
+
}
|
|
1766
|
+
}).catch(function(e) { printError(e.message); });
|
|
1767
|
+
|
|
1768
|
+
} else if (action === "info" && parts[1]) {
|
|
1769
|
+
var agents3 = loadSubagents();
|
|
1770
|
+
var a = agents3.find(function(a) { return a.name === parts[1]; });
|
|
1771
|
+
if (!a) { printError("Agent not found: " + parts[1]); return; }
|
|
1772
|
+
console.log("");
|
|
1773
|
+
console.log(C.bold + " Agent: " + a.name + C.reset);
|
|
1774
|
+
console.log(" Description: " + a.description);
|
|
1775
|
+
console.log(" Role: " + a.role);
|
|
1776
|
+
console.log(" Tools: " + a.tools.join(", "));
|
|
1777
|
+
console.log(" Scope: " + a.scope);
|
|
1778
|
+
console.log(" File: " + a.file);
|
|
1779
|
+
console.log(C.dim + " " + BOX.h.repeat(30) + C.reset);
|
|
1780
|
+
console.log(C.gray + a.prompt.slice(0, 300) + C.reset);
|
|
1781
|
+
console.log("");
|
|
1782
|
+
|
|
1783
|
+
} else {
|
|
1784
|
+
printError("Usage: /agents list|create|run|info <name>");
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// ══════════════════════════════════════════════════
|
|
1789
|
+
// ── 5. SESSION HISTORY (per workdir) ──
|
|
1790
|
+
// ══════════════════════════════════════════════════
|
|
1791
|
+
const SESSIONS_DIR = path.join(CONFIG_DIR, "sessions");
|
|
1792
|
+
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
1793
|
+
|
|
1794
|
+
function getSessionFile() {
|
|
1795
|
+
var hash = require("crypto").createHash("md5").update(config.workdir).digest("hex").slice(0, 8);
|
|
1796
|
+
return path.join(SESSIONS_DIR, hash + ".json");
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function loadSessionHistory() {
|
|
1800
|
+
var f = getSessionFile();
|
|
1801
|
+
if (fs.existsSync(f)) {
|
|
1802
|
+
try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch(e) {}
|
|
1803
|
+
}
|
|
1804
|
+
return { workdir: config.workdir, history: [], inputHistory: [] };
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function saveSessionHistory(session) {
|
|
1808
|
+
fs.writeFileSync(getSessionFile(), JSON.stringify(session, null, 2));
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// ══════════════════════════════════════════════════
|
|
1812
|
+
// ── 6. COST TRACKING ──
|
|
1813
|
+
// ══════════════════════════════════════════════════
|
|
1814
|
+
var sessionCost = { requests: 0, inputTokensEst: 0, outputTokensEst: 0 };
|
|
1815
|
+
|
|
1816
|
+
function cmdCost() {
|
|
1817
|
+
console.log("");
|
|
1818
|
+
console.log(C.bold + " Session Cost Estimate:" + C.reset);
|
|
1819
|
+
console.log(" Requests: " + C.brightCyan + sessionCost.requests + C.reset);
|
|
1820
|
+
console.log(" Est. Input Tokens: " + C.brightCyan + sessionCost.inputTokensEst + C.reset);
|
|
1821
|
+
console.log(" Est. Output Tokens: " + C.brightCyan + sessionCost.outputTokensEst + C.reset);
|
|
1822
|
+
console.log("");
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// ══════════════════════════════════════════════════
|
|
1826
|
+
// ── 7. /doctor — Diagnostics ──
|
|
1827
|
+
// ══════════════════════════════════════════════════
|
|
1828
|
+
async function cmdDoctor() {
|
|
1829
|
+
console.log("");
|
|
1830
|
+
console.log(C.bold + " " + BOX.bot + " BLUN Doctor — System Check" + C.reset);
|
|
1831
|
+
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
1832
|
+
|
|
1833
|
+
// Config
|
|
1834
|
+
var hasKey = config.auth.api_key && config.auth.api_key.length > 10;
|
|
1835
|
+
console.log(" Config: " + (fs.existsSync(CONFIG_FILE) ? C.green + "OK" : C.red + "MISSING") + C.reset);
|
|
1836
|
+
console.log(" API Key: " + (hasKey ? C.green + "OK" : C.red + "NOT SET") + C.reset);
|
|
1837
|
+
|
|
1838
|
+
// API
|
|
1839
|
+
try {
|
|
1840
|
+
var h = await apiCall("GET", "/health");
|
|
1841
|
+
console.log(" API: " + (h.status === 200 ? C.green + "OK (" + h.data.model + ")" : C.red + "ERROR " + h.status) + C.reset);
|
|
1842
|
+
} catch(e) {
|
|
1843
|
+
console.log(" API: " + C.red + "UNREACHABLE" + C.reset);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Git
|
|
1847
|
+
try {
|
|
1848
|
+
execSync("git --version", { stdio: "pipe" });
|
|
1849
|
+
console.log(" Git: " + C.green + "OK" + C.reset);
|
|
1850
|
+
} catch(e) {
|
|
1851
|
+
console.log(" Git: " + C.red + "NOT FOUND" + C.reset);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// SSH
|
|
1855
|
+
try {
|
|
1856
|
+
execSync("ssh -V 2>&1", { stdio: "pipe" });
|
|
1857
|
+
console.log(" SSH: " + C.green + "OK" + C.reset);
|
|
1858
|
+
} catch(e) {
|
|
1859
|
+
console.log(" SSH: " + C.red + "NOT FOUND" + C.reset);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Node
|
|
1863
|
+
console.log(" Node: " + C.green + process.version + C.reset);
|
|
1864
|
+
console.log(" Platform: " + C.green + process.platform + C.reset);
|
|
1865
|
+
|
|
1866
|
+
// Plugins
|
|
1867
|
+
var plugins = loadPlugins();
|
|
1868
|
+
console.log(" Plugins: " + C.brightCyan + Object.keys(plugins).length + " installed" + C.reset);
|
|
1869
|
+
|
|
1870
|
+
// Agents
|
|
1871
|
+
var agents = loadSubagents();
|
|
1872
|
+
console.log(" Agents: " + C.brightCyan + agents.length + " loaded" + C.reset);
|
|
1873
|
+
|
|
1874
|
+
// Hooks
|
|
1875
|
+
var hooks = loadHooks();
|
|
1876
|
+
var hookCount = Object.keys(hooks.pre || {}).length + Object.keys(hooks.post || {}).length;
|
|
1877
|
+
console.log(" Hooks: " + C.brightCyan + hookCount + " registered" + C.reset);
|
|
1878
|
+
|
|
1879
|
+
// Memory
|
|
1880
|
+
var memFiles = fs.existsSync(MEMORY_DIR) ? fs.readdirSync(MEMORY_DIR).length : 0;
|
|
1881
|
+
console.log(" Memory: " + C.brightCyan + memFiles + " entries" + C.reset);
|
|
1882
|
+
|
|
1883
|
+
// Disk
|
|
1884
|
+
console.log(" Workdir: " + C.brightCyan + config.workdir + C.reset);
|
|
1885
|
+
console.log("");
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// ══════════════════════════════════════════════════
|
|
1889
|
+
// ── 8. /model — Model switcher ──
|
|
1890
|
+
// ══════════════════════════════════════════════════
|
|
1891
|
+
function cmdModel(args) {
|
|
1892
|
+
if (!args) {
|
|
1893
|
+
console.log("");
|
|
1894
|
+
console.log(C.bold + " Current Model: " + C.brightCyan + config.model + C.reset);
|
|
1895
|
+
console.log(C.gray + " /model <name> — Switch model" + C.reset);
|
|
1896
|
+
console.log("");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
config.model = args.trim();
|
|
1900
|
+
saveConfig(config);
|
|
1901
|
+
printSuccess("Model switched to: " + config.model);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ══════════════════════════════════════════════════
|
|
1905
|
+
// ── 9. /login /logout ──
|
|
1906
|
+
// ══════════════════════════════════════════════════
|
|
1907
|
+
function cmdLogin(args) {
|
|
1908
|
+
if (args && args.startsWith("key ")) {
|
|
1909
|
+
config.auth.api_key = args.slice(4).trim();
|
|
1910
|
+
config.auth.type = "api_key";
|
|
1911
|
+
saveConfig(config);
|
|
1912
|
+
printSuccess("Logged in with API key.");
|
|
1913
|
+
} else if (args === "oauth") {
|
|
1914
|
+
printInfo("OAuth login not yet implemented. Use API key for now:");
|
|
1915
|
+
printInfo(" /login key <your-api-key>");
|
|
1916
|
+
} else {
|
|
1917
|
+
console.log("");
|
|
1918
|
+
console.log(C.bold + " Login:" + C.reset);
|
|
1919
|
+
console.log(" /login key <api-key> — Login with API key");
|
|
1920
|
+
console.log(" /login oauth — Login with OAuth (coming soon)");
|
|
1921
|
+
console.log("");
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function cmdLogout() {
|
|
1926
|
+
config.auth.api_key = "";
|
|
1927
|
+
config.auth.oauth_token = "";
|
|
1928
|
+
config.auth.oauth_expires = null;
|
|
1929
|
+
saveConfig(config);
|
|
1930
|
+
printSuccess("Logged out.");
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// ══════════════════════════════════════════════════
|
|
1934
|
+
// ── 10. /compact — Clear context ──
|
|
1935
|
+
// ══════════════════════════════════════════════════
|
|
1936
|
+
function cmdCompact() {
|
|
1937
|
+
var before = chatHistory.length;
|
|
1938
|
+
chatHistory = chatHistory.slice(-4);
|
|
1939
|
+
printSuccess("Context compacted: " + before + " → " + chatHistory.length + " messages");
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// ══════════════════════════════════════════════════
|
|
1943
|
+
// ── 11. /review — Review current diff ──
|
|
1944
|
+
// ══════════════════════════════════════════════════
|
|
1945
|
+
async function cmdReview() {
|
|
1946
|
+
try {
|
|
1947
|
+
var diff = execSync("git diff", { cwd: config.workdir, encoding: "utf8", timeout: 10000 });
|
|
1948
|
+
if (!diff.trim()) { printInfo("No changes to review."); return; }
|
|
1949
|
+
printInfo("Sending diff for review...");
|
|
1950
|
+
var resp = await apiCall("POST", "/chat", {
|
|
1951
|
+
message: "Review diesen Git-Diff. Finde Bugs, Sicherheitsprobleme, Verbesserungen:\n\n```diff\n" + diff.slice(0, 8000) + "\n```"
|
|
1952
|
+
});
|
|
1953
|
+
if (resp.status === 200) printAnswer(resp.data.answer, "code-review");
|
|
1954
|
+
else printError(resp.data.error || "Error");
|
|
1955
|
+
} catch(e) { printError(e.message); }
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1075
1958
|
// ── Main Loop ──
|
|
1076
1959
|
async function main() {
|
|
1077
1960
|
// Handle CLI args
|
|
@@ -1110,7 +1993,22 @@ async function main() {
|
|
|
1110
1993
|
}
|
|
1111
1994
|
|
|
1112
1995
|
// Interactive mode
|
|
1996
|
+
// Workdir selection (like Claude Code)
|
|
1997
|
+
if (!process.argv.includes("--no-workdir-prompt")) {
|
|
1998
|
+
var cwdHasBlun = fs.existsSync(path.join(process.cwd(), ".blun"));
|
|
1999
|
+
var cwdHasGit = fs.existsSync(path.join(process.cwd(), ".git"));
|
|
2000
|
+
if (cwdHasBlun || cwdHasGit) {
|
|
2001
|
+
config.workdir = process.cwd();
|
|
2002
|
+
}
|
|
2003
|
+
// Show workdir in header
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1113
2006
|
printHeader();
|
|
2007
|
+
checkForUpdates();
|
|
2008
|
+
|
|
2009
|
+
// Load session history for this workdir
|
|
2010
|
+
var session = loadSessionHistory();
|
|
2011
|
+
chatHistory = session.history.slice(-20);
|
|
1114
2012
|
|
|
1115
2013
|
// Health check
|
|
1116
2014
|
try {
|
|
@@ -1127,7 +2025,9 @@ async function main() {
|
|
|
1127
2025
|
"/generate", "/analyze", "/score", "/eval", "/settings", "/set",
|
|
1128
2026
|
"/status", "/versions", "/health", "/watchdog", "/memory", "/files",
|
|
1129
2027
|
"/sh", "/git", "/ssh", "/deploy", "/read", "/write", "/init",
|
|
1130
|
-
"/screenshot", "/render", "/agent", "/
|
|
2028
|
+
"/screenshot", "/render", "/agent", "/plugin", "/mcp", "/permissions",
|
|
2029
|
+
"/config", "/hooks", "/agents", "/doctor", "/model", "/login", "/logout",
|
|
2030
|
+
"/compact", "/review", "/cost", "/exit"
|
|
1131
2031
|
];
|
|
1132
2032
|
|
|
1133
2033
|
var rl = readline.createInterface({
|
|
@@ -1149,15 +2049,46 @@ async function main() {
|
|
|
1149
2049
|
var input = line.trim();
|
|
1150
2050
|
if (!input) { rl.prompt(); return; }
|
|
1151
2051
|
|
|
1152
|
-
|
|
2052
|
+
// ! prefix = direct bash
|
|
2053
|
+
if (input.startsWith("!")) {
|
|
2054
|
+
cmdShell(input.slice(1).trim());
|
|
2055
|
+
}
|
|
2056
|
+
// # prefix = memory shortcut
|
|
2057
|
+
else if (input.startsWith("#save ")) {
|
|
2058
|
+
var memParts = input.slice(6).split(/\s+/);
|
|
2059
|
+
var memKey = memParts[0];
|
|
2060
|
+
var memVal = memParts.slice(1).join(" ");
|
|
2061
|
+
if (memKey && memVal) {
|
|
2062
|
+
fs.writeFileSync(path.join(MEMORY_DIR, memKey + ".txt"), memVal);
|
|
2063
|
+
printSuccess("Memory saved: " + memKey);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
else if (input.startsWith("#") && !input.startsWith("#save")) {
|
|
2067
|
+
var memKey2 = input.slice(1).trim();
|
|
2068
|
+
var memFile = path.join(MEMORY_DIR, memKey2 + ".txt");
|
|
2069
|
+
if (fs.existsSync(memFile)) {
|
|
2070
|
+
console.log(C.brightCyan + fs.readFileSync(memFile, "utf8") + C.reset);
|
|
2071
|
+
} else {
|
|
2072
|
+
printError("Memory not found: " + memKey2);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
// / prefix = command
|
|
2076
|
+
else if (input.startsWith("/")) {
|
|
2077
|
+
runHook("pre", input.split(/\s+/)[0].slice(1));
|
|
1153
2078
|
await handleCommand(input);
|
|
1154
|
-
|
|
2079
|
+
runHook("post", input.split(/\s+/)[0].slice(1));
|
|
2080
|
+
}
|
|
2081
|
+
// plain text = chat
|
|
2082
|
+
else {
|
|
1155
2083
|
await sendChat(input);
|
|
1156
2084
|
}
|
|
1157
2085
|
rl.prompt();
|
|
1158
2086
|
});
|
|
1159
2087
|
|
|
1160
2088
|
rl.on("close", function() {
|
|
2089
|
+
// Save session history
|
|
2090
|
+
session.history = chatHistory.slice(-50);
|
|
2091
|
+
saveSessionHistory(session);
|
|
1161
2092
|
console.log(C.dim + "\nBye." + C.reset);
|
|
1162
2093
|
process.exit(0);
|
|
1163
2094
|
});
|