blun-king-cli 4.1.1 → 5.0.1

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 (51) hide show
  1. package/api.js +965 -0
  2. package/blun-cli.js +820 -0
  3. package/blunking-api.js +7 -0
  4. package/bot.js +188 -0
  5. package/browser-controller.js +76 -0
  6. package/chat-memory.js +103 -0
  7. package/file-helper.js +63 -0
  8. package/fuzzy-match.js +78 -0
  9. package/identities.js +106 -0
  10. package/installer.js +160 -0
  11. package/job-manager.js +146 -0
  12. package/local-data.js +71 -0
  13. package/message-builder.js +28 -0
  14. package/noisy-evals.js +38 -0
  15. package/package.json +17 -4
  16. package/palace-memory.js +246 -0
  17. package/reference-inspector.js +228 -0
  18. package/runtime.js +555 -0
  19. package/task-executor.js +104 -0
  20. package/tests/browser-controller.test.js +42 -0
  21. package/tests/cli.test.js +93 -0
  22. package/tests/file-helper.test.js +18 -0
  23. package/tests/installer.test.js +39 -0
  24. package/tests/job-manager.test.js +99 -0
  25. package/tests/merge-compat.test.js +77 -0
  26. package/tests/messages.test.js +23 -0
  27. package/tests/noisy-evals.test.js +12 -0
  28. package/tests/noisy-intent-corpus.test.js +45 -0
  29. package/tests/reference-inspector.test.js +36 -0
  30. package/tests/runtime.test.js +119 -0
  31. package/tests/task-executor.test.js +40 -0
  32. package/tests/tools.test.js +23 -0
  33. package/tests/user-profile.test.js +66 -0
  34. package/tests/website-builder.test.js +66 -0
  35. package/tmp-build-smoke/nicrazy-landing/index.html +53 -0
  36. package/tmp-build-smoke/nicrazy-landing/style.css +110 -0
  37. package/tmp-shot-smoke/website-shot-1776006760424.png +0 -0
  38. package/tmp-shot-smoke/website-shot-1776007850007.png +0 -0
  39. package/tmp-shot-smoke/website-shot-1776007886209.png +0 -0
  40. package/tmp-shot-smoke/website-shot-1776007903766.png +0 -0
  41. package/tmp-shot-smoke/website-shot-1776008737117.png +0 -0
  42. package/tmp-shot-smoke/website-shot-1776008988859.png +0 -0
  43. package/tmp-smoke/nicrazy-landing/index.html +66 -0
  44. package/tmp-smoke/nicrazy-landing/style.css +104 -0
  45. package/tools.js +177 -0
  46. package/user-profile.js +395 -0
  47. package/website-builder.js +394 -0
  48. package/website-shot-1776010648230.png +0 -0
  49. package/website_builder.txt +38 -0
  50. package/bin/blun.js +0 -3196
  51. package/setup.js +0 -30
