blun-king-cli 3.0.7 → 4.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.
Files changed (2) hide show
  1. package/bin/blun.js +192 -34
  2. package/package.json +1 -1
package/bin/blun.js CHANGED
@@ -581,11 +581,46 @@ var _globalEraseUI = null;
581
581
  var _globalUILines = 5; // how many lines the input box takes up
582
582
 
583
583
  // ── Chat ──
584
+ // ── SSE Stream Request ──
585
+ function apiStreamCall(endpoint, body) {
586
+ return new Promise(function(resolve, reject) {
587
+ var url = new URL(config.api.base_url + endpoint);
588
+ var isHttps = url.protocol === "https:";
589
+ var options = {
590
+ hostname: url.hostname, port: url.port, path: url.pathname,
591
+ method: "POST",
592
+ headers: { "Content-Type": "application/json" }
593
+ };
594
+ var authToken = config.api.key || config.auth.api_key;
595
+ if (authToken) options.headers["Authorization"] = "Bearer " + authToken;
596
+
597
+ var data = JSON.stringify(body);
598
+ options.headers["Content-Length"] = Buffer.byteLength(data);
599
+
600
+ var mod = isHttps ? https : http;
601
+ var req = mod.request(options, function(res) {
602
+ if (res.statusCode !== 200) {
603
+ var chunks = [];
604
+ res.on("data", function(c) { chunks.push(c); });
605
+ res.on("end", function() {
606
+ try { reject(new Error(JSON.parse(Buffer.concat(chunks).toString()).error || "API Error " + res.statusCode)); }
607
+ catch(e) { reject(new Error("API Error " + res.statusCode)); }
608
+ });
609
+ return;
610
+ }
611
+ resolve(res);
612
+ });
613
+ req.on("error", function(e) { reject(e); });
614
+ req.write(data);
615
+ req.end();
616
+ });
617
+ }
618
+
584
619
  async function sendChat(message) {
585
620
  try {
586
621
  printUserMessage(message);
587
622
 
588
- // Show thinking status on a single line (no input box during thinking — simpler, more reliable)
623
+ // Show thinking animation
589
624
  var dots = 0;
590
625
  var thinkFrames = ["thinking", "thinking.", "thinking..", "thinking..."];
591
626
  console.log("");
@@ -594,36 +629,170 @@ async function sendChat(message) {
594
629
  dots++;
595
630
  }, 300);
596
631
 
597
- var resp = await apiCall("POST", "/chat", {
598
- message: message,
599
- history: chatHistory.slice(-10)
600
- });
632
+ // Try streaming first, fall back to non-streaming
633
+ var useStreaming = true;
634
+ var fullAnswer = "";
635
+ var receivedFiles = [];
636
+ var meta = "";
637
+ var headerPrinted = false;
601
638
 
602
- clearInterval(thinkTimer);
603
- process.stdout.write("\r\x1b[2K");
639
+ try {
640
+ var stream = await apiStreamCall("/chat/stream", {
641
+ message: message,
642
+ workdir: config.workdir,
643
+ history: chatHistory.slice(-10)
644
+ });
604
645
 
605
- if (resp.status !== 200) {
606
- printError(resp.data.error || "API Error " + resp.status);
607
- return;
646
+ clearInterval(thinkTimer);
647
+ process.stdout.write("\r\x1b[2K");
648
+
649
+ var buffer = "";
650
+ var tokenBuffer = "";
651
+ var flushTimer = null;
652
+
653
+ await new Promise(function(resolve, reject) {
654
+ stream.on("data", function(chunk) {
655
+ buffer += chunk.toString();
656
+ var lines = buffer.split("\n");
657
+ buffer = lines.pop() || "";
658
+
659
+ for (var i = 0; i < lines.length; i++) {
660
+ var line = lines[i].trim();
661
+ if (line.startsWith("event: ")) {
662
+ var eventType = line.slice(7);
663
+ // Next line should be data:
664
+ i++;
665
+ if (i < lines.length && lines[i].trim().startsWith("data: ")) {
666
+ var dataStr = lines[i].trim().slice(6);
667
+ try {
668
+ var eventData = JSON.parse(dataStr);
669
+ handleSSEEvent(eventType, eventData);
670
+ } catch(e) {}
671
+ }
672
+ }
673
+ }
674
+ });
675
+
676
+ stream.on("end", function() {
677
+ // Flush remaining token buffer
678
+ if (tokenBuffer.length > 0) {
679
+ process.stdout.write(tokenBuffer);
680
+ tokenBuffer = "";
681
+ }
682
+ resolve();
683
+ });
684
+
685
+ stream.on("error", function(e) { reject(e); });
686
+
687
+ function handleSSEEvent(type, data) {
688
+ switch(type) {
689
+ case "meta":
690
+ // Print header
691
+ if (!headerPrinted) {
692
+ console.log("");
693
+ console.log(C.green + C.bold + " " + BOX.bot + " BLUN King" + C.reset +
694
+ C.gray + " " + BOX.dot + " " + (data.task_type || "") + "/" + (data.role || "") + C.reset);
695
+ console.log(C.green + " " + BOX.h.repeat(40) + C.reset);
696
+ process.stdout.write(" ");
697
+ headerPrinted = true;
698
+ }
699
+ break;
700
+
701
+ case "token":
702
+ fullAnswer += data.text;
703
+ // Write token directly to stdout for live streaming
704
+ process.stdout.write(data.text);
705
+ break;
706
+
707
+ case "retry":
708
+ process.stdout.write("\n" + C.yellow + " ↻ retry: " + (data.reason || []).join(", ") + C.reset + "\n ");
709
+ fullAnswer = "";
710
+ break;
711
+
712
+ case "tool":
713
+ process.stdout.write("\n" + C.cyan + " ⚙ " + data.name + C.reset + "\n ");
714
+ break;
715
+
716
+ case "tool_result":
717
+ process.stdout.write(C.dim + " → " + (data.result || "").slice(0, 100) + C.reset + "\n ");
718
+ break;
719
+
720
+ case "files":
721
+ receivedFiles = data.files || [];
722
+ break;
723
+
724
+ case "done":
725
+ meta = "score: " + ((data.quality || {}).score || "?") +
726
+ (data.status ? " " + BOX.dot + " " + data.status : "");
727
+ break;
728
+
729
+ case "error":
730
+ process.stdout.write("\n" + C.red + " ERROR: " + data.error + C.reset + "\n");
731
+ break;
732
+ }
733
+ }
734
+ });
735
+
736
+ process.stdout.write("\n\n");
737
+
738
+ } catch(streamErr) {
739
+ // Fallback to non-streaming
740
+ useStreaming = false;
741
+ clearInterval(thinkTimer);
742
+ process.stdout.write("\r\x1b[2K");
743
+
744
+ var resp = await apiCall("POST", "/chat", {
745
+ message: message,
746
+ workdir: config.workdir,
747
+ history: chatHistory.slice(-10)
748
+ });
749
+
750
+ if (resp.status !== 200) {
751
+ printError(resp.data.error || "API Error " + resp.status);
752
+ return;
753
+ }
754
+
755
+ fullAnswer = resp.data.answer || "";
756
+ receivedFiles = resp.data.files || [];
757
+ var q = resp.data.quality || {};
758
+ meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
759
+ (q.retried ? " " + BOX.dot + " retried" : "") +
760
+ (resp.data.status ? " " + BOX.dot + " " + resp.data.status : "");
761
+
762
+ await streamAnswer(fullAnswer, meta);
608
763
  }
609
764
 
610
765
  chatHistory.push({ role: "user", content: message });
611
- chatHistory.push({ role: "assistant", content: resp.data.answer });
766
+ chatHistory.push({ role: "assistant", content: fullAnswer });
612
767
 
613
768
  // Token tracking
614
769
  var inTok = Math.ceil(message.length / 3.5);
615
- var outTok = Math.ceil((resp.data.answer || "").length / 3.5);
770
+ var outTok = Math.ceil(fullAnswer.length / 3.5);
616
771
  sessionCost.requests++;
617
772
  sessionCost.inputTokensEst += inTok;
618
773
  sessionCost.outputTokensEst += outTok;
619
774
 
620
- var q = resp.data.quality || {};
621
- var tokInfo = C.dim + (inTok + outTok) + " tok" + C.reset;
622
- var meta = resp.data.task_type + "/" + resp.data.role + " " + BOX.dot + " score: " + (q.score || "?") +
623
- (q.retried ? " " + BOX.dot + " retried" : "") + " " + BOX.dot + " " + tokInfo;
775
+ // ── Save received files to local workdir ──
776
+ if (receivedFiles.length > 0) {
777
+ console.log(C.green + " 📁 Dateien gespeichert:" + C.reset);
778
+ for (var fi = 0; fi < receivedFiles.length; fi++) {
779
+ var f = receivedFiles[fi];
780
+ if (f.content && f.name) {
781
+ var localPath = path.join(config.workdir, f.name);
782
+ var localDir = path.dirname(localPath);
783
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
784
+ fs.writeFileSync(localPath, f.content, "utf8");
785
+ console.log(" " + C.brightCyan + localPath + C.reset + C.dim + " (" + f.size + " bytes)" + C.reset);
786
+ }
787
+ }
788
+ console.log("");
789
+ }
624
790
 
625
- // Streaming typewriter effect
626
- await streamAnswer(resp.data.answer, meta);
791
+ // Show meta for streaming mode
792
+ if (useStreaming && meta) {
793
+ console.log(C.dim + " " + meta + C.reset);
794
+ console.log("");
795
+ }
627
796
 
628
797
  // Auto Memory Dream Mode
629
798
  dreamCounter++;
@@ -1547,27 +1716,16 @@ async function cmdAgent(args) {
1547
1716
  var d = resp.data;
1548
1717
  lastAnswer = d.answer || "";
1549
1718
 
1550
- // Save files locally to workspace
1719
+ // Save files locally to workspace — API now returns file contents
1551
1720
  if (d.files && d.files.length > 0) {
1552
1721
  for (var fi = 0; fi < d.files.length; fi++) {
1553
1722
  var f = d.files[fi];
1554
- var localPath = path.join(workdir, f.name);
1555
- var localDir = path.dirname(localPath);
1556
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
1557
- // If file has content directly, write it
1558
- if (f.content) {
1723
+ if (f.content && f.name) {
1724
+ var localPath = path.join(workdir, f.name);
1725
+ var localDir = path.dirname(localPath);
1726
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
1559
1727
  fs.writeFileSync(localPath, f.content, "utf8");
1560
1728
  f.localPath = localPath;
1561
- } else if (f.download) {
1562
- // Download from server
1563
- try {
1564
- var dlResp = await apiCall("GET", f.download);
1565
- if (dlResp.status === 200) {
1566
- var content = typeof dlResp.data === "string" ? dlResp.data : JSON.stringify(dlResp.data, null, 2);
1567
- fs.writeFileSync(localPath, content, "utf8");
1568
- f.localPath = localPath;
1569
- }
1570
- } catch(dlErr) {}
1571
1729
  }
1572
1730
  totalFiles.push(f);
1573
1731
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "3.0.7",
3
+ "version": "4.0.0",
4
4
  "description": "BLUN King CLI — Premium KI Console",
5
5
  "bin": {
6
6
  "blun": "./bin/blun.js"