blun-king-cli 5.3.4 → 5.3.6
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 +115 -22
- package/browser-controller.js +27 -3
- package/package.json +1 -1
- package/reference-inspector.js +29 -1
- package/runtime.js +15 -1
- package/tests/browser-controller.test.js +5 -0
- package/tests/reference-inspector.test.js +6 -0
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
|
|
@@ -635,6 +734,7 @@ async function sendChat(message) {
|
|
|
635
734
|
|
|
636
735
|
var fullAnswer = "";
|
|
637
736
|
var receivedFiles = [];
|
|
737
|
+
var receivedArtifact = null;
|
|
638
738
|
var meta = "";
|
|
639
739
|
|
|
640
740
|
try {
|
|
@@ -655,6 +755,7 @@ async function sendChat(message) {
|
|
|
655
755
|
|
|
656
756
|
fullAnswer = resp.data.answer || "";
|
|
657
757
|
receivedFiles = resp.data.files || [];
|
|
758
|
+
receivedArtifact = resp.data.artifact || null;
|
|
658
759
|
var q = resp.data.quality || {};
|
|
659
760
|
meta = (resp.data.task_type || "") + "/" + (resp.data.role || "") + " " + BOX.dot + " score: " + (q.score || "?") +
|
|
660
761
|
(q.retried ? " " + BOX.dot + " retried" : "") +
|
|
@@ -679,19 +780,14 @@ async function sendChat(message) {
|
|
|
679
780
|
sessionCost.outputTokensEst += outTok;
|
|
680
781
|
|
|
681
782
|
// ── Save received files to local workdir ──
|
|
682
|
-
|
|
783
|
+
var savedOutputs = await saveApiOutputs(receivedFiles, receivedArtifact);
|
|
784
|
+
if (savedOutputs.length > 0) {
|
|
683
785
|
console.log(C.green + " \uD83D\uDCC1 Dateien gespeichert:" + C.reset);
|
|
684
786
|
lastCreatedFiles = [];
|
|
685
|
-
for (var fi = 0; fi <
|
|
686
|
-
var f =
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
}
|
|
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 });
|
|
695
791
|
}
|
|
696
792
|
console.log("");
|
|
697
793
|
}
|
|
@@ -1619,17 +1715,14 @@ async function cmdAgent(args) {
|
|
|
1619
1715
|
lastAnswer = d.answer || "";
|
|
1620
1716
|
|
|
1621
1717
|
// Save files locally to workspace — API now returns file contents
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
f.localPath = localPath;
|
|
1631
|
-
}
|
|
1632
|
-
totalFiles.push(f);
|
|
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
|
+
});
|
|
1633
1726
|
}
|
|
1634
1727
|
}
|
|
1635
1728
|
|
package/browser-controller.js
CHANGED
|
@@ -3,6 +3,31 @@ const { chromium } = require("playwright");
|
|
|
3
3
|
let browserInstance = null;
|
|
4
4
|
let pageInstance = null;
|
|
5
5
|
|
|
6
|
+
function normalizeUrl(input) {
|
|
7
|
+
const raw = String(input || "").trim();
|
|
8
|
+
if (!raw) {
|
|
9
|
+
throw new Error("No URL provided.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let cleaned = raw
|
|
13
|
+
.replace(/^[<\s"'`]+/, "")
|
|
14
|
+
.replace(/[>\s"'`]+$/, "");
|
|
15
|
+
|
|
16
|
+
if (/^www\./i.test(cleaned)) {
|
|
17
|
+
cleaned = `https://${cleaned}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(cleaned)) {
|
|
21
|
+
cleaned = `https://${cleaned}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return new URL(cleaned).toString();
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
async function ensurePage() {
|
|
7
32
|
if (!browserInstance) {
|
|
8
33
|
browserInstance = await chromium.launch({ headless: true });
|
|
@@ -15,9 +40,7 @@ async function ensurePage() {
|
|
|
15
40
|
|
|
16
41
|
async function open(url) {
|
|
17
42
|
const page = await ensurePage();
|
|
18
|
-
|
|
19
|
-
if (!/^https?:\/\//i.test(cleanUrl)) cleanUrl = "https://" + cleanUrl;
|
|
20
|
-
await page.goto(cleanUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
43
|
+
await page.goto(normalizeUrl(url), { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
21
44
|
return snapshot();
|
|
22
45
|
}
|
|
23
46
|
|
|
@@ -68,6 +91,7 @@ async function close() {
|
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
module.exports = {
|
|
94
|
+
normalizeUrl,
|
|
71
95
|
open,
|
|
72
96
|
click,
|
|
73
97
|
type,
|
package/package.json
CHANGED
package/reference-inspector.js
CHANGED
|
@@ -4,8 +4,36 @@ const { storeReference } = require("./local-data");
|
|
|
4
4
|
|
|
5
5
|
let chromiumLoader = null;
|
|
6
6
|
|
|
7
|
+
function normalizeDetectedUrl(input) {
|
|
8
|
+
const raw = String(input || "").trim();
|
|
9
|
+
if (!raw) return null;
|
|
10
|
+
|
|
11
|
+
let cleaned = raw
|
|
12
|
+
.replace(/^[<\s"'`(]+/, "")
|
|
13
|
+
.replace(/[>\s"'`),.;:!?]+$/, "");
|
|
14
|
+
|
|
15
|
+
if (!cleaned) return null;
|
|
16
|
+
if (/^www\./i.test(cleaned)) cleaned = `https://${cleaned}`;
|
|
17
|
+
if (/^[a-z0-9.-]+\.[a-z]{2,}(\/|$)/i.test(cleaned) && !/^[a-z][a-z0-9+.-]*:/i.test(cleaned)) {
|
|
18
|
+
cleaned = `https://${cleaned}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(cleaned);
|
|
23
|
+
if (!/^https?:$/i.test(parsed.protocol) && !/^data:$/i.test(parsed.protocol)) return null;
|
|
24
|
+
return parsed.toString();
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
function extractUrls(text) {
|
|
8
|
-
|
|
31
|
+
const matches = [
|
|
32
|
+
...Array.from(String(text || "").matchAll(/https?:\/\/[^\s)>"']+/gi)).map((match) => match[0]),
|
|
33
|
+
...Array.from(String(text || "").matchAll(/(?:^|[\s<(])((?:www\.)?[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s)>"']*)?)/gi)).map((match) => match[1])
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
return [...new Set(matches.map(normalizeDetectedUrl).filter(Boolean))];
|
|
9
37
|
}
|
|
10
38
|
|
|
11
39
|
function summarizeHtml(source) {
|
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
|
}
|
|
@@ -40,3 +40,8 @@ test("browser controller can save a screenshot file", async () => {
|
|
|
40
40
|
|
|
41
41
|
await browser.close();
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
test("browser controller normalizes bracketed hostnames into valid urls", () => {
|
|
45
|
+
assert.equal(browser.normalizeUrl("<www.autofiles.de>"), "https://www.autofiles.de/");
|
|
46
|
+
assert.equal(browser.normalizeUrl("autofiles.de"), "https://autofiles.de/");
|
|
47
|
+
});
|
|
@@ -34,3 +34,9 @@ test("reference helper builds a prompt block from inspected references", () => {
|
|
|
34
34
|
assert.match(block, /Example/);
|
|
35
35
|
assert.match(block, /example\.png/);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
test("reference helper extracts and normalizes bare domains and bracketed urls", () => {
|
|
39
|
+
const urls = inspector.extractUrls("mach einen screenshot von <www.autofiles.de> und vielleicht auch autofiles.de");
|
|
40
|
+
|
|
41
|
+
assert.deepEqual(urls, ["https://www.autofiles.de/", "https://autofiles.de/"]);
|
|
42
|
+
});
|