blun-king-cli 2.9.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/blun.js +164 -37
- package/package.json +1 -1
package/bin/blun.js
CHANGED
|
@@ -233,10 +233,66 @@ 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);
|
|
239
|
-
console.log(C.brightBlue + " " + BOX.h.repeat(40) + C.reset);
|
|
240
296
|
console.log(C.brightWhite + " " + msg + C.reset);
|
|
241
297
|
}
|
|
242
298
|
|
|
@@ -520,23 +576,13 @@ async function handleCommand(input) {
|
|
|
520
576
|
async function sendChat(message) {
|
|
521
577
|
try {
|
|
522
578
|
printUserMessage(message);
|
|
523
|
-
// Animated thinking
|
|
579
|
+
// Animated thinking
|
|
524
580
|
var dots = 0;
|
|
525
581
|
var thinkFrames = ["thinking", "thinking.", "thinking..", "thinking..."];
|
|
526
|
-
console.log(""); // line for thinking text
|
|
527
582
|
var thinkTimer = setInterval(function() {
|
|
528
|
-
process.stdout.write("\x1b[s"); // save cursor
|
|
529
|
-
// Move to thinking line (above the UI)
|
|
530
|
-
if (uiStartRow > 0) process.stdout.write("\x1b[" + (uiStartRow + 1) + "A");
|
|
531
583
|
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " " + thinkFrames[dots % 4] + " " + C.reset);
|
|
532
|
-
process.stdout.write("\x1b[u"); // restore cursor
|
|
533
584
|
dots++;
|
|
534
585
|
}, 300);
|
|
535
|
-
// Redraw input box below thinking line
|
|
536
|
-
processing = false;
|
|
537
|
-
uiStartRow = -1;
|
|
538
|
-
drawPrompt();
|
|
539
|
-
processing = true;
|
|
540
586
|
|
|
541
587
|
var resp = await apiCall("POST", "/chat", {
|
|
542
588
|
message: message,
|
|
@@ -544,10 +590,7 @@ async function sendChat(message) {
|
|
|
544
590
|
});
|
|
545
591
|
|
|
546
592
|
clearInterval(thinkTimer);
|
|
547
|
-
|
|
548
|
-
eraseUI();
|
|
549
|
-
// Clear thinking line
|
|
550
|
-
process.stdout.write("\x1b[1A\r\x1b[2K");
|
|
593
|
+
process.stdout.write("\r\x1b[2K");
|
|
551
594
|
|
|
552
595
|
if (resp.status !== 200) {
|
|
553
596
|
printError(resp.data.error || "API Error " + resp.status);
|
|
@@ -568,7 +611,9 @@ async function sendChat(message) {
|
|
|
568
611
|
var tokInfo = C.dim + (inTok + outTok) + " tok" + C.reset;
|
|
569
612
|
var meta = resp.data.task_type + "/" + resp.data.role + " " + BOX.dot + " score: " + (q.score || "?") +
|
|
570
613
|
(q.retried ? " " + BOX.dot + " retried" : "") + " " + BOX.dot + " " + tokInfo;
|
|
571
|
-
|
|
614
|
+
|
|
615
|
+
// Streaming typewriter effect
|
|
616
|
+
await streamAnswer(resp.data.answer, meta);
|
|
572
617
|
|
|
573
618
|
// Auto Memory Dream Mode
|
|
574
619
|
dreamCounter++;
|
|
@@ -1463,56 +1508,133 @@ async function cmdAgent(args) {
|
|
|
1463
1508
|
var lastAnswer = "";
|
|
1464
1509
|
var goal = args;
|
|
1465
1510
|
var context = [];
|
|
1511
|
+
var workdir = config.workdir;
|
|
1512
|
+
|
|
1513
|
+
// Show thinking animation
|
|
1514
|
+
var thinkTimer = setInterval(function() {
|
|
1515
|
+
var phase = loop === 0 ? "planning" : "step " + loop + "/" + maxLoops;
|
|
1516
|
+
process.stdout.write("\r\x1b[2K" + C.dim + " " + BOX.bot + " [" + phase + "] working..." + C.reset);
|
|
1517
|
+
}, 300);
|
|
1466
1518
|
|
|
1467
1519
|
while (loop < maxLoops) {
|
|
1468
1520
|
loop++;
|
|
1469
|
-
// Phase indicator
|
|
1470
|
-
var phase = loop === 1 ? "planning" : "step " + loop;
|
|
1471
|
-
process.stdout.write("\r\x1b[K" + C.dim + " " + BOX.bot + " [" + phase + "/" + maxLoops + "] working..." + C.reset);
|
|
1472
1521
|
|
|
1473
1522
|
var resp = await apiCall("POST", "/agent", {
|
|
1474
1523
|
goal: goal,
|
|
1524
|
+
workdir: workdir,
|
|
1475
1525
|
context: context.slice(-5),
|
|
1476
1526
|
verbose: false
|
|
1477
1527
|
});
|
|
1478
|
-
process.stdout.write("\r\x1b[K");
|
|
1479
1528
|
|
|
1480
1529
|
if (resp.status !== 200) {
|
|
1530
|
+
clearInterval(thinkTimer);
|
|
1531
|
+
process.stdout.write("\r\x1b[2K");
|
|
1481
1532
|
printError("Step " + loop + " failed: " + (resp.data.error || "Error"));
|
|
1482
1533
|
break;
|
|
1483
1534
|
}
|
|
1484
1535
|
|
|
1485
1536
|
var d = resp.data;
|
|
1486
1537
|
lastAnswer = d.answer || "";
|
|
1487
|
-
|
|
1488
|
-
|
|
1538
|
+
|
|
1539
|
+
// Save files locally to workspace
|
|
1540
|
+
if (d.files && d.files.length > 0) {
|
|
1541
|
+
for (var fi = 0; fi < d.files.length; fi++) {
|
|
1542
|
+
var f = d.files[fi];
|
|
1543
|
+
var localPath = path.join(workdir, f.name);
|
|
1544
|
+
var localDir = path.dirname(localPath);
|
|
1545
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
1546
|
+
// If file has content directly, write it
|
|
1547
|
+
if (f.content) {
|
|
1548
|
+
fs.writeFileSync(localPath, f.content, "utf8");
|
|
1549
|
+
f.localPath = localPath;
|
|
1550
|
+
} else if (f.download) {
|
|
1551
|
+
// Download from server
|
|
1552
|
+
try {
|
|
1553
|
+
var dlResp = await apiCall("GET", f.download);
|
|
1554
|
+
if (dlResp.status === 200) {
|
|
1555
|
+
var content = typeof dlResp.data === "string" ? dlResp.data : JSON.stringify(dlResp.data, null, 2);
|
|
1556
|
+
fs.writeFileSync(localPath, content, "utf8");
|
|
1557
|
+
f.localPath = localPath;
|
|
1558
|
+
}
|
|
1559
|
+
} catch(dlErr) {}
|
|
1560
|
+
}
|
|
1561
|
+
totalFiles.push(f);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// If answer contains code blocks with filenames, extract and save them
|
|
1566
|
+
var codeBlockRe = /```(?:\w+)?\s*\n\/\/\s*(.+\.(?:html|css|js|json|md|txt|py))\n([\s\S]*?)```/g;
|
|
1567
|
+
var cbMatch;
|
|
1568
|
+
while ((cbMatch = codeBlockRe.exec(lastAnswer)) !== null) {
|
|
1569
|
+
var cbFile = cbMatch[1].trim();
|
|
1570
|
+
var cbContent = cbMatch[2];
|
|
1571
|
+
var cbPath = path.join(workdir, cbFile);
|
|
1572
|
+
var cbDir = path.dirname(cbPath);
|
|
1573
|
+
if (!fs.existsSync(cbDir)) fs.mkdirSync(cbDir, { recursive: true });
|
|
1574
|
+
fs.writeFileSync(cbPath, cbContent, "utf8");
|
|
1575
|
+
totalFiles.push({ name: cbFile, localPath: cbPath });
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Also extract filename: pattern like "**index.html**:" or "Datei: index.html"
|
|
1579
|
+
var fileHeaderRe = /(?:\*\*|Datei:\s*)([a-zA-Z0-9_\-/.]+\.(?:html|css|js|json|md|txt|py))\*?\*?:?\s*\n```(?:\w+)?\n([\s\S]*?)```/g;
|
|
1580
|
+
var fhMatch;
|
|
1581
|
+
while ((fhMatch = fileHeaderRe.exec(lastAnswer)) !== null) {
|
|
1582
|
+
var fhFile = fhMatch[1].trim();
|
|
1583
|
+
var fhContent = fhMatch[2];
|
|
1584
|
+
var fhPath = path.join(workdir, fhFile);
|
|
1585
|
+
var fhDir = path.dirname(fhPath);
|
|
1586
|
+
if (!fs.existsSync(fhDir)) fs.mkdirSync(fhDir, { recursive: true });
|
|
1587
|
+
if (!totalFiles.some(function(tf) { return tf.name === fhFile; })) {
|
|
1588
|
+
fs.writeFileSync(fhPath, fhContent, "utf8");
|
|
1589
|
+
totalFiles.push({ name: fhFile, localPath: fhPath });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
context.push({ step: loop, answer: lastAnswer.slice(0, 500), files: totalFiles.length, workdir: workdir });
|
|
1489
1594
|
|
|
1490
1595
|
// Check if the agent says it's done
|
|
1491
1596
|
var doneSignals = ["done", "complete", "finished", "fertig", "erledigt", "all steps", "nothing left"];
|
|
1492
1597
|
var isDone = doneSignals.some(function(s) { return lastAnswer.toLowerCase().includes(s); });
|
|
1493
1598
|
|
|
1494
|
-
// Show brief step result
|
|
1495
|
-
console.log(C.dim + " [" + loop + "] " + C.reset + C.green + (d.steps_executed || 1) + " steps" +
|
|
1496
|
-
(d.files && d.files.length > 0 ? ", " + d.files.length + " files" : "") + C.reset +
|
|
1497
|
-
(isDone ? C.yellow + " ✓ done" + C.reset : ""));
|
|
1498
|
-
|
|
1499
1599
|
if (isDone) break;
|
|
1500
1600
|
|
|
1501
1601
|
// Feed result back as next goal
|
|
1502
|
-
goal = "Continue with the original goal: " + args +
|
|
1503
|
-
"\
|
|
1602
|
+
goal = "Continue with the original goal: " + args +
|
|
1603
|
+
"\nWorkspace: " + workdir +
|
|
1604
|
+
"\nFiles created so far: " + totalFiles.map(function(f) { return f.name; }).join(", ") +
|
|
1605
|
+
"\n\nLast step result: " + lastAnswer.slice(0, 1000) +
|
|
1606
|
+
"\n\nWrite the actual file contents as code blocks. If everything is done, say 'done'.";
|
|
1504
1607
|
}
|
|
1505
1608
|
|
|
1609
|
+
clearInterval(thinkTimer);
|
|
1610
|
+
process.stdout.write("\r\x1b[2K");
|
|
1611
|
+
|
|
1506
1612
|
// Final summary
|
|
1507
1613
|
console.log("");
|
|
1508
|
-
var
|
|
1509
|
-
|
|
1614
|
+
var savedCount = totalFiles.filter(function(f) { return f.localPath; }).length;
|
|
1615
|
+
var meta = loop + " loops" + (totalFiles.length > 0 ? " " + BOX.dot + " " + savedCount + " files saved" : "");
|
|
1616
|
+
|
|
1617
|
+
// Token tracking
|
|
1618
|
+
var inTok = Math.ceil(args.length / 3.5);
|
|
1619
|
+
var outTok = Math.ceil(lastAnswer.length / 3.5);
|
|
1620
|
+
sessionCost.requests += loop;
|
|
1621
|
+
sessionCost.inputTokensEst += inTok * loop;
|
|
1622
|
+
sessionCost.outputTokensEst += outTok;
|
|
1623
|
+
|
|
1624
|
+
await streamAnswer(lastAnswer, meta);
|
|
1625
|
+
|
|
1510
1626
|
if (totalFiles.length > 0) {
|
|
1511
|
-
console.log(C.green + "
|
|
1512
|
-
totalFiles.forEach(function(f) {
|
|
1627
|
+
console.log(C.green + " 📁 Saved to workspace:" + C.reset);
|
|
1628
|
+
totalFiles.forEach(function(f) {
|
|
1629
|
+
if (f.localPath) console.log(" " + C.brightCyan + f.localPath + C.reset);
|
|
1630
|
+
else console.log(" " + C.dim + f.name + " (not saved)" + C.reset);
|
|
1631
|
+
});
|
|
1513
1632
|
console.log("");
|
|
1514
1633
|
}
|
|
1515
|
-
} catch(e) {
|
|
1634
|
+
} catch(e) {
|
|
1635
|
+
if (typeof thinkTimer !== "undefined") clearInterval(thinkTimer);
|
|
1636
|
+
printError(e.message);
|
|
1637
|
+
}
|
|
1516
1638
|
}
|
|
1517
1639
|
|
|
1518
1640
|
// ── Screenshot/Render (CLI wrapper) ──
|
|
@@ -2732,8 +2854,13 @@ async function main() {
|
|
|
2732
2854
|
await handleCommand(detected);
|
|
2733
2855
|
runHook("post", detected.split(/\s+/)[0].slice(1));
|
|
2734
2856
|
} else if (sessionMode === "agent") {
|
|
2735
|
-
// Agent mode:
|
|
2736
|
-
|
|
2857
|
+
// Agent mode: detect if question or task
|
|
2858
|
+
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());
|
|
2859
|
+
if (isQuestion) {
|
|
2860
|
+
await sendChat(input);
|
|
2861
|
+
} else {
|
|
2862
|
+
await cmdAgent(input);
|
|
2863
|
+
}
|
|
2737
2864
|
} else {
|
|
2738
2865
|
await sendChat(input);
|
|
2739
2866
|
}
|