blun-king-cli 5.3.5 → 5.3.7

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 (3) hide show
  1. package/blun-cli.js +265 -22
  2. package/package.json +1 -1
  3. package/runtime.js +15 -1
package/blun-cli.js CHANGED
@@ -112,6 +112,105 @@ function apiCall(method, endpoint, body) {
112
112
  });
113
113
  }
114
114
 
115
+ function downloadArtifactToWorkdir(downloadPath, filename) {
116
+ return new Promise(function(resolve, reject) {
117
+ if (!downloadPath || !filename) return resolve(null);
118
+
119
+ var url = new URL(config.api.base_url + downloadPath);
120
+ var isHttps = url.protocol === "https:";
121
+ var options = {
122
+ hostname: url.hostname,
123
+ port: url.port,
124
+ path: url.pathname + (url.search || ""),
125
+ method: "GET",
126
+ headers: {},
127
+ timeout: config.api.timeout
128
+ };
129
+
130
+ var authToken = config.api.key || config.auth.api_key;
131
+ if (authToken) {
132
+ options.headers["Authorization"] = "Bearer " + authToken;
133
+ } else if (config.auth.type === "oauth" && config.auth.oauth_token) {
134
+ options.headers["Authorization"] = "Bearer " + config.auth.oauth_token;
135
+ }
136
+
137
+ var mod = isHttps ? https : http;
138
+ var req = mod.request(options, function(res) {
139
+ if (res.statusCode !== 200) {
140
+ var errChunks = [];
141
+ res.on("data", function(c) { errChunks.push(c); });
142
+ res.on("end", function() {
143
+ reject(new Error("Artifact download failed: HTTP " + res.statusCode + (errChunks.length ? " " + Buffer.concat(errChunks).toString() : "")));
144
+ });
145
+ return;
146
+ }
147
+
148
+ var localPath = path.join(config.workdir, path.basename(filename));
149
+ var localDir = path.dirname(localPath);
150
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
151
+ var out = fs.createWriteStream(localPath);
152
+
153
+ res.pipe(out);
154
+ out.on("finish", function() {
155
+ out.close(function() {
156
+ try {
157
+ var stat = fs.statSync(localPath);
158
+ resolve({
159
+ name: path.basename(localPath),
160
+ path: localPath,
161
+ size: stat.size
162
+ });
163
+ } catch (statErr) {
164
+ reject(statErr);
165
+ }
166
+ });
167
+ });
168
+ out.on("error", function(err) {
169
+ out.destroy();
170
+ reject(err);
171
+ });
172
+ });
173
+
174
+ req.on("error", function(e) { reject(e); });
175
+ req.on("timeout", function() { req.destroy(); reject(new Error("Artifact download timeout")); });
176
+ req.end();
177
+ });
178
+ }
179
+
180
+ async function saveApiOutputs(receivedFiles, artifact) {
181
+ var saved = [];
182
+ var seen = new Set();
183
+
184
+ for (var fi = 0; fi < (receivedFiles || []).length; fi++) {
185
+ var f = receivedFiles[fi];
186
+ var relativeName = f && (f.name || f.path);
187
+ if (f && f.content && relativeName) {
188
+ var localPath = path.join(config.workdir, relativeName);
189
+ var localDir = path.dirname(localPath);
190
+ if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
191
+ fs.writeFileSync(localPath, f.content, "utf8");
192
+ var fileInfo = {
193
+ name: relativeName,
194
+ path: localPath,
195
+ size: typeof f.size === "number" ? f.size : Buffer.byteLength(String(f.content), "utf8")
196
+ };
197
+ saved.push(fileInfo);
198
+ seen.add(path.basename(relativeName));
199
+ }
200
+ }
201
+
202
+ if (artifact && artifact.download && artifact.filename && !seen.has(path.basename(artifact.filename))) {
203
+ try {
204
+ var downloaded = await downloadArtifactToWorkdir(artifact.download, artifact.filename);
205
+ if (downloaded) saved.push(downloaded);
206
+ } catch (artifactErr) {
207
+ printError("Artefakt konnte nicht lokal gespeichert werden: " + artifactErr.message);
208
+ }
209
+ }
210
+
211
+ return saved;
212
+ }
213
+
115
214
  // ── Chat History ──
116
215
  var chatHistory = [];
117
216
  var lastCreatedFiles = []; // Track files from last response for follow-up context
