blun-king-cli 5.3.7 → 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
+ }
253
+ }
254
+ throw new Error("Artifact download failed: " + downloadErr.message);
255
+ }
256
+ }
257
+
258
+ async function saveApiOutputs(receivedFiles, artifact, references) {
259
+ var savedFiles = [];
260
+ var files = Array.isArray(receivedFiles) ? receivedFiles : [];
261
+
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") });
199
267
  }
200
268
  }
201
269
 
202
- if (artifact && artifact.download && artifact.filename && !seen.has(path.basename(artifact.filename))) {
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() {
@@ -739,7 +818,6 @@ async function sendChat(message) {
739
818
 
740
819
  var fullAnswer = "";
741
820
  var receivedFiles = [];
742
- var receivedArtifact = null;
743
821
  var meta = "";
744
822
 
745
823
  try {
@@ -760,7 +838,6 @@ async function sendChat(message) {
760
838
 
761
839
  fullAnswer = resp.data.answer || "";
762
840
  receivedFiles = resp.data.files || [];
763
- receivedArtifact = resp.data.artifact || null;
764
841
  var q = resp.data.quality || {};
765
842
  meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
766
843
  (q.retried ? " " + BOX.dot + " retried" : "") +
@@ -784,18 +861,7 @@ async function sendChat(message) {
784
861
  sessionCost.inputTokensEst += inTok;
785
862
  sessionCost.outputTokensEst += outTok;
786
863
 
787
- // ── Save received files to local workdir ──
788
- var savedOutputs = await saveApiOutputs(receivedFiles, receivedArtifact);
789
- if (savedOutputs.length > 0) {
790
- console.log(C.green + " \uD83D\uDCC1 Dateien gespeichert:" + C.reset);
791
- lastCreatedFiles = [];
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 });
796
- }
797
- console.log("");
798
- }
864
+ await saveApiOutputs(receivedFiles, resp.data.artifact || null, resp.data.references || []);
799
865
 
800
866
  // Auto Memory Dream Mode
801
867
  dreamCounter++;
@@ -1105,7 +1171,6 @@ function cmdSet(args) {
1105
1171
  break;
1106
1172
 
1107
1173
  default:
1108
- // Handle plugin.X.Y settings
1109
1174
  if (key && key.startsWith("plugin.")) {
1110
1175
  var pluginParts = key.split(".");
1111
1176
  if (pluginParts.length === 3) {
@@ -1658,7 +1723,6 @@ function cmdPlugin(args) {
1658
1723
  if (!plugins[parts[1]]) { printError("Plugin not found: " + parts[1]); return; }
1659
1724
  plugins[parts[1]].running = true;
1660
1725
  savePlugins(plugins);
1661
- // Actually start built-in plugins
1662
1726
  if (parts[1] === "telegram") { ensureTgBot(); }
1663
1727
  printSuccess("Plugin '" + parts[1] + "' started.");
1664
1728
 
@@ -1864,17 +1928,10 @@ async function cmdAgent(args) {
1864
1928
  var d = resp.data;
1865
1929
  lastAnswer = d.answer || "";
1866
1930
 
1867
- // Save files locally to workspace API now returns file contents
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
- });
1876
- }
1877
- }
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
+ });
1878
1935
 
1879
1936
  // If answer contains code blocks with filenames, extract and save them
1880
1937
  var codeBlockRe = /```(?:\w+)?\s*\n\/\/\s*(.+\.(?:html|css|js|json|md|txt|py))\n([\s\S]*?)```/g;
@@ -3307,23 +3364,157 @@ async function main() {
3307
3364
  await handleCommand(input);
3308
3365
  runHook("post", input.split(/\s+/)[0].slice(1));
3309
3366
  } else {
3310
- // 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();
3311
3373
  var urlMatch = input.match(/(https?:\/\/\S+)/i) || input.match(/\b((?:www\.)?[a-zA-Z0-9-]+\.[a-z]{2,}(?:\/\S*)?)\b/i);
3312
- var browserWords = /\b(öffne|open|schau|zeig|geh auf|gehen|navigate|browse|screenshot|bildschirmfoto|aufrufen|besuch|check|prüf|analysier)\b/i;
3313
- 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())) {
3314
3500
  var browserUrl = urlMatch[1];
3315
3501
  try {
3316
3502
  var bc = require(path.join(__dirname, "browser-controller"));
3317
- if (/screenshot|bildschirmfoto/i.test(input)) {
3503
+ if (screenshotish.test(normalizedInput)) {
3318
3504
  printInfo("Screenshot: " + browserUrl);
3319
3505
  var snap = await bc.open(browserUrl);
3506
+ rememberBrowserContext(snap, { source: "open", requestedUrl: browserUrl });
3320
3507
  var shotFile = path.join(config.workdir, "screenshot-" + Date.now() + ".png");
3321
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 });
3322
3511
  printSuccess("Screenshot gespeichert: " + shotFile);
3323
3512
  console.log(" " + C.gray + "Title: " + C.reset + snap.title);
3324
3513
  } else {
3325
- printInfo("Öffne " + browserUrl + "...");
3514
+ printInfo("Oeffne " + browserUrl + "...");
3326
3515
  var snap = await bc.open(browserUrl);
3516
+ rememberBrowserContext(snap, { source: "open", requestedUrl: browserUrl });
3517
+ rememberBrowserAction("open", { url: snap.url });
3327
3518
  printSuccess(snap.title);
3328
3519
  console.log(" " + C.gray + "URL: " + C.reset + snap.url);
3329
3520
  if (snap.text) {
@@ -3332,37 +3523,38 @@ async function main() {
3332
3523
  console.log(" " + preview);
3333
3524
  }
3334
3525
  console.log("");
3335
- // Send page context to chat so BLUN King can discuss it
3336
- await sendChat(input + "\n\n[Browser-Kontext: " + snap.title + " — " + snap.text.slice(0, 1000) + "]");
3526
+ if (!localBrowserRequest) {
3527
+ await sendChat(browserContextPrompt(input));
3528
+ }
3337
3529
  }
3338
- } catch(bcErr) {
3339
- // Playwright not available, send to chat normally
3530
+ } catch (bcErr) {
3340
3531
  await sendChat(input);
3341
3532
  }
3342
3533
  }
3343
- // Intent detection — natural language → command
3344
3534
  else {
3345
- var detected = detectIntent(input);
3346
- if (detected) {
3347
- printInfo(" " + detected);
3348
- runHook("pre", detected.split(/\s+/)[0].slice(1));
3349
- await handleCommand(detected);
3350
- runHook("post", detected.split(/\s+/)[0].slice(1));
3351
- } else if (sessionMode === "agent") {
3352
- // Agent mode: detect if question or task
3353
- var trimmed = input.trim();
3354
- var isChat = trimmed.length < 15 || // short messages = chat
3355
- /^(hi|hallo|hey|moin|servus|yo|sup|ok|ja|nein|danke|thanks|bye|tschüss|ciao)/i.test(trimmed) ||
3356
- /^(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) ||
3357
- /\?$/.test(trimmed);
3358
- if (isChat) {
3359
- 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
+ }
3360
3555
  } else {
3361
- await cmdAgent(input);
3556
+ await sendChat(input);
3362
3557
  }
3363
- } else {
3364
- await sendChat(input);
3365
- }
3366
3558
  }
3367
3559
  }
3368
3560