blun-king-cli 2.9.0 → 3.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 +188 -24
- package/package.json +1 -1
package/bin/blun.js
CHANGED
|
@@ -233,6 +233,63 @@ function printAnswer(answer, meta) {
|
|
|
233
233
|
console.log("");
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// Streaming typewriter output — word by word
|
|
237
|
+
async function streamAnswer(answer, meta) {
|
|
238
|
+
console.log("");
|
|
239
|
+
console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset + (meta ? C.gray + " " + BOX.dot + " " + meta + C.reset : ""));
|
|
240
|
+
console.log(C.green + " " + BOX.h.repeat(40) + C.reset);
|
|
241
|
+
|
|
242
|
+
var lines = answer.split("\n");
|
|
243
|
+
var inCode = false;
|
|
244
|
+
var delay = 15; // ms per word
|
|
245
|
+
|
|
246
|
+
for (var i = 0; i < lines.length; i++) {
|
|
247
|
+
var line = lines[i];
|
|
248
|
+
|
|
249
|
+
if (line.startsWith("```")) {
|
|
250
|
+
inCode = !inCode;
|
|
251
|
+
if (inCode) {
|
|
252
|
+
var lang = line.slice(3).trim();
|
|
253
|
+
console.log(C.dim + " " + BOX.tl + BOX.h.repeat(38) + (lang ? " " + lang + " " : "") + C.reset);
|
|
254
|
+
} else {
|
|
255
|
+
console.log(C.dim + " " + BOX.bl + BOX.h.repeat(38) + C.reset);
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (inCode) {
|
|
261
|
+
console.log(C.cyan + " " + BOX.v + " " + line + C.reset);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Determine prefix and color
|
|
266
|
+
var prefix = " ";
|
|
267
|
+
var color = C.reset;
|
|
268
|
+
var text = line;
|
|
269
|
+
|
|
270
|
+
if (line.startsWith("# ")) { prefix = "\n" + C.bold + C.yellow + " \u2588 "; text = line.slice(2); color = C.reset + "\n"; }
|
|
271
|
+
else if (line.startsWith("## ")) { prefix = "\n" + C.bold + C.magenta + " \u25B6 "; text = line.slice(3); color = C.reset; }
|
|
272
|
+
else if (line.startsWith("### ")) { prefix = C.bold + C.brightWhite + " \u25AA "; text = line.slice(4); color = C.reset; }
|
|
273
|
+
else if (line.match(/^\s*[-*] /)) { prefix = " " + C.green + "\u2022 " + C.reset; text = line.replace(/^\s*[-*] /, ""); }
|
|
274
|
+
else if (line.match(/^\s*\d+\.\s/)) { var m = line.match(/^(\s*\d+\.)\s(.*)/); prefix = " " + C.yellow + m[1] + C.reset + " "; text = m[2]; }
|
|
275
|
+
else if (line.trim() === "") { console.log(""); continue; }
|
|
276
|
+
|
|
277
|
+
// Stream words
|
|
278
|
+
var words = text.split(" ");
|
|
279
|
+
process.stdout.write(prefix);
|
|
280
|
+
for (var w = 0; w < words.length; w++) {
|
|
281
|
+
var word = words[w];
|
|
282
|
+
// Inline formatting
|
|
283
|
+
word = word.replace(/\*\*(.+?)\*\*/g, C.bold + C.brightWhite + "$1" + C.reset);
|
|
284
|
+
word = word.replace(/`(.+?)`/g, C.cyan + "$1" + C.reset);
|
|
285
|
+
process.stdout.write(word + (w < words.length - 1 ? " " : ""));
|
|
286
|
+
await new Promise(function(r) { setTimeout(r, delay); });
|
|
287
|
+
}
|
|
288
|
+
process.stdout.write(color + "\n");
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
291
|
+
}
|
|
292
|
+
|
|
236
293
|
function printUserMessage(msg) {
|
|
237
294
|
console.log("");
|
|
238
295
|
console.log(C.brightBlue + C.bold + " " + BOX.user + " Du" + C.reset);
|
|
@@ -520,13 +577,23 @@ async function handleCommand(input) {
|
|
|
520
577
|
async function sendChat(message) {
|
|
521
578
|
try {
|
|
522
579
|
printUserMessage(message);
|
|
523
|
-
// Animated thinking
|
|
580
|
+
// Animated thinking + keep input box visible
|
|
524
581
|
var dots = 0;
|
|
525
582
|
var thinkFrames = ["thinking", "thinking.", "thinking..", "thinking..."];
|
|
583
|
+
console.log(""); // line for thinking text
|
|
526
584
|
var thinkTimer = setInterval(function() {
|
|
527
|
-
process.stdout.write("\
|
|
585
|
+
process.stdout.write("\x1b[s"); // save cursor
|
|
586
|
+
// Move to thinking line (above the UI)
|
|
587
|
+
if (uiStartRow > 0) process.stdout.write("\x1b[" + (uiStartRow + 1) + "A");
|
|
588
|
+
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " " + thinkFrames[dots % 4] + " " + C.reset);
|
|
589
|
+
process.stdout.write("\x1b[u"); // restore cursor
|
|
528
590
|
dots++;
|
|
529
591
|
}, 300);
|
|
592
|
+
// Redraw input box below thinking line
|
|
593
|
+
processing = false;
|
|
594
|
+
uiStartRow = -1;
|
|
595
|
+
drawPrompt();
|
|
596
|
+
processing = true;
|
|
530
597
|
|
|
531
598
|
var resp = await apiCall("POST", "/chat", {
|
|
532
599
|
message: message,
|
|
@@ -534,7 +601,10 @@ async function sendChat(message) {
|
|
|
534
601
|
});
|
|
535
602
|
|
|
536
603
|
clearInterval(thinkTimer);
|
|
537
|
-
|
|
604
|
+
// Erase UI before printing answer
|
|
605
|
+
eraseUI();
|
|
606
|
+
// Clear thinking line
|
|
607
|
+
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
538
608
|
|
|
539
609
|
if (resp.status !== 200) {
|
|
540
610
|
printError(resp.data.error || "API Error " + resp.status);
|
|
@@ -555,7 +625,9 @@ async function sendChat(message) {
|
|
|
555
625
|
var tokInfo = C.dim + (inTok + outTok) + " tok" + C.reset;
|
|
556
626
|
var meta = resp.data.task_type + "/" + resp.data.role + " " + BOX.dot + " score: " + (q.score || "?") +
|
|
557
627
|
(q.retried ? " " + BOX.dot + " retried" : "") + " " + BOX.dot + " " + tokInfo;
|
|
558
|
-
|
|
628
|
+
|
|
629
|
+
// Streaming typewriter effect
|
|
630
|
+
await streamAnswer(resp.data.answer, meta);
|
|
559
631
|
|
|
560
632
|
// Auto Memory Dream Mode
|
|
561
633
|
dreamCounter++;
|
|
@@ -1450,56 +1522,143 @@ async function cmdAgent(args) {
|
|
|
1450
1522
|
var lastAnswer = "";
|
|
1451
1523
|
var goal = args;
|
|
1452
1524
|
var context = [];
|
|
1525
|
+
var workdir = config.workdir;
|
|
1526
|
+
|
|
1527
|
+
// Show thinking + keep input box
|
|
1528
|
+
console.log("");
|
|
1529
|
+
var thinkTimer = setInterval(function() {
|
|
1530
|
+
process.stdout.write("\x1b[s");
|
|
1531
|
+
if (uiStartRow > 0) process.stdout.write("\x1b[" + (uiStartRow + 1) + "A");
|
|
1532
|
+
var phase = loop === 0 ? "planning" : "step " + loop + "/" + maxLoops;
|
|
1533
|
+
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " [" + phase + "] working..." + C.reset);
|
|
1534
|
+
process.stdout.write("\x1b[u");
|
|
1535
|
+
}, 300);
|
|
1536
|
+
processing = false;
|
|
1537
|
+
uiStartRow = -1;
|
|
1538
|
+
drawPrompt();
|
|
1539
|
+
processing = true;
|
|
1453
1540
|
|
|
1454
1541
|
while (loop < maxLoops) {
|
|
1455
1542
|
loop++;
|
|
1456
|
-
// Phase indicator
|
|
1457
|
-
var phase = loop === 1 ? "planning" : "step " + loop;
|
|
1458
|
-
process.stdout.write("\r\x1b[K" + C.dim + " " + BOX.bot + " [" + phase + "/" + maxLoops + "] working..." + C.reset);
|
|
1459
1543
|
|
|
1460
1544
|
var resp = await apiCall("POST", "/agent", {
|
|
1461
1545
|
goal: goal,
|
|
1546
|
+
workdir: workdir,
|
|
1462
1547
|
context: context.slice(-5),
|
|
1463
1548
|
verbose: false
|
|
1464
1549
|
});
|
|
1465
|
-
process.stdout.write("\r\x1b[K");
|
|
1466
1550
|
|
|
1467
1551
|
if (resp.status !== 200) {
|
|
1552
|
+
clearInterval(thinkTimer);
|
|
1553
|
+
eraseUI();
|
|
1554
|
+
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
1468
1555
|
printError("Step " + loop + " failed: " + (resp.data.error || "Error"));
|
|
1469
1556
|
break;
|
|
1470
1557
|
}
|
|
1471
1558
|
|
|
1472
1559
|
var d = resp.data;
|
|
1473
1560
|
lastAnswer = d.answer || "";
|
|
1474
|
-
|
|
1475
|
-
|
|
1561
|
+
|
|
1562
|
+
// Save files locally to workspace
|
|
1563
|
+
if (d.files && d.files.length > 0) {
|
|
1564
|
+
for (var fi = 0; fi < d.files.length; fi++) {
|
|
1565
|
+
var f = d.files[fi];
|
|
1566
|
+
var localPath = path.join(workdir, f.name);
|
|
1567
|
+
var localDir = path.dirname(localPath);
|
|
1568
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
1569
|
+
// If file has content directly, write it
|
|
1570
|
+
if (f.content) {
|
|
1571
|
+
fs.writeFileSync(localPath, f.content, "utf8");
|
|
1572
|
+
f.localPath = localPath;
|
|
1573
|
+
} else if (f.download) {
|
|
1574
|
+
// Download from server
|
|
1575
|
+
try {
|
|
1576
|
+
var dlResp = await apiCall("GET", f.download);
|
|
1577
|
+
if (dlResp.status === 200) {
|
|
1578
|
+
var content = typeof dlResp.data === "string" ? dlResp.data : JSON.stringify(dlResp.data, null, 2);
|
|
1579
|
+
fs.writeFileSync(localPath, content, "utf8");
|
|
1580
|
+
f.localPath = localPath;
|
|
1581
|
+
}
|
|
1582
|
+
} catch(dlErr) {}
|
|
1583
|
+
}
|
|
1584
|
+
totalFiles.push(f);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// If answer contains code blocks with filenames, extract and save them
|
|
1589
|
+
var codeBlockRe = /```(?:\w+)?\s*\n\/\/\s*(.+\.(?:html|css|js|json|md|txt|py))\n([\s\S]*?)```/g;
|
|
1590
|
+
var cbMatch;
|
|
1591
|
+
while ((cbMatch = codeBlockRe.exec(lastAnswer)) !== null) {
|
|
1592
|
+
var cbFile = cbMatch[1].trim();
|
|
1593
|
+
var cbContent = cbMatch[2];
|
|
1594
|
+
var cbPath = path.join(workdir, cbFile);
|
|
1595
|
+
var cbDir = path.dirname(cbPath);
|
|
1596
|
+
if (!fs.existsSync(cbDir)) fs.mkdirSync(cbDir, { recursive: true });
|
|
1597
|
+
fs.writeFileSync(cbPath, cbContent, "utf8");
|
|
1598
|
+
totalFiles.push({ name: cbFile, localPath: cbPath });
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Also extract filename: pattern like "**index.html**:" or "Datei: index.html"
|
|
1602
|
+
var fileHeaderRe = /(?:\*\*|Datei:\s*)([a-zA-Z0-9_\-/.]+\.(?:html|css|js|json|md|txt|py))\*?\*?:?\s*\n```(?:\w+)?\n([\s\S]*?)```/g;
|
|
1603
|
+
var fhMatch;
|
|
1604
|
+
while ((fhMatch = fileHeaderRe.exec(lastAnswer)) !== null) {
|
|
1605
|
+
var fhFile = fhMatch[1].trim();
|
|
1606
|
+
var fhContent = fhMatch[2];
|
|
1607
|
+
var fhPath = path.join(workdir, fhFile);
|
|
1608
|
+
var fhDir = path.dirname(fhPath);
|
|
1609
|
+
if (!fs.existsSync(fhDir)) fs.mkdirSync(fhDir, { recursive: true });
|
|
1610
|
+
if (!totalFiles.some(function(tf) { return tf.name === fhFile; })) {
|
|
1611
|
+
fs.writeFileSync(fhPath, fhContent, "utf8");
|
|
1612
|
+
totalFiles.push({ name: fhFile, localPath: fhPath });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
context.push({ step: loop, answer: lastAnswer.slice(0, 500), files: totalFiles.length, workdir: workdir });
|
|
1476
1617
|
|
|
1477
1618
|
// Check if the agent says it's done
|
|
1478
1619
|
var doneSignals = ["done", "complete", "finished", "fertig", "erledigt", "all steps", "nothing left"];
|
|
1479
1620
|
var isDone = doneSignals.some(function(s) { return lastAnswer.toLowerCase().includes(s); });
|
|
1480
1621
|
|
|
1481
|
-
// Show brief step result
|
|
1482
|
-
console.log(C.dim + " [" + loop + "] " + C.reset + C.green + (d.steps_executed || 1) + " steps" +
|
|
1483
|
-
(d.files && d.files.length > 0 ? ", " + d.files.length + " files" : "") + C.reset +
|
|
1484
|
-
(isDone ? C.yellow + " ✓ done" + C.reset : ""));
|
|
1485
|
-
|
|
1486
1622
|
if (isDone) break;
|
|
1487
1623
|
|
|
1488
1624
|
// Feed result back as next goal
|
|
1489
|
-
goal = "Continue with the original goal: " + args +
|
|
1490
|
-
"\
|
|
1625
|
+
goal = "Continue with the original goal: " + args +
|
|
1626
|
+
"\nWorkspace: " + workdir +
|
|
1627
|
+
"\nFiles created so far: " + totalFiles.map(function(f) { return f.name; }).join(", ") +
|
|
1628
|
+
"\n\nLast step result: " + lastAnswer.slice(0, 1000) +
|
|
1629
|
+
"\n\nWrite the actual file contents as code blocks. If everything is done, say 'done'.";
|
|
1491
1630
|
}
|
|
1492
1631
|
|
|
1632
|
+
clearInterval(thinkTimer);
|
|
1633
|
+
eraseUI();
|
|
1634
|
+
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
1635
|
+
|
|
1493
1636
|
// Final summary
|
|
1494
1637
|
console.log("");
|
|
1495
|
-
var
|
|
1496
|
-
|
|
1638
|
+
var savedCount = totalFiles.filter(function(f) { return f.localPath; }).length;
|
|
1639
|
+
var meta = loop + " loops" + (totalFiles.length > 0 ? " " + BOX.dot + " " + savedCount + " files saved" : "");
|
|
1640
|
+
|
|
1641
|
+
// Token tracking
|
|
1642
|
+
var inTok = Math.ceil(args.length / 3.5);
|
|
1643
|
+
var outTok = Math.ceil(lastAnswer.length / 3.5);
|
|
1644
|
+
sessionCost.requests += loop;
|
|
1645
|
+
sessionCost.inputTokensEst += inTok * loop;
|
|
1646
|
+
sessionCost.outputTokensEst += outTok;
|
|
1647
|
+
|
|
1648
|
+
await streamAnswer(lastAnswer, meta);
|
|
1649
|
+
|
|
1497
1650
|
if (totalFiles.length > 0) {
|
|
1498
|
-
console.log(C.green + "
|
|
1499
|
-
totalFiles.forEach(function(f) {
|
|
1651
|
+
console.log(C.green + " 📁 Saved to workspace:" + C.reset);
|
|
1652
|
+
totalFiles.forEach(function(f) {
|
|
1653
|
+
if (f.localPath) console.log(" " + C.brightCyan + f.localPath + C.reset);
|
|
1654
|
+
else console.log(" " + C.dim + f.name + " (not saved)" + C.reset);
|
|
1655
|
+
});
|
|
1500
1656
|
console.log("");
|
|
1501
1657
|
}
|
|
1502
|
-
} catch(e) {
|
|
1658
|
+
} catch(e) {
|
|
1659
|
+
if (typeof thinkTimer !== "undefined") clearInterval(thinkTimer);
|
|
1660
|
+
printError(e.message);
|
|
1661
|
+
}
|
|
1503
1662
|
}
|
|
1504
1663
|
|
|
1505
1664
|
// ── Screenshot/Render (CLI wrapper) ──
|
|
@@ -2719,8 +2878,13 @@ async function main() {
|
|
|
2719
2878
|
await handleCommand(detected);
|
|
2720
2879
|
runHook("post", detected.split(/\s+/)[0].slice(1));
|
|
2721
2880
|
} else if (sessionMode === "agent") {
|
|
2722
|
-
// Agent mode:
|
|
2723
|
-
|
|
2881
|
+
// Agent mode: detect if question or task
|
|
2882
|
+
var isQuestion = /^(wo |was |wie |wer |wann |warum |kannst|hast|ist |sind |where|what|how|who|when|why|can you|do you|is |are |\?$)/i.test(input.trim());
|
|
2883
|
+
if (isQuestion) {
|
|
2884
|
+
await sendChat(input);
|
|
2885
|
+
} else {
|
|
2886
|
+
await cmdAgent(input);
|
|
2887
|
+
}
|
|
2724
2888
|
} else {
|
|
2725
2889
|
await sendChat(input);
|
|
2726
2890
|
}
|