blun-king-cli 5.3.6 → 5.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/blun-cli.js CHANGED
@@ -7,6 +7,12 @@ const { execSync, spawn } = require("child_process");
7
7
  const https = require("https");
8
8
  const http = require("http");
9
9
 
10
+ if (process.platform === "win32") {
11
+ try {
12
+ execSync("chcp 65001>nul", { stdio: "ignore", shell: process.env.ComSpec || "cmd.exe" });
13
+ } catch (e) {}
14
+ }
15
+
10
16
  // ── Config ──
11
17
  var _rawHome = process.env.BLUN_HOME || process.env.REAL_HOME || require("os").homedir();
12
18
  // Strip .claude-telegram-profile suffix if present (Claude Code Telegram session override)
@@ -112,16 +118,15 @@ function apiCall(method, endpoint, body) {
112
118
  });
113
119
  }
114
120
 
115
- function downloadArtifactToWorkdir(downloadPath, filename) {
121
+ function downloadBinary(urlOrPath) {
116
122
  return new Promise(function(resolve, reject) {
117
- if (!downloadPath || !filename) return resolve(null);
118
-
119
- var url = new URL(config.api.base_url + downloadPath);
123
+ var absolute = new URL(String(urlOrPath || ""), config.api.base_url).toString();
124
+ var url = new URL(absolute);
120
125
  var isHttps = url.protocol === "https:";
121
126
  var options = {
122
127
  hostname: url.hostname,
123
128
  port: url.port,
124
- path: url.pathname + (url.search || ""),
129
+ path: url.pathname + url.search,
125
130
  method: "GET",
126
131
  headers: {},
127
132
  timeout: config.api.timeout
@@ -136,84 +141,158 @@ function downloadArtifactToWorkdir(downloadPath, filename) {
136
141
 
137
142
  var mod = isHttps ? https : http;
138
143
  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);
144
+ var chunks = [];
145
+ res.on("data", function(c) { chunks.push(c); });
146
+ res.on("end", function() {
147
+ var buffer = Buffer.concat(chunks);
148
+ if ((res.statusCode || 500) >= 400) {
149
+ return reject(new Error("HTTP " + res.statusCode + " " + buffer.toString("utf8").slice(0, 400)));
150
+ }
151
+ resolve({ buffer: buffer, contentType: res.headers["content-type"] || "", url: absolute });
171
152
  });
172
153
  });
173
-
174
154
  req.on("error", function(e) { reject(e); });
175
- req.on("timeout", function() { req.destroy(); reject(new Error("Artifact download timeout")); });
155
+ req.on("timeout", function() { req.destroy(); reject(new Error("Timeout")); });
176
156
  req.end();
177
157
  });
178
158
  }