@@ -556,6 +655,11 @@ async function handleCommand(input) {
556
655
  await cmdAgent(args);
557
656
  break;
558
657
 
658
+ case "/tg":
659
+ case "/telegram":
660
+ await cmdTelegram(args);
661
+ break;
662
+
559
663
  case "/screenshot":
560
664
  await cmdScreenshot(args);
561
665
  break;
@@ -635,6 +739,7 @@ async function sendChat(message) {
635
739
 
636
740
  var fullAnswer = "";
637
741
  var receivedFiles = [];
742
+ var receivedArtifact = null;
638
743
  var meta = "";
639
744
 
640
745
  try {
@@ -655,6 +760,7 @@ async function sendChat(message) {
655
760
 
656
761
  fullAnswer = resp.data.answer || "";
657
762
  receivedFiles = resp.data.files || [];
763
+ receivedArtifact = resp.data.artifact || null;
658
764
  var q = resp.data.quality || {};
659
765
  meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
660
766
  (q.retried ? " " + BOX.dot + " retried" : "") +
@@ -679,19 +785,14 @@ async function sendChat(message) {
679
785
  sessionCost.outputTokensEst += outTok;
680
786
 
681
787
  // ── Save received files to local workdir ──
682
- if (receivedFiles.length > 0) {
788
+ var savedOutputs = await saveApiOutputs(receivedFiles, receivedArtifact);
789
+ if (savedOutputs.length > 0) {
683
790
  console.log(C.green + " \uD83D\uDCC1 Dateien gespeichert:" + C.reset);
684
791
  lastCreatedFiles = [];
685
- for (var fi = 0; fi < receivedFiles.length; fi++) {
686
- var f = receivedFiles[fi];
687
- if (f.content && f.name) {
688
- var localPath = path.join(config.workdir, f.name);
689
- var localDir = path.dirname(localPath);
690
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
691
- fs.writeFileSync(localPath, f.content, "utf8");
692
- console.log(" " + C.brightCyan + localPath + C.reset + C.dim + " (" + f.size + " bytes)" + C.reset);
693
- lastCreatedFiles.push({ name: f.name, path: localPath, size: f.size });
694
- }
792
+ for (var fi = 0; fi < savedOutputs.length; fi++) {
793
+ var f = savedOutputs[fi];
794
+ console.log(" " + C.brightCyan + f.path + C.reset + C.dim + " (" + f.size + " bytes)" + C.reset);
795
+ lastCreatedFiles.push({ name: f.name, path: f.path, size: f.size });
695
796
  }
696
797
  console.log("");
697
798
  }
@@ -1004,6 +1105,21 @@ function cmdSet(args) {
1004
1105
  break;
1005
1106
 
1006
1107
  default:
1108
+ // Handle plugin.X.Y settings
1109
+ if (key && key.startsWith("plugin.")) {
1110
+ var pluginParts = key.split(".");
1111
+ if (pluginParts.length === 3) {
1112
+ var pName = pluginParts[1];
1113
+ var pField = pluginParts[2];
1114
+ var plugins = loadPlugins();
1115
+ if (!plugins[pName]) { printError("Plugin '" + pName + "' not installed. /plugin add " + pName); break; }
1116
+ if (!plugins[pName].config) plugins[pName].config = {};
1117
+ plugins[pName].config[pField] = val;
1118
+ savePlugins(plugins);
1119
+ printSuccess("Plugin " + pName + "." + pField + " = " + (pField.includes("token") ? "***" + val.slice(-6) : val));
1120
+ break;
1121
+ }
1122
+ }
1007
1123
  printError("Unknown setting. Use /settings to see all options.");
1008
1124
  }
1009
1125
  }
@@ -1542,6 +1658,8 @@ function cmdPlugin(args) {
1542
1658
  if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
1543
1659
  plugins[parts[1]].running = true;
1544
1660
  savePlugins(plugins);
1661
+ // Actually start built-in plugins
1662
+ if (parts[1] === "telegram") { ensureTgBot(); }
1545
1663
  printSuccess("Plugin '" + parts[1] + "' started.");
1546
1664
 
1547
1665
  } else if (action === "stop" && parts[1]) {
@@ -1555,6 +1673,134 @@ function cmdPlugin(args) {
1555
1673
  }
1556
1674
  }
1557
1675
 
1676
+ // ── Telegram Plugin ──
1677
+ var _tgBot = null;
1678
+ var _tgMessages = [];
1679
+ var _tgMaxMessages = 50;
1680
+
1681
+ function getTgConfig() {
1682
+ var plugins = loadPlugins();
1683
+ var tg = plugins.telegram;
1684
+ if (!tg || !tg.config) return null;
1685
+ return tg.config;
1686
+ }
1687
+
1688
+ function ensureTgBot() {
1689
+ if (_tgBot) return _tgBot;
1690
+ var cfg = getTgConfig();
1691
+ if (!cfg || !cfg.bot_token) {
1692
+ printError("Telegram not configured. Run: /plugin add telegram");
1693
+ printInfo("Then set token: /set plugin.telegram.bot_token <YOUR_BOT_TOKEN>");
1694
+ printInfo("And chat ID: /set plugin.telegram.chat_id <YOUR_CHAT_ID>");
1695
+ return null;
1696
+ }
1697
+ try {
1698
+ var TelegramBot = require("node-telegram-bot-api");
1699
+ _tgBot = new TelegramBot(cfg.bot_token, { polling: true });
1700
+ _tgBot.on("message", function(msg) {
1701
+ _tgMessages.push({
1702
+ id: msg.message_id,
1703
+ from: msg.from ? (msg.from.first_name || msg.from.username || "?") : "?",
1704
+ text: msg.text || "(media)",
1705
+ date: new Date(msg.date * 1000).toLocaleTimeString(),
1706
+ chat_id: String(msg.chat.id)
1707
+ });
1708
+ if (_tgMessages.length > _tgMaxMessages) _tgMessages.shift();
1709
+ });
1710
+ printSuccess("Telegram Bot connected.");
1711
+ return _tgBot;
1712
+ } catch(e) {
1713
+ printError("Telegram connection failed: " + e.message);
1714
+ return null;
1715
+ }
1716
+ }
1717
+
1718
+ async function cmdTelegram(args) {
1719
+ var parts = (args || "").trim().split(/\s+/);
1720
+ var action = parts[0] || "status";
1721
+
1722
+ if (action === "status") {
1723
+ var cfg = getTgConfig();
1724
+ if (!cfg || !cfg.bot_token) {
1725
+ printError("Telegram not configured.");
1726
+ printInfo(" /plugin add telegram");
1727
+ printInfo(" /set plugin.telegram.bot_token <TOKEN>");
1728
+ printInfo(" /set plugin.telegram.chat_id <CHAT_ID>");
1729
+ } else {
1730
+ console.log("");
1731
+ console.log(C.bold + " Telegram Plugin" + C.reset);
1732
+ console.log(" Token: " + C.green + (cfg.bot_token ? "***" + cfg.bot_token.slice(-6) : "not set") + C.reset);
1733
+ console.log(" Chat: " + C.green + (cfg.chat_id || "not set") + C.reset);
1734
+ console.log(" Bot: " + (_tgBot ? C.green + "connected" : C.red + "disconnected") + C.reset);
1735
+ console.log(" Messages buffered: " + _tgMessages.length);
1736
+ console.log("");
1737
+ }
1738
+
1739
+ } else if (action === "connect" || action === "start") {
1740
+ ensureTgBot();
1741
+
1742
+ } else if (action === "send") {
1743
+ var cfg = getTgConfig();
1744
+ if (!cfg || !cfg.chat_id) { printError("No chat_id set. /set plugin.telegram.chat_id <ID>"); return; }
1745
+ var bot = ensureTgBot();
1746
+ if (!bot) return;
1747
+ var text = parts.slice(1).join(" ");
1748
+ if (!text) { printError("Usage: /tg send <message>"); return; }
1749
+ try {
1750
+ await bot.sendMessage(cfg.chat_id, text);
1751
+ printSuccess("Message sent.");
1752
+ } catch(e) {
1753
+ printError("Send failed: " + e.message);
1754
+ }
1755
+
1756
+ } else if (action === "read" || action === "messages" || action === "inbox") {
1757
+ if (_tgMessages.length === 0) {
1758
+ printInfo("No messages yet. Connect first: /tg connect");
1759
+ return;
1760
+ }
1761
+ console.log("");
1762
+ console.log(C.bold + " Last " + Math.min(_tgMessages.length, 20) + " messages:" + C.reset);
1763
+ _tgMessages.slice(-20).forEach(function(m) {
1764
+ console.log(C.dim + " [" + m.date + "] " + C.reset + C.brightCyan + m.from + C.reset + ": " + m.text);
1765
+ });
1766
+ console.log("");
1767
+
1768
+ } else if (action === "disconnect" || action === "stop") {
1769
+ if (_tgBot) {
1770
+ _tgBot.stopPolling();
1771
+ _tgBot = null;
1772
+ printSuccess("Telegram disconnected.");
1773
+ } else {
1774
+ printInfo("Not connected.");
1775
+ }
1776
+
1777
+ } else if (action === "file" || action === "photo") {
1778
+ var cfg = getTgConfig();
1779
+ if (!cfg || !cfg.chat_id) { printError("No chat_id set."); return; }
1780
+ var bot = ensureTgBot();
1781
+ if (!bot) return;
1782
+ var filePath = parts.slice(1).join(" ");
1783
+ if (!filePath || !fs.existsSync(filePath)) { printError("File not found: " + filePath); return; }
1784
+ try {
1785
+ await bot.sendDocument(cfg.chat_id, filePath);
1786
+ printSuccess("File sent: " + path.basename(filePath));
1787
+ } catch(e) {
1788
+ printError("Send failed: " + e.message);
1789
+ }
1790
+
1791
+ } else {
1792
+ console.log("");
1793
+ console.log(C.bold + " Telegram Commands:" + C.reset);
1794
+ console.log(" /tg status Show connection status");
1795
+ console.log(" /tg connect Connect to Telegram");
1796
+ console.log(" /tg send <msg> Send a message");
1797
+ console.log(" /tg read Show received messages");
1798
+ console.log(" /tg file <path> Send a file/photo");
1799
+ console.log(" /tg disconnect Disconnect");
1800
+ console.log("");
1801
+ }
1802
+ }
1803
+
1558
1804
  function cmdPermissions(args) {
1559
1805
  var perms = loadPermissions();
1560
1806
  if (!args) {
@@ -1619,17 +1865,14 @@ async function cmdAgent(args) {
1619
1865
  lastAnswer = d.answer || "";
1620
1866
 
1621
1867
  // Save files locally to workspace — API now returns file contents
1622
- if (d.files && d.files.length > 0) {
1623
- for (var fi = 0; fi < d.files.length; fi++) {
1624
- var f = d.files[fi];
1625
- if (f.content && f.name) {
1626
- var localPath = path.join(workdir, f.name);
1627
- var localDir = path.dirname(localPath);
1628
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
1629
- fs.writeFileSync(localPath, f.content, "utf8");
1630
- f.localPath = localPath;
1631
- }
1632
- totalFiles.push(f);
1868
+ var savedStepOutputs = await saveApiOutputs(d.files || [], d.artifact || null);
1869
+ if (savedStepOutputs.length > 0) {
1870
+ for (var fi = 0; fi < savedStepOutputs.length; fi++) {
1871
+ totalFiles.push({
1872
+ name: savedStepOutputs[fi].name,
1873
+ localPath: savedStepOutputs[fi].path,
1874
+ size: savedStepOutputs[fi].size
1875
+ });
1633
1876
  }
1634
1877
  }
1635
1878
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "5.3.5",
3
+ "version": "5.3.7",
4
4
  "description": "BLUN King CLI — Your local AI assistant powered by Gemma4",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/runtime.js CHANGED
@@ -56,6 +56,10 @@ function clampConfidence(score) {
56
56
  return Math.max(0, Math.min(1, score / 8));
57
57
  }
58
58
 
59
+ function hasResolvableUrl(raw) {
60
+ return /(https?:\/\/\S+)|(?:^|[\s<(])(?:www\.)?[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s)>"']*)?/i.test(String(raw || ""));
61
+ }
62
+
59
63
  function detectIntent(raw, session) {
60
64
  const text = normalize(raw);
61
65
  const activeTask = session?.activeTask || null;
@@ -226,6 +230,12 @@ function detectIntent(raw, session) {
226
230
  const wantsDebug = hasAny(text, debugTerms);
227
231
  const wantsOperator = hasAny(text, operatorTerms);
228
232
  const wantsCoding = hasAny(text, codingTerms);
233
+ const hasUrl = hasResolvableUrl(raw);
234
+ const screenshotStorageComplaint =
235
+ activeTask?.type === "browser_capture" &&
236
+ wantsScreenshot &&
237
+ !hasUrl &&
238
+ /\b(ordner|datei|gespeichert|download|liegt|finde|fehlt|nicht im ordner|nicht da)\b/i.test(text);
229
239
 
230
240
  const scores = {
231
241
  installation:
@@ -233,7 +243,7 @@ function detectIntent(raw, session) {
233
243
  scoreApproxWords(text, ["installier", "installieren", "telegram", "telegramm", "plugin", "skill", "playwright"], 1.5) +
234
244
  scoreApproxPhrases(text, ["telegram bot", "playwright"], 2),
235
245
  browser_capture:
236
- (wantsWebsite ? 2 : scoreApproxWords(text, ["website", "webseite"], 1.5)) +
246
+ ((wantsWebsite || hasUrl) ? 2 : scoreApproxWords(text, ["website", "webseite"], 1.5)) +
237
247
  (wantsScreenshot ? 3 : scoreApproxWords(text, ["screenshot", "screen"], 1.5)),
238
248
  website_builder:
239
249
  (wantsWebsite ? 2 : scoreApproxWords(text, ["website", "webseite", "landingpage", "homepage"], 1.5)) +
@@ -256,6 +266,10 @@ function detectIntent(raw, session) {
256
266
  return { kind: "followup", activeTask, scores, confidence: 1 };
257
267
  }
258
268
 
269
+ if (screenshotStorageComplaint) {
270
+ return { kind: "followup", activeTask, scores, confidence: 1 };
271
+ }
272
+
259
273
  if (/^(hi|hey|hallo|moin|servus|yo|na|guten morgen|guten abend)\b/.test(text) && text.split(" ").length <= 5) {
260
274
  return { kind: "chat", scores, confidence: 1 };
261
275
  }