@@ -0,0 +1,246 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const { execFileSync } = require("child_process");
5
+
6
+ const BLUN_HOME = process.env.BLUN_HOME || path.join(os.homedir(), ".blun");
7
+ const PALACE_BASE = path.join(BLUN_HOME, "palaces");
8
+ const GLOBAL_PALACE = path.join(PALACE_BASE, "_global");
9
+
10
+ let mempalaceSupport;
11
+
12
+ ensureDir(PALACE_BASE);
13
+ ensurePalaceSkeleton(GLOBAL_PALACE);
14
+
15
+ function ensureDir(dir) {
16
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ function ensurePalaceSkeleton(dir) {
20
+ ensureDir(dir);
21
+ ensureDir(path.join(dir, "memory"));
22
+ }
23
+
24
+ function safeId(value) {
25
+ return String(value || "global").replace(/[^a-zA-Z0-9._-]/g, "_");
26
+ }
27
+
28
+ function getUserPalace(userId) {
29
+ const dir = userId ? path.join(PALACE_BASE, safeId(userId)) : GLOBAL_PALACE;
30
+ ensurePalaceSkeleton(dir);
31
+ maybeInitMempalace(dir);
32
+ return dir;
33
+ }
34
+
35
+ function getPythonBinary() {
36
+ if (process.env.BLUN_PYTHON) return process.env.BLUN_PYTHON;
37
+ return process.platform === "win32" ? "python" : "python3";
38
+ }
39
+
40
+ function detectMempalace() {
41
+ if (mempalaceSupport !== undefined) return mempalaceSupport;
42
+
43
+ try {
44
+ execFileSync(getPythonBinary(), ["-m", "mempalace", "--help"], {
45
+ encoding: "utf8",
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ timeout: 4000
48
+ });
49
+ mempalaceSupport = true;
50
+ } catch {
51
+ mempalaceSupport = false;
52
+ }
53
+
54
+ return mempalaceSupport;
55
+ }
56
+
57
+ function maybeInitMempalace(dir) {
58
+ if (!detectMempalace()) return false;
59
+
60
+ const marker = path.join(dir, ".mempalace-init");
61
+ if (fs.existsSync(marker)) return true;
62
+
63
+ try {
64
+ execFileSync(getPythonBinary(), ["-m", "mempalace", "init", "--yes", "."], {
65
+ cwd: dir,
66
+ encoding: "utf8",
67
+ stdio: ["ignore", "pipe", "pipe"],
68
+ timeout: 15000
69
+ });
70
+ fs.writeFileSync(marker, "ok", "utf8");
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function runMempalace(dir, args, timeout = 10000) {
78
+ return execFileSync(getPythonBinary(), ["-m", "mempalace", ...args], {
79
+ cwd: dir,
80
+ encoding: "utf8",
81
+ stdio: ["ignore", "pipe", "pipe"],
82
+ timeout
83
+ });
84
+ }
85
+
86
+ function memoryDir(dir) {
87
+ const target = path.join(dir, "memory");
88
+ ensureDir(target);
89
+ return target;
90
+ }
91
+
92
+ function listMemoryFiles(dir) {
93
+ const target = memoryDir(dir);
94
+ return fs.readdirSync(target)
95
+ .filter((entry) => entry.endsWith(".md"))
96
+ .map((entry) => path.join(target, entry));
97
+ }
98
+
99
+ function scoreText(text, query) {
100
+ const haystack = String(text || "").toLowerCase();
101
+ const needles = String(query || "").toLowerCase().split(/\s+/).filter(Boolean);
102
+ if (needles.length === 0) return haystack.length ? 1 : 0;
103
+ return needles.reduce((score, needle) => score + (haystack.includes(needle) ? 1 : 0), 0);
104
+ }
105
+
106
+ function fallbackSearch(query, limit, userId) {
107
+ const dir = getUserPalace(userId);
108
+ const scored = listMemoryFiles(dir)
109
+ .map((file) => {
110
+ const content = fs.readFileSync(file, "utf8");
111
+ return {
112
+ file,
113
+ content,
114
+ score: scoreText(content, query)
115
+ };
116
+ })
117
+ .filter((entry) => entry.score > 0)
118
+ .sort((a, b) => b.score - a.score || b.file.localeCompare(a.file))
119
+ .slice(0, limit);
120
+
121
+ if (scored.length === 0) return "Keine Ergebnisse.";
122
+
123
+ return scored.map((entry) => {
124
+ const snippet = entry.content.replace(/\s+/g, " ").trim().slice(0, 300);
125
+ return `${path.basename(entry.file)}\n${snippet}`;
126
+ }).join("\n\n");
127
+ }
128
+
129
+ function fallbackWakeUp(userId) {
130
+ const dir = getUserPalace(userId);
131
+ const entries = listMemoryFiles(dir)
132
+ .map((file) => ({ file, stat: fs.statSync(file) }))
133
+ .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
134
+ .slice(0, 5)
135
+ .map(({ file }) => {
136
+ const content = fs.readFileSync(file, "utf8").replace(/\s+/g, " ").trim();
137
+ return `- ${path.basename(file)}: ${content.slice(0, 220)}`;
138
+ });
139
+
140
+ return entries.length ? entries.join("\n") : "";
141
+ }
142
+
143
+ function fallbackStats(userId) {
144
+ const dir = getUserPalace(userId);
145
+ const files = listMemoryFiles(dir);
146
+ return `Palace local fallback active. Files: ${files.length}`;
147
+ }
148
+
149
+ function appendEntry(dir, filename, text) {
150
+ const file = path.join(memoryDir(dir), filename);
151
+ fs.appendFileSync(file, text, "utf8");
152
+ return file;
153
+ }
154
+
155
+ function palaceSearch(query, limit = 5, userId) {
156
+ const dir = getUserPalace(userId);
157
+
158
+ if (detectMempalace()) {
159
+ try {
160
+ const out = runMempalace(dir, ["search", String(query || ""), "--results", String(limit)], 10000);
161
+ return out.trim() || "Keine Ergebnisse.";
162
+ } catch {
163
+ return fallbackSearch(query, limit, userId);
164
+ }
165
+ }
166
+
167
+ return fallbackSearch(query, limit, userId);
168
+ }
169
+
170
+ function palaceStore(text, source = "misc", userId) {
171
+ const dir = getUserPalace(userId);
172
+ const stamp = new Date();
173
+ const filename = `${stamp.toISOString().slice(0, 10)}_${safeId(source).slice(0, 30) || "misc"}.md`;
174
+ const entry = `\n## ${stamp.toISOString()}\n${String(text || "").trim()}\n`;
175
+
176
+ try {
177
+ appendEntry(dir, filename, entry);
178
+
179
+ if (detectMempalace()) {
180
+ try {
181
+ runMempalace(dir, ["mine", "."], 30000);
182
+ } catch {
183
+ // Local markdown store already succeeded.
184
+ }
185
+ }
186
+
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ function palaceWakeUp(userId) {
194
+ const dir = getUserPalace(userId);
195
+
196
+ if (detectMempalace()) {
197
+ try {
198
+ const out = runMempalace(dir, ["wake-up"], 10000);
199
+ return out.trim();
200
+ } catch {
201
+ return fallbackWakeUp(userId);
202
+ }
203
+ }
204
+
205
+ return fallbackWakeUp(userId);
206
+ }
207
+
208
+ function palaceLearn(knowledge, source = "learn", userId) {
209
+ return palaceStore(knowledge, `learn_${source}`, userId);
210
+ }
211
+
212
+ function palaceLogChat(userId, role, content) {
213
+ const dir = getUserPalace(userId);
214
+ const stamp = new Date();
215
+ const filename = `chat_${stamp.toISOString().slice(0, 10)}.md`;
216
+ const snippet = String(content || "").replace(/\s+/g, " ").trim().slice(0, 1200);
217
+ appendEntry(dir, filename, `\n**${stamp.toISOString().slice(11, 16)} ${role}:** ${snippet}\n`);
218
+ return true;
219
+ }
220
+
221
+ function palaceStats(userId) {
222
+ const dir = getUserPalace(userId);
223
+
224
+ if (detectMempalace()) {
225
+ try {
226
+ const out = runMempalace(dir, ["status"], 10000);
227
+ return out.trim();
228
+ } catch {
229
+ return fallbackStats(userId);
230
+ }
231
+ }
232
+
233
+ return fallbackStats(userId);
234
+ }
235
+
236
+ module.exports = {
237
+ PALACE_BASE,
238
+ GLOBAL_PALACE,
239
+ getUserPalace,
240
+ palaceSearch,
241
+ palaceStore,
242
+ palaceWakeUp,
243
+ palaceLearn,
244
+ palaceLogChat,
245
+ palaceStats
246
+ };
@@ -0,0 +1,228 @@
1
+ const path = require("path");
2
+ const { execFile } = require("child_process");
3
+ const { storeReference } = require("./local-data");
4
+
5
+ let chromiumLoader = null;
6
+
7
+ function extractUrls(text) {
8
+ return Array.from(String(text || "").matchAll(/https?:\/\/[^\s)>"']+/gi)).map((match) => match[0]);
9
+ }
10
+
11
+ function summarizeHtml(source) {
12
+ const title = (source.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [, ""])[1].trim();
13
+ const h1s = [...source.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/ig)]
14
+ .map((match) => match[1].replace(/<[^>]+>/g, "").trim())
15
+ .filter(Boolean);
16
+ const links = [...source.matchAll(/<a\b/ig)].length;
17
+ const images = [...source.matchAll(/<img\b/ig)].length;
18
+ const text = cleanText((source.match(/<body[^>]*>([\s\S]*?)<\/body>/i) || [, source])[1]).slice(0, 3000);
19
+
20
+ return {
21
+ title,
22
+ h1s,
23
+ links,
24
+ images,
25
+ text,
26
+ responsive: /<meta[^>]+name=["']viewport["']/i.test(source)
27
+ };
28
+ }
29
+
30
+ function cleanText(source) {
31
+ return String(source || "")
32
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
33
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
34
+ .replace(/<[^>]+>/g, " ")
35
+ .replace(/\s+/g, " ")
36
+ .trim();
37
+ }
38
+
39
+ function shouldAutoReference(input, task = {}) {
40
+ const text = String(input || "");
41
+ if (!extractUrls(text).length) return false;
42
+ if (["installation", "browser_capture"].includes(task?.type)) return false;
43
+ if (task?.type === "website_builder" || task?.type === "analysis") return true;
44
+ return /\b(referenz|vorlage|inspiriert|angelehnt|wie diese|wie die|schau dir|analysier.*https?:\/\/|bau.*https?:\/\/)\b/i.test(text);
45
+ }
46
+
47
+ function buildReferencePromptBlock(references = []) {
48
+ if (!Array.isArray(references) || references.length === 0) return "";
49
+
50
+ const lines = ["[Auto Reference Chain]"];
51
+ references.forEach((ref, index) => {
52
+ const summary = ref.summary || ref;
53
+ lines.push(`Referenz ${index + 1}: ${ref.url || "-"}`);
54
+ if (summary.title) lines.push(`Titel: ${summary.title}`);
55
+ if (Array.isArray(summary.h1s) && summary.h1s.length) lines.push(`H1: ${summary.h1s.join(" | ")}`);
56
+ if (summary.text) lines.push(`Text: ${String(summary.text).slice(0, 500)}`);
57
+ if (typeof summary.links === "number") lines.push(`Links: ${summary.links}`);
58
+ if (typeof summary.images === "number") lines.push(`Bilder: ${summary.images}`);
59
+ if (ref.screenshotPath) lines.push(`Screenshot: ${ref.screenshotPath}`);
60
+ if (ref.error) lines.push(`Fehler: ${ref.error}`);
61
+ });
62
+
63
+ return lines.join("\n");
64
+ }
65
+
66
+ async function loadChromium() {
67
+ if (chromiumLoader !== null) return chromiumLoader;
68
+
69
+ try {
70
+ const playwright = require("playwright");
71
+ chromiumLoader = playwright.chromium;
72
+ } catch {
73
+ chromiumLoader = false;
74
+ }
75
+
76
+ return chromiumLoader;
77
+ }
78
+
79
+ function execFileAsync(command, args) {
80
+ return new Promise((resolve, reject) => {
81
+ execFile(command, args, { maxBuffer: 20 * 1024 * 1024 }, (error, stdout, stderr) => {
82
+ if (error) reject(new Error(stderr || error.message));
83
+ else resolve(stdout);
84
+ });
85
+ });
86
+ }
87
+
88
+ async function fetchHtml(url, options = {}) {
89
+ try {
90
+ const resp = await fetch(url, {
91
+ headers: {
92
+ "User-Agent": options.userAgent || "BLUN-King/5.0 (+reference-inspector)"
93
+ }
94
+ });
95
+ if (!resp.ok) throw new Error(`Reference fetch failed with HTTP ${resp.status}`);
96
+ return await resp.text();
97
+ } catch {
98
+ try {
99
+ const curlBinary = process.platform === "win32" ? "curl.exe" : "curl";
100
+ return await execFileAsync(curlBinary, [
101
+ "-L",
102
+ "-A",
103
+ options.userAgent || "BLUN-King/5.0 (+reference-inspector)",
104
+ url
105
+ ]);
106
+ } catch {
107
+ if (process.platform === "win32") {
108
+ const script = [
109
+ `$ProgressPreference='SilentlyContinue'`,
110
+ `$resp = Invoke-WebRequest -UseBasicParsing -Headers @{'User-Agent'='${options.userAgent || "BLUN-King/5.0 (+reference-inspector)"}'} -Uri '${url}'`,
111
+ `[Console]::Out.Write($resp.Content)`
112
+ ].join("; ");
113
+ return await execFileAsync("powershell.exe", ["-NoProfile", "-Command", script]);
114
+ }
115
+
116
+ throw new Error("Reference fetch failed");
117
+ }
118
+ }
119
+ }
120
+
121
+ async function inspectReference(url, options = {}) {
122
+ let html = "";
123
+ let summary = {
124
+ title: "",
125
+ h1s: [],
126
+ links: 0,
127
+ images: 0,
128
+ responsive: false
129
+ };
130
+ const result = {
131
+ url,
132
+ fetchedAt: Date.now(),
133
+ summary,
134
+ html,
135
+ screenshotPath: null,
136
+ screenshotAvailable: false,
137
+ error: null
138
+ };
139
+
140
+ try {
141
+ html = await fetchHtml(url, options);
142
+ summary = summarizeHtml(html);
143
+ result.html = html;
144
+ result.summary = summary;
145
+ } catch (error) {
146
+ result.error = error.message;
147
+ }
148
+
149
+ if (!result.error && options.screenshot !== false) {
150
+ const chromium = await loadChromium();
151
+ if (chromium) {
152
+ try {
153
+ const browser = await chromium.launch({ headless: true });
154
+ try {
155
+ const page = await browser.newPage({ viewport: { width: 1440, height: 1024 } });
156
+ await page.goto(url, { waitUntil: "networkidle", timeout: options.timeout || 30000 });
157
+ const stored = storeReference(url, {
158
+ type: "reference",
159
+ mode: "html+screenshot",
160
+ summary
161
+ });
162
+ const screenshotPath = path.join(path.dirname(stored.filePath), `${stored.id}.png`);
163
+ await page.screenshot({ path: screenshotPath, fullPage: true });
164
+ result.screenshotPath = screenshotPath;
165
+ result.screenshotAvailable = true;
166
+ result.storage = stored.filePath;
167
+ } finally {
168
+ await browser.close();
169
+ }
170
+ } catch {
171
+ result.screenshotAvailable = false;
172
+ }
173
+ }
174
+ }
175
+
176
+ if (!result.storage) {
177
+ const stored = storeReference(url, {
178
+ type: "reference",
179
+ mode: result.error ? "error" : "html",
180
+ summary,
181
+ error: result.error
182
+ });
183
+ result.storage = stored.filePath;
184
+ }
185
+
186
+ return result;
187
+ }
188
+
189
+ async function screenshotUrl(url, options = {}) {
190
+ const result = await inspectReference(url, { ...options, screenshot: true });
191
+ return {
192
+ url: result.url,
193
+ screenshotPath: result.screenshotPath,
194
+ screenshotAvailable: result.screenshotAvailable,
195
+ summary: result.summary,
196
+ storage: result.storage,
197
+ error: result.error
198
+ };
199
+ }
200
+
201
+ async function autoReferenceChain(input, options = {}) {
202
+ const urls = extractUrls(input).slice(0, options.limit || 2);
203
+ const references = [];
204
+
205
+ for (const url of urls) {
206
+ try {
207
+ references.push(await inspectReference(url, { screenshot: options.screenshot !== false }));
208
+ } catch (error) {
209
+ references.push({
210
+ url,
211
+ error: error.message,
212
+ summary: { title: "", h1s: [], links: 0, images: 0, text: "", responsive: false }
213
+ });
214
+ }
215
+ }
216
+
217
+ return references;
218
+ }
219
+
220
+ module.exports = {
221
+ extractUrls,
222
+ inspectReference,
223
+ summarizeHtml,
224
+ shouldAutoReference,
225
+ buildReferencePromptBlock,
226
+ screenshotUrl,
227
+ autoReferenceChain
228
+ };