179
159
 
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));
160
+ function ensureLocalDir(filePath) {
161
+ var dir = path.dirname(filePath);
162
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
163
+ }
164
+
165
+ function saveTextFileToWorkdir(fileName, content) {
166
+ var localPath = path.join(config.workdir, fileName);
167
+ ensureLocalDir(localPath);
168
+ fs.writeFileSync(localPath, content, "utf8");
169
+ return localPath;
170
+ }
171
+
172
+ function printSavedFiles(savedFiles) {
173
+ if (!savedFiles.length) return;
174
+ console.log(C.green + " Dateien gespeichert:" + C.reset);
175
+ savedFiles.forEach(function(file) {
176
+ var sizeLabel = typeof file.size === "number" ? C.dim + " (" + file.size + " bytes)" + C.reset : "";
177
+ console.log(" " + C.brightCyan + file.path + C.reset + sizeLabel);
178
+ });
179
+ console.log("");
180
+ }
181
+
182
+ function rememberBrowserContext(snapshot, extra) {
183
+ var snap = snapshot || {};
184
+ lastBrowserContext = {
185
+ url: snap.url || (lastBrowserContext && lastBrowserContext.url) || "",
186
+ title: snap.title || (lastBrowserContext && lastBrowserContext.title) || "",
187
+ text: String(snap.text || (lastBrowserContext && lastBrowserContext.text) || "").slice(0, 2000),
188
+ updatedAt: new Date().toISOString()
189
+ };
190
+ if (extra && typeof extra === "object") {
191
+ Object.keys(extra).forEach(function(key) {
192
+ lastBrowserContext[key] = extra[key];
193
+ });
194
+ }
195
+ return lastBrowserContext;
196
+ }
197
+
198
+ function browserContextPrompt(input) {
199
+ if (!lastBrowserContext || !lastBrowserContext.url) return input;
200
+ return input + "\n\n[Browser-Kontext: " + (lastBrowserContext.title || lastBrowserContext.url) + " - " + String(lastBrowserContext.text || "").slice(0, 1000) + "\nURL: " + lastBrowserContext.url + "]";
201
+ }
202
+
203
+ function rememberBrowserAction(type, payload) {
204
+ lastBrowserAction = {
205
+ type: type,
206
+ payload: payload || {},
207
+ updatedAt: new Date().toISOString()
208
+ };
209
+ return lastBrowserAction;
210
+ }
211
+
212
+ async function saveArtifactToWorkdir(artifact, references) {
213
+ if (!artifact) return [];
214
+
215
+ var referenceList = Array.isArray(references) ? references : [];
216
+ var downloadUrl = artifact.download || artifact.url || "";
217
+ var fileName = artifact.filename || artifact.name || "";
218
+
219
+ if (!fileName && downloadUrl) {
220
+ try {
221
+ fileName = path.basename(new URL(downloadUrl, config.api.base_url).pathname) || "";
222
+ } catch (e) {}
223
+ }
224
+
225
+ if (!fileName) {
226
+ fileName = "artifact-" + Date.now();
227
+ }
228
+
229
+ var localPath = path.join(config.workdir, fileName);
230
+ ensureLocalDir(localPath);
231
+
232
+ if (artifact.content && !artifact.encoding) {
233
+ fs.writeFileSync(localPath, artifact.content, "utf8");
234
+ return [{ name: fileName, path: localPath, size: Buffer.byteLength(artifact.content, "utf8") }];
235
+ }
236
+
237
+ try {
238
+ if (!downloadUrl) throw new Error("No artifact download URL provided.");
239
+ var result = await downloadBinary(downloadUrl);
240
+ fs.writeFileSync(localPath, result.buffer);
241
+ return [{ name: fileName, path: localPath, size: result.buffer.length }];
242
+ } catch (downloadErr) {
243
+ var screenshotRef = referenceList.find(function(ref) { return ref && ref.url; });
244
+ if (/\.(png|jpg|jpeg|webp)$/i.test(fileName) && screenshotRef) {
245
+ var bc = require(path.join(__dirname, "browser-controller"));
246
+ try {
247
+ await bc.open(screenshotRef.url);
248
+ await bc.screenshot(localPath);
249
+ return [{ name: fileName, path: localPath, size: fs.statSync(localPath).size }];
250
+ } finally {
251
+ await bc.close().catch(function() {});
252
+ }
199
253
  }
254
+ throw new Error("Artifact download failed: " + downloadErr.message);
200
255
  }
256
+ }
257
+
258
+ async function saveApiOutputs(receivedFiles, artifact, references) {
259
+ var savedFiles = [];
260
+ var files = Array.isArray(receivedFiles) ? receivedFiles : [];
201
261
 
202
- if (artifact && artifact.download && artifact.filename && !seen.has(path.basename(artifact.filename))) {
262
+ for (var fi = 0; fi < files.length; fi++) {
263
+ var f = files[fi];
264
+ if (f && f.content && f.name) {
265
+ var localPath = saveTextFileToWorkdir(f.name, f.content);
266
+ savedFiles.push({ name: f.name, path: localPath, size: f.size || Buffer.byteLength(f.content, "utf8") });
267
+ }
268
+ }
269
+
270
+ if (artifact) {
203
271
  try {
204
- var downloaded = await downloadArtifactToWorkdir(artifact.download, artifact.filename);
205
- if (downloaded) saved.push(downloaded);
272
+ var artifactFiles = await saveArtifactToWorkdir(artifact, references);
273
+ artifactFiles.forEach(function(file) {
274
+ if (!savedFiles.some(function(existing) { return existing.path === file.path; })) {
275
+ savedFiles.push(file);
276
+ }
277
+ });
206
278
  } catch (artifactErr) {
207
279
  printError("Artefakt konnte nicht lokal gespeichert werden: " + artifactErr.message);
208
280
  }
209
281
  }
210
282
 
211
- return saved;
283
+ if (savedFiles.length) {
284
+ lastCreatedFiles = savedFiles.slice();
285
+ printSavedFiles(savedFiles);
286
+ }
287
+
288
+ return savedFiles;
212
289
  }
213
290
 
214
291
  // ── Chat History ──
215
292
  var chatHistory = [];
216
293
  var lastCreatedFiles = []; // Track files from last response for follow-up context
294
+ var lastBrowserContext = null; // Persist browser follow-up context even after local browser closes
295
+ var lastBrowserAction = null; // Repeatable local browser action state
217
296
 
218
297
  // ── Print Helpers ──
219
298
  function printHeader() {
@@ -655,6 +734,11 @@ async function handleCommand(input) {
655
734
  await cmdAgent(args);
656
735
  break;
657
736
 
737
+ case "/tg":
738
+ case "/telegram":
739
+ await cmdTelegram(args);
740
+ break;
741
+
658
742
  case "/screenshot":
659
743
  await cmdScreenshot(args);
660
744
  break;
@@ -734,7 +818,6 @@ async function sendChat(message) {
734
818
 
735
819
  var fullAnswer = "";
736
820
  var receivedFiles = [];
737
- var receivedArtifact = null;
738
821
  var meta = "";
739
822
 
740
823
  try {
@@ -755,7 +838,6 @@ async function sendChat(message) {
755
838
 
756
839
  fullAnswer = resp.data.answer || "";
757
840
  receivedFiles = resp.data.files || [];
758
- receivedArtifact = resp.data.artifact || null;
759
841
  var q = resp.data.quality || {};
760
842
  meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
761
843
  (q.retried ? " " + BOX.dot + " retried" : "") +
@@ -779,18 +861,7 @@ async function sendChat(message) {
779
861
  sessionCost.inputTokensEst += inTok;
780
862
  sessionCost.outputTokensEst += outTok;
781
863
 
782
- // ── Save received files to local workdir ──
783
- var savedOutputs = await saveApiOutputs(receivedFiles, receivedArtifact);
784
- if (savedOutputs.length > 0) {
785
- console.log(C.green + " \uD83D\uDCC1 Dateien gespeichert:" + C.reset);
786
- lastCreatedFiles = [];
787
- for (var fi = 0; fi < savedOutputs.length; fi++) {
788
- var f = savedOutputs[fi];
789
- console.log(" " + C.brightCyan + f.path + C.reset + C.dim + " (" + f.size + " bytes)" + C.reset);
790
- lastCreatedFiles.push({ name: f.name, path: f.path, size: f.size });
791
- }
792
- console.log("");
793
- }
864
+ await saveApiOutputs(receivedFiles, resp.data.artifact || null, resp.data.references || []);
794
865
 
795
866
  // Auto Memory Dream Mode
796
867
  dreamCounter++;
@@ -1100,6 +1171,20 @@ function cmdSet(args) {
1100
1171
  break;
1101
1172
 
1102
1173
  default:
1174
+ if (key && key.startsWith("plugin.")) {
1175
+ var pluginParts = key.split(".");
1176
+ if (pluginParts.length === 3) {
1177
+ var pName = pluginParts[1];
1178
+ var pField = pluginParts[2];
1179
+ var plugins = loadPlugins();
1180
+ if (!plugins[pName]) { printError("Plugin '" + pName + "' not installed. /plugin add " + pName); break; }
1181
+ if (!plugins[pName].config) plugins[pName].config = {};
1182
+ plugins[pName].config[pField] = val;
1183
+ savePlugins(plugins);
1184
+ printSuccess("Plugin " + pName + "." + pField + " = " + (pField.includes("token") ? "***" + val.slice(-6) : val));
1185
+ break;
1186
+ }
1187
+ }
1103
1188
  printError("Unknown setting. Use /settings to see all options.");
1104
1189
  }
1105
1190
  }
@@ -1638,6 +1723,7 @@ function cmdPlugin(args) {
1638
1723
  if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
1639
1724
  plugins[parts[1]].running = true;
1640
1725
  savePlugins(plugins);
1726
+ if (parts[1] === "telegram") { ensureTgBot(); }
1641
1727
  printSuccess("Plugin '" + parts[1] + "' started.");
1642
1728
 
1643
1729
  } else if (action === "stop" && parts[1]) {
@@ -1651,6 +1737,134 @@ function cmdPlugin(args) {
1651
1737
  }
1652
1738
  }
1653
1739
 
1740
+ // ── Telegram Plugin ──
1741
+ var _tgBot = null;
1742
+ var _tgMessages = [];
1743
+ var _tgMaxMessages = 50;
1744
+
1745
+ function getTgConfig() {
1746
+ var plugins = loadPlugins();
1747
+ var tg = plugins.telegram;
1748
+ if (!tg || !tg.config) return null;
1749
+ return tg.config;
1750
+ }
1751
+
1752
+ function ensureTgBot() {
1753
+ if (_tgBot) return _tgBot;
1754
+ var cfg = getTgConfig();
1755
+ if (!cfg || !cfg.bot_token) {
1756
+ printError("Telegram not configured. Run: /plugin add telegram");
1757
+ printInfo("Then set token: /set plugin.telegram.bot_token <YOUR_BOT_TOKEN>");
1758
+ printInfo("And chat ID: /set plugin.telegram.chat_id <YOUR_CHAT_ID>");
1759
+ return null;
1760
+ }
1761
+ try {
1762
+ var TelegramBot = require("node-telegram-bot-api");
1763
+ _tgBot = new TelegramBot(cfg.bot_token, { polling: true });
1764
+ _tgBot.on("message", function(msg) {
1765
+ _tgMessages.push({
1766
+ id: msg.message_id,
1767
+ from: msg.from ? (msg.from.first_name || msg.from.username || "?") : "?",
1768
+ text: msg.text || "(media)",
1769
+ date: new Date(msg.date * 1000).toLocaleTimeString(),
1770
+ chat_id: String(msg.chat.id)
1771
+ });
1772
+ if (_tgMessages.length > _tgMaxMessages) _tgMessages.shift();
1773
+ });
1774
+ printSuccess("Telegram Bot connected.");
1775
+ return _tgBot;
1776
+ } catch(e) {
1777
+ printError("Telegram connection failed: " + e.message);
1778
+ return null;
1779
+ }
1780
+ }
1781
+
1782
+ async function cmdTelegram(args) {
1783
+ var parts = (args || "").trim().split(/\s+/);
1784
+ var action = parts[0] || "status";
1785
+
1786
+ if (action === "status") {
1787
+ var cfg = getTgConfig();
1788
+ if (!cfg || !cfg.bot_token) {
1789
+ printError("Telegram not configured.");
1790
+ printInfo(" /plugin add telegram");
1791
+ printInfo(" /set plugin.telegram.bot_token <TOKEN>");
1792
+ printInfo(" /set plugin.telegram.chat_id <CHAT_ID>");
1793
+ } else {
1794
+ console.log("");
1795
+ console.log(C.bold + " Telegram Plugin" + C.reset);
1796
+ console.log(" Token: " + C.green + (cfg.bot_token ? "***" + cfg.bot_token.slice(-6) : "not set") + C.reset);
1797
+ console.log(" Chat: " + C.green + (cfg.chat_id || "not set") + C.reset);
1798
+ console.log(" Bot: " + (_tgBot ? C.green + "connected" : C.red + "disconnected") + C.reset);
1799
+ console.log(" Messages buffered: " + _tgMessages.length);
1800
+ console.log("");
1801
+ }
1802
+
1803
+ } else if (action === "connect" || action === "start") {
1804
+ ensureTgBot();
1805
+
1806
+ } else if (action === "send") {
1807
+ var cfg = getTgConfig();
1808
+ if (!cfg || !cfg.chat_id) { printError("No chat_id set. /set plugin.telegram.chat_id <ID>"); return; }
1809
+ var bot = ensureTgBot();
1810
+ if (!bot) return;
1811
+ var text = parts.slice(1).join(" ");
1812
+ if (!text) { printError("Usage: /tg send <message>"); return; }
1813
+ try {
1814
+ await bot.sendMessage(cfg.chat_id, text);
1815
+ printSuccess("Message sent.");
1816
+ } catch(e) {
1817
+ printError("Send failed: " + e.message);
1818
+ }
1819
+
1820
+ } else if (action === "read" || action === "messages" || action === "inbox") {
1821
+ if (_tgMessages.length === 0) {
1822
+ printInfo("No messages yet. Connect first: /tg connect");
1823
+ return;
1824
+ }
1825
+ console.log("");
1826
+ console.log(C.bold + " Last " + Math.min(_tgMessages.length, 20) + " messages:" + C.reset);
1827
+ _tgMessages.slice(-20).forEach(function(m) {
1828
+ console.log(C.dim + " [" + m.date + "] " + C.reset + C.brightCyan + m.from + C.reset + ": " + m.text);
1829
+ });
1830
+ console.log("");
1831
+
1832
+ } else if (action === "disconnect" || action === "stop") {
1833
+ if (_tgBot) {
1834
+ _tgBot.stopPolling();
1835
+ _tgBot = null;
1836
+ printSuccess("Telegram disconnected.");
1837
+ } else {
1838
+ printInfo("Not connected.");
1839
+ }
1840
+
1841
+ } else if (action === "file" || action === "photo") {
1842
+ var cfg = getTgConfig();
1843
+ if (!cfg || !cfg.chat_id) { printError("No chat_id set."); return; }
1844
+ var bot = ensureTgBot();
1845
+ if (!bot) return;
1846
+ var filePath = parts.slice(1).join(" ");
1847
+ if (!filePath || !fs.existsSync(filePath)) { printError("File not found: " + filePath); return; }
1848
+ try {
1849
+ await bot.sendDocument(cfg.chat_id, filePath);
1850
+ printSuccess("File sent: " + path.basename(filePath));
1851
+ } catch(e) {
1852
+ printError("Send failed: " + e.message);
1853
+ }
1854
+
1855
+ } else {
1856
+ console.log("");
1857
+ console.log(C.bold + " Telegram Commands:" + C.reset);
1858
+ console.log(" /tg status Show connection status");
1859
+ console.log(" /tg connect Connect to Telegram");
1860
+ console.log(" /tg send <msg> Send a message");
1861
+ console.log(" /tg read Show received messages");
1862
+ console.log(" /tg file <path> Send a file/photo");
1863
+ console.log(" /tg disconnect Disconnect");
1864
+ console.log("");
1865
+ }
1866
+ }
1867
+
1654
1868
  function cmdPermissions(args) {
1655
1869
  var perms = loadPermissions();
1656
1870
  if (!args) {
@@ -1714,17 +1928,10 @@ async function cmdAgent(args) {
1714
1928
  var d = resp.data;
1715
1929
  lastAnswer = d.answer || "";
1716
1930
 
1717
- // Save files locally to workspace API now returns file contents
1718
- var savedStepOutputs = await saveApiOutputs(d.files || [], d.artifact || null);
1719
- if (savedStepOutputs.length > 0) {
1720
- for (var fi = 0; fi < savedStepOutputs.length; fi++) {
1721
- totalFiles.push({
1722
- name: savedStepOutputs[fi].name,
1723
- localPath: savedStepOutputs[fi].path,
1724
- size: savedStepOutputs[fi].size
1725
- });
1726
- }
1727
- }
1931
+ var savedApiFiles = await saveApiOutputs(d.files || [], d.artifact || null, d.references || []);
1932
+ savedApiFiles.forEach(function(file) {
1933
+ totalFiles.push({ name: file.name, localPath: file.path, size: file.size });
1934
+ });
1728
1935
 
1729
1936
  // If answer contains code blocks with filenames, extract and save them
1730
1937
  var codeBlockRe = /```(?:\w+)?\s*\n\/\/\s*(.+\.(?:html|css|js|json|md|txt|py))\n([\s\S]*?)```/g;
@@ -3157,23 +3364,157 @@ async function main() {
3157
3364
  await handleCommand(input);
3158
3365
  runHook("post", input.split(/\s+/)[0].slice(1));
3159
3366
  } else {
3160
- // Browser intent natural language with URL → local Playwright
3367
+ var lowerInput = String(input || "").toLowerCase();
3368
+ var normalizedInput = String(input || "")
3369
+ .normalize("NFD")
3370
+ .replace(/[\u0300-\u036f]/g, "")
3371
+ .toLowerCase();
3372
+ var trimmedInput = String(input || "").trim();
3161
3373
  var urlMatch = input.match(/(https?:\/\/\S+)/i) || input.match(/\b((?:www\.)?[a-zA-Z0-9-]+\.[a-z]{2,}(?:\/\S*)?)\b/i);
3162
- var browserWords = /\b(öffne|open|schau|zeig|geh auf|gehen|navigate|browse|screenshot|bildschirmfoto|aufrufen|besuch|check|prüf|analysier)\b/i;
3163
- if (urlMatch && browserWords.test(input)) {
3374
+ var screenshotish = /\b(screenshot|screen ?shot|scre+ns?h[o0]t|screesnhot|screnshot|bildschirmfoto|bild)\b/i;
3375
+ var browserWords = /\b(offne|oeffne|open|schau|zeig|geh auf|gehen|navigate|browse|screenshot|bildschirmfoto|aufrufen|besuch|check|checken|pruef|analysier)\b/i;
3376
+ var localBrowserRequest = /\b(browser|bowser|brwoser|broser|browserfenster|lokalen browser|local browser|auf meinem pc im browser|im browser|browser lokal|lokal im browser)\b/i.test(normalizedInput);
3377
+ var looksLikeSearch = !urlMatch &&
3378
+ trimmedInput.length > 1 &&
3379
+ trimmedInput.length <= 80 &&
3380
+ trimmedInput.split(/\s+/).length <= 5 &&
3381
+ !/^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschuess|ciao)$/i.test(normalizedInput) &&
3382
+ !/^(wo|was|wie|wer|wann|warum)\b/i.test(normalizedInput) &&
3383
+ !/(screenshot|bildschirmfoto|png|datei|file|ordner|link|pfad|screesnhot|screnshot)\b/i.test(normalizedInput);
3384
+ var browserFollowupRequest = !urlMatch &&
3385
+ !!(lastBrowserContext && lastBrowserContext.url) &&
3386
+ /^(und|auch|noch|dann|danach|davon|darunter|weiter|weiterhin)\b/i.test(normalizedInput);
3387
+ var browserController = null;
3388
+ var browserOpen = false;
3389
+ try {
3390
+ browserController = require(path.join(__dirname, "browser-controller"));
3391
+ browserOpen = typeof browserController.isOpen === "function" && browserController.isOpen();
3392
+ } catch (browserStateErr) {}
3393
+
3394
+ if (lastCreatedFiles.length > 0 &&
3395
+ /(wo|welcher|welche|link|pfad|ordner|liegt|finde|zeig|anzeigen)/i.test(lowerInput) &&
3396
+ /(screenshot|bild|png|datei|file)/i.test(lowerInput)) {
3397
+ printInfo("Letzte gespeicherte Datei(en):");
3398
+ printSavedFiles(lastCreatedFiles);
3399
+ }
3400
+ else if (!urlMatch &&
3401
+ /\b(browser|browserfenster|lokalen browser|local browser|lokalen browsr|lokalen brwoser)\b/i.test(normalizedInput) &&
3402
+ !/(screenshot|bildschirmfoto|png|bild)\b/i.test(normalizedInput)) {
3403
+ try {
3404
+ printInfo("Oeffne lokalen Browser...");
3405
+ await browserController.open("about:blank");
3406
+ rememberBrowserAction("open", { url: "about:blank" });
3407
+ printSuccess("Lokaler Browser geoeffnet.");
3408
+ } catch (bcOpenErr) {
3409
+ printError("Lokaler Browser konnte nicht geoeffnet werden: " + bcOpenErr.message);
3410
+ }
3411
+ }
3412
+ else if (!urlMatch && lastBrowserAction && /\b(nochmal|noch mal|erneut|wieder|same again|again)\b/i.test(normalizedInput)) {
3413
+ try {
3414
+ if (lastBrowserAction.type === "search" && lastBrowserAction.payload && lastBrowserAction.payload.query) {
3415
+ printInfo("Suche im lokalen Browser: " + lastBrowserAction.payload.query);
3416
+ var repeatSearchSnap = await browserController.search(lastBrowserAction.payload.query);
3417
+ rememberBrowserContext(repeatSearchSnap, { query: lastBrowserAction.payload.query, source: "search_repeat" });
3418
+ printSuccess(repeatSearchSnap.title || "Google-Suche geoeffnet.");
3419
+ console.log(" " + C.gray + "URL: " + C.reset + repeatSearchSnap.url);
3420
+ } else if (lastBrowserAction.type === "screenshot" && (browserOpen || (lastBrowserContext && lastBrowserContext.url))) {
3421
+ var repeatSnap = browserOpen ? await browserController.snapshot() : await browserController.open(lastBrowserContext.url);
3422
+ rememberBrowserContext(repeatSnap, { source: "snapshot_repeat" });
3423
+ var repeatShot = path.join(config.workdir, "screenshot-" + Date.now() + ".png");
3424
+ await browserController.screenshot(repeatShot);
3425
+ lastCreatedFiles = [{ name: path.basename(repeatShot), path: repeatShot, size: fs.statSync(repeatShot).size }];
3426
+ rememberBrowserAction("screenshot", { url: repeatSnap.url, path: repeatShot });
3427
+ printSuccess("Screenshot gespeichert: " + repeatShot);
3428
+ console.log(" " + C.gray + "URL: " + C.reset + repeatSnap.url);
3429
+ } else if (lastBrowserAction.type === "open" && lastBrowserAction.payload && lastBrowserAction.payload.url) {
3430
+ printInfo("Oeffne " + lastBrowserAction.payload.url + "...");
3431
+ var repeatOpenSnap = await browserController.open(lastBrowserAction.payload.url);
3432
+ rememberBrowserContext(repeatOpenSnap, { source: "open_repeat", requestedUrl: lastBrowserAction.payload.url });
3433
+ printSuccess(repeatOpenSnap.title || lastBrowserAction.payload.url);
3434
+ console.log(" " + C.gray + "URL: " + C.reset + repeatOpenSnap.url);
3435
+ } else {
3436
+ await sendChat(browserContextPrompt(input));
3437
+ }
3438
+ } catch (repeatErr) {
3439
+ await sendChat(browserContextPrompt(input));
3440
+ }
3441
+ }
3442
+ else if (!urlMatch && (browserOpen || (lastBrowserContext && lastBrowserContext.url)) && screenshotish.test(normalizedInput)) {
3443
+ try {
3444
+ var currentSnap = browserOpen ? await browserController.snapshot() : await browserController.open(lastBrowserContext.url);
3445
+ rememberBrowserContext(currentSnap, { source: "snapshot_followup" });
3446
+ var currentShot = path.join(config.workdir, "screenshot-" + Date.now() + ".png");
3447
+ await browserController.screenshot(currentShot);
3448
+ lastCreatedFiles = [{ name: path.basename(currentShot), path: currentShot, size: fs.statSync(currentShot).size }];
3449
+ rememberBrowserAction("screenshot", { url: currentSnap.url, path: currentShot });
3450
+ printSuccess("Screenshot der offenen Seite gespeichert: " + currentShot);
3451
+ console.log(" " + C.gray + "URL: " + C.reset + currentSnap.url);
3452
+ } catch (followErr) {
3453
+ printError("Screenshot der offenen Seite fehlgeschlagen: " + followErr.message);
3454
+ }
3455
+ }
3456
+ else if (!urlMatch && (browserOpen || (lastBrowserContext && lastBrowserContext.url)) && /(welche seite|wo bist du|welche url|url|was ist offen|wo gerade)/i.test(normalizedInput)) {
3457
+ try {
3458
+ var statusSnap = browserOpen ? await browserController.snapshot() : lastBrowserContext;
3459
+ rememberBrowserContext(statusSnap, { source: "status_followup" });
3460
+ printSuccess(statusSnap.title || "Browserseite aktiv.");
3461
+ console.log(" " + C.gray + "URL: " + C.reset + statusSnap.url);
3462
+ } catch (statusErr) {
3463
+ printError("Browserstatus konnte nicht gelesen werden: " + statusErr.message);
3464
+ }
3465
+ }
3466
+ else if (!urlMatch && (browserOpen || (lastBrowserContext && lastBrowserContext.url)) && /(analysier|analyse|schau|check|pruef|was siehst du|was ist auf der seite)/i.test(normalizedInput)) {
3467
+ try {
3468
+ var ctxSnap = browserOpen ? await browserController.snapshot() : lastBrowserContext;
3469
+ rememberBrowserContext(ctxSnap, { source: "analysis_followup" });
3470
+ await sendChat(browserContextPrompt(input));
3471
+ } catch (ctxErr) {
3472
+ await sendChat(input);
3473
+ }
3474
+ }
3475
+ else if (browserFollowupRequest) {
3476
+ await sendChat(browserContextPrompt(input));
3477
+ }
3478
+ else if (looksLikeSearch) {
3479
+ try {
3480
+ if (browserOpen || (lastBrowserContext && lastBrowserContext.url)) {
3481
+ printInfo("Suche im lokalen Browser: " + trimmedInput);
3482
+ var searchSnap = await browserController.search(trimmedInput);
3483
+ rememberBrowserContext(searchSnap, { query: trimmedInput, source: "search" });
3484
+ rememberBrowserAction("search", { query: trimmedInput });
3485
+ printSuccess(searchSnap.title || "Google-Suche geoeffnet.");
3486
+ console.log(" " + C.gray + "URL: " + C.reset + searchSnap.url);
3487
+ if (searchSnap.text) {
3488
+ console.log(C.dim + " " + BOX.h.repeat(40) + C.reset);
3489
+ console.log(" " + searchSnap.text.slice(0, 500).replace(/\n{3,}/g, "\n\n"));
3490
+ }
3491
+ console.log("");
3492
+ } else {
3493
+ await sendChat(input);
3494
+ }
3495
+ } catch (searchErr) {
3496
+ await sendChat(input);
3497
+ }
3498
+ }
3499
+ else if (urlMatch && (localBrowserRequest || browserWords.test(normalizedInput) || screenshotish.test(normalizedInput) || /\blokal|mein pc|pc\b/i.test(normalizedInput) || normalizedInput.trim() === String(urlMatch[0]).toLowerCase() || normalizedInput.trim() === String(urlMatch[1]).toLowerCase())) {
3164
3500
  var browserUrl = urlMatch[1];
3165
3501
  try {
3166
3502
  var bc = require(path.join(__dirname, "browser-controller"));
3167
- if (/screenshot|bildschirmfoto/i.test(input)) {
3503
+ if (screenshotish.test(normalizedInput)) {
3168
3504
  printInfo("Screenshot: " + browserUrl);
3169
3505
  var snap = await bc.open(browserUrl);
3506
+ rememberBrowserContext(snap, { source: "open", requestedUrl: browserUrl });
3170
3507
  var shotFile = path.join(config.workdir, "screenshot-" + Date.now() + ".png");
3171
3508
  await bc.screenshot(shotFile);
3509
+ lastCreatedFiles = [{ name: path.basename(shotFile), path: shotFile, size: fs.statSync(shotFile).size }];
3510
+ rememberBrowserAction("screenshot", { url: snap.url, path: shotFile });
3172
3511
  printSuccess("Screenshot gespeichert: " + shotFile);
3173
3512
  console.log(" " + C.gray + "Title: " + C.reset + snap.title);
3174
3513
  } else {
3175
- printInfo("Öffne " + browserUrl + "...");
3514
+ printInfo("Oeffne " + browserUrl + "...");
3176
3515
  var snap = await bc.open(browserUrl);
3516
+ rememberBrowserContext(snap, { source: "open", requestedUrl: browserUrl });
3517
+ rememberBrowserAction("open", { url: snap.url });
3177
3518
  printSuccess(snap.title);
3178
3519
  console.log(" " + C.gray + "URL: " + C.reset + snap.url);
3179
3520
  if (snap.text) {
@@ -3182,37 +3523,38 @@ async function main() {
3182
3523
  console.log(" " + preview);
3183
3524
  }
3184
3525
  console.log("");
3185
- // Send page context to chat so BLUN King can discuss it
3186
- await sendChat(input + "\n\n[Browser-Kontext: " + snap.title + " — " + snap.text.slice(0, 1000) + "]");
3526
+ if (!localBrowserRequest) {
3527
+ await sendChat(browserContextPrompt(input));
3528
+ }
3187
3529
  }
3188
- } catch(bcErr) {
3189
- // Playwright not available, send to chat normally
3530
+ } catch (bcErr) {
3190
3531
  await sendChat(input);
3191
3532
  }
3192
3533
  }
3193
- // Intent detection — natural language → command
3194
3534
  else {
3195
- var detected = detectIntent(input);
3196
- if (detected) {
3197
- printInfo(" " + detected);
3198
- runHook("pre", detected.split(/\s+/)[0].slice(1));
3199
- await handleCommand(detected);
3200
- runHook("post", detected.split(/\s+/)[0].slice(1));
3201
- } else if (sessionMode === "agent") {
3202
- // Agent mode: detect if question or task
3203
- var trimmed = input.trim();
3204
- var isChat = trimmed.length < 15 || // short messages = chat
3205
- /^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschüss|ciao)/i.test(trimmed) ||
3206
- /^(wo |was |wie |wer |wann |warum |kannst|hast|ist |sind |where|what|how|who|when|why|can you|do you|is |are )/i.test(trimmed) ||
3207
- /\?$/.test(trimmed);
3208
- if (isChat) {
3209
- await sendChat(input);
3535
+ var detected = detectIntent(input);
3536
+ if (detected) {
3537
+ printInfo("-> " + detected);
3538
+ runHook("pre", detected.split(/\s+/)[0].slice(1));
3539
+ await handleCommand(detected);
3540
+ runHook("post", detected.split(/\s+/)[0].slice(1));
3541
+ } else if (lastBrowserContext && lastBrowserContext.url && !/^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschuess|ciao)$/i.test(normalizedInput)) {
3542
+ await sendChat(browserContextPrompt(input));
3543
+ } else if (sessionMode === "agent") {
3544
+ // Agent mode: detect if question or task
3545
+ var trimmed = input.trim();
3546
+ var isChat = trimmed.length < 15 || // short messages = chat
3547
+ /^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschuess|ciao)/i.test(trimmed) ||
3548
+ /^(wo |was |wie |wer |wann |warum |kannst|hast|ist |sind |where|what|how|who|when|why|can you|do you|is |are )/i.test(trimmed) ||
3549
+ /\?$/.test(trimmed);
3550
+ if (isChat) {
3551
+ await sendChat(input);
3552
+ } else {
3553
+ await cmdAgent(input);
3554
+ }
3210
3555
  } else {
3211
- await cmdAgent(input);
3556
+ await sendChat(input);
3212
3557
  }
3213
- } else {
3214
- await sendChat(input);
3215
- }
3216
3558
  }
3217
3559
  }
3218
3560