blun-king-cli 4.1.1 → 5.0.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/api.js +965 -0
- package/blun-cli.js +763 -0
- package/blunking-api.js +7 -0
- package/bot.js +188 -0
- package/browser-controller.js +76 -0
- package/chat-memory.js +103 -0
- package/file-helper.js +63 -0
- package/fuzzy-match.js +78 -0
- package/identities.js +106 -0
- package/installer.js +160 -0
- package/job-manager.js +146 -0
- package/local-data.js +71 -0
- package/message-builder.js +28 -0
- package/noisy-evals.js +38 -0
- package/package.json +17 -4
- package/palace-memory.js +246 -0
- package/reference-inspector.js +228 -0
- package/runtime.js +555 -0
- package/task-executor.js +104 -0
- package/tests/browser-controller.test.js +42 -0
- package/tests/cli.test.js +93 -0
- package/tests/file-helper.test.js +18 -0
- package/tests/installer.test.js +39 -0
- package/tests/job-manager.test.js +99 -0
- package/tests/merge-compat.test.js +77 -0
- package/tests/messages.test.js +23 -0
- package/tests/noisy-evals.test.js +12 -0
- package/tests/noisy-intent-corpus.test.js +45 -0
- package/tests/reference-inspector.test.js +36 -0
- package/tests/runtime.test.js +119 -0
- package/tests/task-executor.test.js +40 -0
- package/tests/tools.test.js +23 -0
- package/tests/user-profile.test.js +66 -0
- package/tests/website-builder.test.js +66 -0
- package/tmp-build-smoke/nicrazy-landing/index.html +53 -0
- package/tmp-build-smoke/nicrazy-landing/style.css +110 -0
- package/tmp-shot-smoke/website-shot-1776006760424.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007850007.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007886209.png +0 -0
- package/tmp-shot-smoke/website-shot-1776007903766.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008737117.png +0 -0
- package/tmp-shot-smoke/website-shot-1776008988859.png +0 -0
- package/tmp-smoke/nicrazy-landing/index.html +66 -0
- package/tmp-smoke/nicrazy-landing/style.css +104 -0
- package/tools.js +177 -0
- package/user-profile.js +395 -0
- package/website-builder.js +394 -0
- package/website-shot-1776010648230.png +0 -0
- package/website_builder.txt +38 -0
- package/bin/blun.js +0 -3196
- package/setup.js +0 -30
package/api.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const runtime = require("./runtime");
|
|
6
|
+
const tools = require("./tools");
|
|
7
|
+
const memory = require("./chat-memory");
|
|
8
|
+
const palace = require("./palace-memory");
|
|
9
|
+
const fileHelper = require("./file-helper");
|
|
10
|
+
const browser = require("./browser-controller");
|
|
11
|
+
const taskExecutor = require("./task-executor");
|
|
12
|
+
const userProfile = require("./user-profile");
|
|
13
|
+
const jobs = require("./job-manager");
|
|
14
|
+
const { evaluateNoisyIntentCorpus } = require("./noisy-evals");
|
|
15
|
+
const { buildConversationMessages } = require("./message-builder");
|
|
16
|
+
const { inspectReference, extractUrls, autoReferenceChain, buildReferencePromptBlock, shouldAutoReference, screenshotUrl } = require("./reference-inspector");
|
|
17
|
+
const { findFacts, storeFact } = require("./local-data");
|
|
18
|
+
const { buildWebsiteProject, validateWebsiteOutput } = require("./website-builder");
|
|
19
|
+
|
|
20
|
+
const PORT = Number(process.env.BLUN_PORT || 3200);
|
|
21
|
+
const MODEL = process.env.BLUN_MODEL || "blun-king-v500";
|
|
22
|
+
const FAST_MODEL = process.env.BLUN_FAST_MODEL || MODEL;
|
|
23
|
+
const OLLAMA_URL = process.env.OLLAMA_URL || "http://127.0.0.1:11434/api/chat";
|
|
24
|
+
const TOKENS = String(process.env.BLUN_API_TOKENS || process.env.BLUN_API_TOKEN || "")
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((value) => value.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
|
|
29
|
+
function isLoopback(ip = "") {
|
|
30
|
+
return ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(ip);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function auth(req, res, next) {
|
|
34
|
+
if (req.path === "/health") return next();
|
|
35
|
+
|
|
36
|
+
if (TOKENS.length === 0) {
|
|
37
|
+
if (isLoopback(req.ip) || isLoopback(req.socket?.remoteAddress)) return next();
|
|
38
|
+
return res.status(503).json({ error: "Remote access requires BLUN_API_TOKEN or BLUN_API_TOKENS." });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const authHeader = req.headers.authorization || "";
|
|
42
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
43
|
+
if (!token || !TOKENS.includes(token)) return res.status(401).json({ error: "Unauthorized" });
|
|
44
|
+
next();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireLoopback(req, res, next) {
|
|
48
|
+
if (isLoopback(req.ip) || isLoopback(req.socket?.remoteAddress)) return next();
|
|
49
|
+
return res.status(403).json({ error: "Browser control is local-only." });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ollamaChat(messages, model = MODEL) {
|
|
53
|
+
const resp = await fetch(OLLAMA_URL, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ model, messages, stream: false })
|
|
57
|
+
});
|
|
58
|
+
if (!resp.ok) throw new Error(`Ollama error ${resp.status}`);
|
|
59
|
+
const data = await resp.json();
|
|
60
|
+
return String(data.message?.content || "").replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function generateRecoveredAnswer({ task, session, message, identityId, sessionId, previousAnswer, reasons, model }) {
|
|
64
|
+
const prompt = composePromptWithPalace(task, session, message, identityId, sessionId);
|
|
65
|
+
const retryPrompt = runtime.buildRetryPrompt(reasons);
|
|
66
|
+
const messages = buildConversationMessages({
|
|
67
|
+
prompt,
|
|
68
|
+
session,
|
|
69
|
+
message
|
|
70
|
+
});
|
|
71
|
+
messages.push({ role: "assistant", content: previousAnswer });
|
|
72
|
+
messages.push({ role: "user", content: retryPrompt });
|
|
73
|
+
return ollamaChat(messages, model);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function generateVettedAnswer({ task, session, message, identityId, sessionId, model, history, extraPrompt }) {
|
|
77
|
+
const promptBase = composePromptWithPalace(task, session, message, identityId, sessionId);
|
|
78
|
+
const prompt = [promptBase, extraPrompt].filter(Boolean).join("\n\n");
|
|
79
|
+
const messages = buildConversationMessages({
|
|
80
|
+
prompt,
|
|
81
|
+
session,
|
|
82
|
+
history,
|
|
83
|
+
message
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let answer = await ollamaChat(messages, model);
|
|
87
|
+
let quality = runtime.selfCheckResponse(answer, task, message);
|
|
88
|
+
let retries = 0;
|
|
89
|
+
|
|
90
|
+
while (task.type !== "chat" && quality.blocked && retries < 2) {
|
|
91
|
+
answer = await generateRecoveredAnswer({
|
|
92
|
+
task,
|
|
93
|
+
session,
|
|
94
|
+
message,
|
|
95
|
+
identityId,
|
|
96
|
+
sessionId,
|
|
97
|
+
previousAnswer: answer,
|
|
98
|
+
reasons: quality.reasons.length ? quality.reasons : ["action_mismatch"],
|
|
99
|
+
model
|
|
100
|
+
});
|
|
101
|
+
quality = runtime.selfCheckResponse(answer, task, message);
|
|
102
|
+
retries += 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (task.type !== "chat" && quality.blocked) {
|
|
106
|
+
answer = runtime.buildBlockedActionReply(task, quality.reasons);
|
|
107
|
+
quality = runtime.selfCheckResponse(answer, { ...task, type: "chat" }, message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { prompt, answer, quality, retried: retries > 0, retries };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function ollamaStream(messages, onToken, model = MODEL) {
|
|
114
|
+
const resp = await fetch(OLLAMA_URL, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({ model, messages, stream: true })
|
|
118
|
+
});
|
|
119
|
+
if (!resp.ok) throw new Error(`Ollama error ${resp.status}`);
|
|
120
|
+
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
const reader = resp.body.getReader();
|
|
123
|
+
let buffer = "";
|
|
124
|
+
let full = "";
|
|
125
|
+
|
|
126
|
+
while (true) {
|
|
127
|
+
const chunk = await reader.read();
|
|
128
|
+
if (chunk.done) break;
|
|
129
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
130
|
+
const lines = buffer.split("\n");
|
|
131
|
+
buffer = lines.pop() || "";
|
|
132
|
+
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
if (!trimmed) continue;
|
|
136
|
+
try {
|
|
137
|
+
const payload = JSON.parse(trimmed);
|
|
138
|
+
const token = String(payload.message?.content || "");
|
|
139
|
+
if (!token) continue;
|
|
140
|
+
full += token;
|
|
141
|
+
const visible = token.replace(/<think>[\s\S]*?<\/think>/gi, "");
|
|
142
|
+
if (visible && onToken) onToken(visible);
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore malformed stream lines.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return full.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function referenceQueryCandidates(message) {
|
|
153
|
+
const stripped = String(message || "")
|
|
154
|
+
.replace(/https?:\/\/[^\s]+/gi, " ")
|
|
155
|
+
.replace(/[^\p{L}\p{N}.\s-]/gu, " ")
|
|
156
|
+
.split(/\s+/)
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
|
|
159
|
+
const stop = new Set([
|
|
160
|
+
"bitte", "baue", "bau", "erstelle", "mach", "mache", "webseite", "website", "seite", "landingpage",
|
|
161
|
+
"landing", "referenz", "vorlage", "wie", "bei", "orientier", "orientiere", "angelehnt", "inspiriert",
|
|
162
|
+
"von", "mir", "eine", "einen", "ein", "fuer", "für", "und", "oder", "mit", "aus"
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
return stripped
|
|
166
|
+
.filter((part) => !stop.has(part.toLowerCase()))
|
|
167
|
+
.slice(0, 5)
|
|
168
|
+
.join(" ");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function searchReferenceUrls(query) {
|
|
172
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
173
|
+
const resp = await fetch(searchUrl, {
|
|
174
|
+
headers: { "User-Agent": "BLUN-King/5.0 (+reference-search)" }
|
|
175
|
+
});
|
|
176
|
+
if (!resp.ok) return [];
|
|
177
|
+
|
|
178
|
+
const html = await resp.text();
|
|
179
|
+
const found = [...html.matchAll(/result__url[^>]*>\s*([^<\s]+)\s*</gi)]
|
|
180
|
+
.map((match) => match[1])
|
|
181
|
+
.map((value) => {
|
|
182
|
+
if (/^https?:\/\//i.test(value)) return value;
|
|
183
|
+
if (/^[a-z0-9.-]+\.[a-z]{2,}/i.test(value)) return `https://${value}`;
|
|
184
|
+
return null;
|
|
185
|
+
})
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
|
|
188
|
+
return [...new Set(found)].slice(0, 2);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function resolveReferenceTargets(input, limit = 2) {
|
|
192
|
+
const direct = extractUrls(input).slice(0, limit);
|
|
193
|
+
if (direct.length > 0) return direct;
|
|
194
|
+
|
|
195
|
+
if (!/\b(referenz|vorlage|wie|bei|inspiriert|angelehnt)\b/i.test(String(input || ""))) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const query = referenceQueryCandidates(input);
|
|
200
|
+
if (!query) return [];
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
return await searchReferenceUrls(`${query} website`);
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function collectReferenceContext(input) {
|
|
210
|
+
const urls = await resolveReferenceTargets(input, 2);
|
|
211
|
+
const references = [];
|
|
212
|
+
|
|
213
|
+
for (const url of urls) {
|
|
214
|
+
try {
|
|
215
|
+
const result = await inspectReference(url, { screenshot: true });
|
|
216
|
+
references.push({
|
|
217
|
+
url,
|
|
218
|
+
title: result.summary.title,
|
|
219
|
+
h1s: result.summary.h1s,
|
|
220
|
+
links: result.summary.links,
|
|
221
|
+
images: result.summary.images,
|
|
222
|
+
responsive: result.summary.responsive,
|
|
223
|
+
text: result.summary.text,
|
|
224
|
+
html: result.html,
|
|
225
|
+
screenshotPath: result.screenshotPath,
|
|
226
|
+
storage: result.storage,
|
|
227
|
+
error: result.error
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
references.push({ url, error: error.message });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return references;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function collectAutoReferencePrompt(input, task) {
|
|
238
|
+
if (!shouldAutoReference(input, task)) return { references: [], promptBlock: "" };
|
|
239
|
+
const references = await autoReferenceChain(input, { screenshot: true, limit: 2 });
|
|
240
|
+
return {
|
|
241
|
+
references,
|
|
242
|
+
promptBlock: buildReferencePromptBlock(references)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function buildWebsiteArtifact(goal, workdir) {
|
|
247
|
+
const references = await collectReferenceContext(goal);
|
|
248
|
+
const built = buildWebsiteProject(goal, references);
|
|
249
|
+
const validation = validateWebsiteOutput(built.files, built);
|
|
250
|
+
|
|
251
|
+
if (!validation.ok) {
|
|
252
|
+
throw new Error(`Website build blocked: ${validation.issues.join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const create = await tools.executeTool("create_project", { name: built.projectName, files: built.files }, workdir);
|
|
256
|
+
const artifact = fileHelper.writeArtifact(`${built.projectName}.json`, JSON.stringify(built.files, null, 2), "utf8");
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
brand: built.brand,
|
|
260
|
+
industry: built.industry,
|
|
261
|
+
validation,
|
|
262
|
+
projectRoot: create.projectRoot,
|
|
263
|
+
verified: create.verified,
|
|
264
|
+
references,
|
|
265
|
+
files: create.fileObjects.map((file) => ({
|
|
266
|
+
path: file.path,
|
|
267
|
+
content: file.content
|
|
268
|
+
})),
|
|
269
|
+
artifact: {
|
|
270
|
+
artifact_id: artifact.artifact_id,
|
|
271
|
+
filename: artifact.filename,
|
|
272
|
+
download: `/download/${artifact.artifact_id}`
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getIdentityId(req) {
|
|
278
|
+
return req.body.identity && req.body.identity !== "auto" ? String(req.body.identity) : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function composePromptWithPalace(task, session, message, identityId, sessionId) {
|
|
282
|
+
const palaceContext = palace.palaceWakeUp(sessionId);
|
|
283
|
+
const learnedContext = userProfile.buildPromptContext(sessionId, message);
|
|
284
|
+
const basePrompt = runtime.composeSystemPrompt(task, session, message, identityId);
|
|
285
|
+
const parts = [basePrompt];
|
|
286
|
+
|
|
287
|
+
if (learnedContext) {
|
|
288
|
+
parts.push(`[Gelerntes Nutzerwissen]\n${learnedContext.slice(0, 1500)}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (palaceContext) {
|
|
292
|
+
parts.push(`[Langzeitgedaechtnis]\n${palaceContext.slice(0, 2000)}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return parts.join("\n\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function updateSession(sessionId, task, identityId, message, answer, workdir) {
|
|
299
|
+
memory.appendHistory(sessionId, "user", message);
|
|
300
|
+
memory.appendHistory(sessionId, "assistant", answer);
|
|
301
|
+
palace.palaceLogChat(sessionId, "user", message);
|
|
302
|
+
palace.palaceLogChat(sessionId, "assistant", answer);
|
|
303
|
+
|
|
304
|
+
if ((task.type === "website_builder" || task.type === "file_generation" || task.type === "browser_capture" || task.type === "installation") && !task.followup) {
|
|
305
|
+
memory.setActiveTask(sessionId, {
|
|
306
|
+
type: task.type,
|
|
307
|
+
role: task.role,
|
|
308
|
+
output: task.output,
|
|
309
|
+
workdir,
|
|
310
|
+
lastUserIntent: message,
|
|
311
|
+
identityId
|
|
312
|
+
});
|
|
313
|
+
} else if (!task.followup && task.type === "chat") {
|
|
314
|
+
memory.clearActiveTask(sessionId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function handleChatRequest(body = {}) {
|
|
319
|
+
const message = String(body.message || "").trim();
|
|
320
|
+
const sessionId = String(body.session_id || "default");
|
|
321
|
+
const workdir = path.resolve(body.workdir || process.cwd());
|
|
322
|
+
const preferredIdentity = body.identity && body.identity !== "auto" ? String(body.identity) : undefined;
|
|
323
|
+
|
|
324
|
+
const session = memory.loadSession(sessionId);
|
|
325
|
+
const routing = runtime.classifyTaskDetailed(message, session);
|
|
326
|
+
const task = routing.task;
|
|
327
|
+
const identity = runtime.resolveIdentity(message, task, session, preferredIdentity);
|
|
328
|
+
const autoReference = await collectAutoReferencePrompt(message, task);
|
|
329
|
+
|
|
330
|
+
if (taskExecutor.isDirectActionTask(task)) {
|
|
331
|
+
const direct = await taskExecutor.executeDirectTask({ task, message, session, workdir });
|
|
332
|
+
updateSession(sessionId, task, identity.id, message, direct.answer, workdir);
|
|
333
|
+
return {
|
|
334
|
+
answer: direct.answer,
|
|
335
|
+
task_type: task.type,
|
|
336
|
+
role: task.role,
|
|
337
|
+
identity: identity.id,
|
|
338
|
+
routing,
|
|
339
|
+
status: direct.status,
|
|
340
|
+
steps: direct.steps || [],
|
|
341
|
+
files: direct.files || [],
|
|
342
|
+
references: direct.references || [],
|
|
343
|
+
artifact: direct.artifact || null,
|
|
344
|
+
install: direct.install,
|
|
345
|
+
quality: { score: 100, flags: [], retried: false },
|
|
346
|
+
tokens: { input: 0, output: 0, total: 0 }
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const model = task.type === "chat" && message.length < 50 ? FAST_MODEL : MODEL;
|
|
351
|
+
const generated = await generateVettedAnswer({
|
|
352
|
+
task,
|
|
353
|
+
session,
|
|
354
|
+
message,
|
|
355
|
+
identityId: identity.id,
|
|
356
|
+
sessionId,
|
|
357
|
+
model,
|
|
358
|
+
history: body.history,
|
|
359
|
+
extraPrompt: autoReference.promptBlock
|
|
360
|
+
});
|
|
361
|
+
const answer = generated.answer;
|
|
362
|
+
const quality = generated.quality;
|
|
363
|
+
const retried = generated.retried;
|
|
364
|
+
|
|
365
|
+
updateSession(sessionId, task, identity.id, message, answer, workdir);
|
|
366
|
+
|
|
367
|
+
const inputTokens = runtime.estimateTokens(generated.prompt + "\n" + message);
|
|
368
|
+
const outputTokens = runtime.estimateTokens(answer);
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
answer,
|
|
372
|
+
task_type: task.type,
|
|
373
|
+
role: task.role,
|
|
374
|
+
identity: identity.id,
|
|
375
|
+
routing,
|
|
376
|
+
references: autoReference.references,
|
|
377
|
+
quality: { score: quality.score, flags: quality.reasons, retried, blocked: quality.blocked || false, critical: quality.critical || false },
|
|
378
|
+
tokens: { input: inputTokens, output: outputTokens, total: inputTokens + outputTokens }
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function handleAgentRequest(body = {}) {
|
|
383
|
+
const goal = String(body.goal || body.message || "").trim();
|
|
384
|
+
const sessionId = String(body.session_id || "default");
|
|
385
|
+
const workdir = path.resolve(body.workdir || process.cwd());
|
|
386
|
+
const preferredIdentity = body.identity && body.identity !== "auto" ? String(body.identity) : undefined;
|
|
387
|
+
const session = memory.loadSession(sessionId);
|
|
388
|
+
const routing = runtime.classifyTaskDetailed(goal, session);
|
|
389
|
+
const task = routing.task;
|
|
390
|
+
const identity = runtime.resolveIdentity(goal, task, session, preferredIdentity);
|
|
391
|
+
const autoReference = await collectAutoReferencePrompt(goal, task);
|
|
392
|
+
|
|
393
|
+
let status = "completed";
|
|
394
|
+
let steps = [];
|
|
395
|
+
let files = [];
|
|
396
|
+
let artifact = null;
|
|
397
|
+
let answer = "";
|
|
398
|
+
let references = autoReference.references;
|
|
399
|
+
let validation = null;
|
|
400
|
+
|
|
401
|
+
if (taskExecutor.isDirectActionTask(task)) {
|
|
402
|
+
const direct = await taskExecutor.executeDirectTask({ task, message: goal, session, workdir });
|
|
403
|
+
status = direct.status;
|
|
404
|
+
steps = direct.steps || [];
|
|
405
|
+
answer = direct.answer;
|
|
406
|
+
files = direct.files || [];
|
|
407
|
+
references = direct.references || [];
|
|
408
|
+
artifact = direct.artifact || null;
|
|
409
|
+
} else if (task.type === "website_builder") {
|
|
410
|
+
steps.push({ step: 1, action: "intent_detected", detail: "website_builder" });
|
|
411
|
+
steps.push({ step: 2, action: "identity_selected", detail: identity.id });
|
|
412
|
+
const built = await buildWebsiteArtifact(goal, workdir);
|
|
413
|
+
files = built.files;
|
|
414
|
+
artifact = built.artifact;
|
|
415
|
+
references = built.references;
|
|
416
|
+
validation = built.validation;
|
|
417
|
+
steps.push({ step: 3, action: "business_type_lock", detail: built.industry });
|
|
418
|
+
steps.push({ step: 4, action: "reference_analysis", detail: `${references.length} reference(s)` });
|
|
419
|
+
steps.push({ step: 5, action: "validity_gate", detail: validation.issues.length ? validation.issues.join(", ") : "passed" });
|
|
420
|
+
steps.push({ step: 6, action: "writing_files", files: files.map((file) => file.path) });
|
|
421
|
+
answer = `Landingpage gebaut. Business-Type-Lock: ${built.industry}. Referenzlogik und Validity-Gate wurden geprueft.`;
|
|
422
|
+
status = built.verified ? "completed" : "partial";
|
|
423
|
+
} else if (task.type === "file_generation") {
|
|
424
|
+
const filename = body.filename || "artifact.txt";
|
|
425
|
+
const write = await tools.executeTool("write_file", { path: filename, content: goal }, workdir);
|
|
426
|
+
files = [{ path: path.relative(workdir, write.path), content: goal }];
|
|
427
|
+
artifact = fileHelper.writeArtifact(filename, goal);
|
|
428
|
+
steps.push({ step: 1, action: "writing_file", files: [files[0].path] });
|
|
429
|
+
status = write.verified ? "completed" : "partial";
|
|
430
|
+
answer = "Datei erstellt.";
|
|
431
|
+
} else {
|
|
432
|
+
const generated = await generateVettedAnswer({
|
|
433
|
+
task,
|
|
434
|
+
session,
|
|
435
|
+
message: goal,
|
|
436
|
+
identityId: identity.id,
|
|
437
|
+
sessionId,
|
|
438
|
+
model: MODEL,
|
|
439
|
+
extraPrompt: autoReference.promptBlock
|
|
440
|
+
});
|
|
441
|
+
answer = generated.answer;
|
|
442
|
+
steps.push({ step: 1, action: "intent_detected", detail: task.type });
|
|
443
|
+
steps.push({ step: 2, action: "identity_selected", detail: identity.id });
|
|
444
|
+
steps.push({ step: 3, action: "response_generated" });
|
|
445
|
+
if (generated.retried) {
|
|
446
|
+
steps.push({ step: 4, action: "self_check_retry", detail: `${generated.retries} retry` });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
updateSession(sessionId, task, identity.id, goal, answer, workdir);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
status,
|
|
454
|
+
task_type: task.type,
|
|
455
|
+
role: task.role,
|
|
456
|
+
identity: identity.id,
|
|
457
|
+
routing,
|
|
458
|
+
steps,
|
|
459
|
+
answer,
|
|
460
|
+
workdir,
|
|
461
|
+
validation,
|
|
462
|
+
references,
|
|
463
|
+
files,
|
|
464
|
+
artifact,
|
|
465
|
+
verified: status === "completed"
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function runBackgroundJob(jobId, endpoint, handler, body) {
|
|
470
|
+
queueMicrotask(async () => {
|
|
471
|
+
jobs.updateJob(jobId, { status: "running", startedAt: Date.now(), error: null });
|
|
472
|
+
jobs.appendJobLog(jobId, "started", {
|
|
473
|
+
endpoint,
|
|
474
|
+
hasResumeContext: Boolean(body?.resume_context),
|
|
475
|
+
workdir: body?.workdir || process.cwd()
|
|
476
|
+
});
|
|
477
|
+
try {
|
|
478
|
+
const result = await handler(body);
|
|
479
|
+
jobs.appendJobLog(jobId, "completed", {
|
|
480
|
+
task_type: result.task_type,
|
|
481
|
+
status: result.status || "completed",
|
|
482
|
+
answer: String(result.answer || "").slice(0, 300),
|
|
483
|
+
fileCount: Array.isArray(result.files) ? result.files.length : 0,
|
|
484
|
+
referenceCount: Array.isArray(result.references) ? result.references.length : 0,
|
|
485
|
+
artifact: result.artifact?.filename || null
|
|
486
|
+
});
|
|
487
|
+
jobs.updateJob(jobId, { status: "completed", finishedAt: Date.now(), result });
|
|
488
|
+
} catch (error) {
|
|
489
|
+
jobs.appendJobLog(jobId, "failed", { error: error.message });
|
|
490
|
+
jobs.updateJob(jobId, { status: "failed", finishedAt: Date.now(), error: error.message, result: null });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function createApp() {
|
|
496
|
+
const app = express();
|
|
497
|
+
app.use(express.json({ limit: "10mb" }));
|
|
498
|
+
app.use(auth);
|
|
499
|
+
|
|
500
|
+
app.get("/identities", (req, res) => {
|
|
501
|
+
res.json({ identities: runtime.listIdentities() });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
app.get("/memory/facts", (req, res) => {
|
|
505
|
+
res.json({ facts: findFacts(String(req.query.q || "")) });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
app.post("/memory/facts", (req, res) => {
|
|
509
|
+
const record = storeFact({
|
|
510
|
+
scope: String(req.body.scope || "global"),
|
|
511
|
+
key: String(req.body.key || ""),
|
|
512
|
+
value: req.body.value,
|
|
513
|
+
tags: Array.isArray(req.body.tags) ? req.body.tags : []
|
|
514
|
+
});
|
|
515
|
+
res.json({ stored: true, record });
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
app.get("/memory/palace", (req, res) => {
|
|
519
|
+
const sessionId = String(req.query.session_id || "default");
|
|
520
|
+
res.json({
|
|
521
|
+
session_id: sessionId,
|
|
522
|
+
stats: palace.palaceStats(sessionId),
|
|
523
|
+
wake_up: palace.palaceWakeUp(sessionId)
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
app.post("/memory/palace/search", (req, res) => {
|
|
528
|
+
const sessionId = String(req.body.session_id || "default");
|
|
529
|
+
res.json({
|
|
530
|
+
session_id: sessionId,
|
|
531
|
+
result: palace.palaceSearch(String(req.body.query || ""), Number(req.body.limit || 5), sessionId)
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
app.post("/reference/inspect", async (req, res) => {
|
|
536
|
+
try {
|
|
537
|
+
const url = String(req.body.url || "").trim();
|
|
538
|
+
if (!url) return res.status(400).json({ error: "url is required" });
|
|
539
|
+
res.json(await inspectReference(url, { screenshot: req.body.screenshot !== false }));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
res.status(500).json({ error: error.message });
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
app.post("/reference/screenshot", async (req, res) => {
|
|
546
|
+
try {
|
|
547
|
+
const url = String(req.body.url || "").trim();
|
|
548
|
+
if (!url) return res.status(400).json({ error: "url is required" });
|
|
549
|
+
res.json(await screenshotUrl(url, { timeout: req.body.timeout }));
|
|
550
|
+
} catch (error) {
|
|
551
|
+
res.status(500).json({ error: error.message });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
app.get("/browser/status", requireLoopback, async (req, res) => {
|
|
556
|
+
try {
|
|
557
|
+
res.json(await browser.snapshot());
|
|
558
|
+
} catch (error) {
|
|
559
|
+
res.json({ url: null, title: "", text: "", error: error.message });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
app.post("/browser/open", requireLoopback, async (req, res) => {
|
|
564
|
+
try {
|
|
565
|
+
res.json(await browser.open(String(req.body.url || "")));
|
|
566
|
+
} catch (error) {
|
|
567
|
+
res.status(500).json({ error: error.message });
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
app.post("/browser/click", requireLoopback, async (req, res) => {
|
|
572
|
+
try {
|
|
573
|
+
res.json(await browser.click(String(req.body.selector || "")));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
res.status(500).json({ error: error.message });
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
app.post("/browser/type", requireLoopback, async (req, res) => {
|
|
580
|
+
try {
|
|
581
|
+
res.json(await browser.type(String(req.body.selector || ""), String(req.body.text || "")));
|
|
582
|
+
} catch (error) {
|
|
583
|
+
res.status(500).json({ error: error.message });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
app.post("/browser/press", requireLoopback, async (req, res) => {
|
|
588
|
+
try {
|
|
589
|
+
res.json(await browser.press(String(req.body.key || "Enter")));
|
|
590
|
+
} catch (error) {
|
|
591
|
+
res.status(500).json({ error: error.message });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
app.post("/browser/screenshot", requireLoopback, async (req, res) => {
|
|
596
|
+
try {
|
|
597
|
+
const targetPath = path.resolve(String(req.body.path || path.join(process.cwd(), `browser-shot-${Date.now()}.png`)));
|
|
598
|
+
res.json(await browser.screenshot(targetPath));
|
|
599
|
+
} catch (error) {
|
|
600
|
+
res.status(500).json({ error: error.message });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
app.post("/browser/close", requireLoopback, async (req, res) => {
|
|
605
|
+
try {
|
|
606
|
+
res.json(await browser.close());
|
|
607
|
+
} catch (error) {
|
|
608
|
+
res.status(500).json({ error: error.message });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
app.post("/chat", async (req, res) => {
|
|
613
|
+
try {
|
|
614
|
+
if (req.body.background) {
|
|
615
|
+
const job = jobs.createJob({
|
|
616
|
+
endpoint: "/chat",
|
|
617
|
+
sessionId: String(req.body.session_id || "default"),
|
|
618
|
+
payload: req.body,
|
|
619
|
+
mode: "chat"
|
|
620
|
+
});
|
|
621
|
+
jobs.appendJobLog(job.id, "queued", { mode: "chat", message: String(req.body.message || "").slice(0, 200) });
|
|
622
|
+
runBackgroundJob(job.id, "/chat", handleChatRequest, req.body);
|
|
623
|
+
return res.json({ status: "queued", job_id: job.id });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
res.json(await handleChatRequest(req.body));
|
|
627
|
+
} catch (error) {
|
|
628
|
+
res.status(500).json({ error: error.message });
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
app.post("/chat/stream", async (req, res) => {
|
|
633
|
+
try {
|
|
634
|
+
const message = String(req.body.message || "").trim();
|
|
635
|
+
const sessionId = String(req.body.session_id || "default");
|
|
636
|
+
const preferredIdentity = getIdentityId(req);
|
|
637
|
+
const session = memory.loadSession(sessionId);
|
|
638
|
+
const routing = runtime.classifyTaskDetailed(message, session);
|
|
639
|
+
const task = routing.task;
|
|
640
|
+
const identity = runtime.resolveIdentity(message, task, session, preferredIdentity);
|
|
641
|
+
|
|
642
|
+
if (taskExecutor.isDirectActionTask(task)) {
|
|
643
|
+
res.writeHead(200, {
|
|
644
|
+
"Content-Type": "text/event-stream",
|
|
645
|
+
"Cache-Control": "no-cache",
|
|
646
|
+
Connection: "keep-alive"
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const sendEvent = (type, payload) => {
|
|
650
|
+
res.write(`event: ${type}\n`);
|
|
651
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const direct = await taskExecutor.executeDirectTask({ task, message, session, workdir: process.cwd() });
|
|
655
|
+
updateSession(sessionId, task, identity.id, message, direct.answer, process.cwd());
|
|
656
|
+
sendEvent("meta", { task_type: task.type, role: task.role, identity: identity.id, routing });
|
|
657
|
+
sendEvent("done", {
|
|
658
|
+
answer: direct.answer,
|
|
659
|
+
install: direct.install,
|
|
660
|
+
files: direct.files || [],
|
|
661
|
+
references: direct.references || [],
|
|
662
|
+
artifact: direct.artifact || null,
|
|
663
|
+
quality: { score: 100, flags: [] }
|
|
664
|
+
});
|
|
665
|
+
res.end();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const model = task.type === "chat" && message.length < 50 ? FAST_MODEL : MODEL;
|
|
670
|
+
|
|
671
|
+
res.writeHead(200, {
|
|
672
|
+
"Content-Type": "text/event-stream",
|
|
673
|
+
"Cache-Control": "no-cache",
|
|
674
|
+
Connection: "keep-alive"
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const sendEvent = (type, payload) => {
|
|
678
|
+
res.write(`event: ${type}\n`);
|
|
679
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const prompt = composePromptWithPalace(task, session, message, identity.id, sessionId);
|
|
683
|
+
const messages = buildConversationMessages({
|
|
684
|
+
prompt,
|
|
685
|
+
session,
|
|
686
|
+
history: req.body.history,
|
|
687
|
+
message
|
|
688
|
+
});
|
|
689
|
+
sendEvent("meta", { task_type: task.type, role: task.role, identity: identity.id, routing });
|
|
690
|
+
let answer = await ollamaStream(messages, (token) => sendEvent("token", { text: token }), model);
|
|
691
|
+
let quality = runtime.selfCheckResponse(answer, task, message);
|
|
692
|
+
let retries = 0;
|
|
693
|
+
|
|
694
|
+
while (task.type !== "chat" && quality.blocked && retries < 2) {
|
|
695
|
+
answer = await generateRecoveredAnswer({
|
|
696
|
+
task,
|
|
697
|
+
session,
|
|
698
|
+
message,
|
|
699
|
+
identityId: identity.id,
|
|
700
|
+
sessionId,
|
|
701
|
+
previousAnswer: answer,
|
|
702
|
+
reasons: quality.reasons.length ? quality.reasons : ["action_mismatch"],
|
|
703
|
+
model
|
|
704
|
+
});
|
|
705
|
+
quality = runtime.selfCheckResponse(answer, task, message);
|
|
706
|
+
retries += 1;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (task.type !== "chat" && quality.blocked) {
|
|
710
|
+
answer = runtime.buildBlockedActionReply(task, quality.reasons);
|
|
711
|
+
quality = runtime.selfCheckResponse(answer, { ...task, type: "chat" }, message);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
updateSession(sessionId, task, identity.id, message, answer, process.cwd());
|
|
715
|
+
sendEvent("done", { answer, quality: { ...quality, retried: retries > 0 } });
|
|
716
|
+
res.end();
|
|
717
|
+
} catch (error) {
|
|
718
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`);
|
|
719
|
+
res.end();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
app.post("/agent", async (req, res) => {
|
|
724
|
+
try {
|
|
725
|
+
if (req.body.background) {
|
|
726
|
+
const job = jobs.createJob({
|
|
727
|
+
endpoint: "/agent",
|
|
728
|
+
sessionId: String(req.body.session_id || "default"),
|
|
729
|
+
payload: req.body,
|
|
730
|
+
mode: "agent"
|
|
731
|
+
});
|
|
732
|
+
jobs.appendJobLog(job.id, "queued", { mode: "agent", goal: String(req.body.goal || req.body.message || "").slice(0, 200) });
|
|
733
|
+
runBackgroundJob(job.id, "/agent", handleAgentRequest, req.body);
|
|
734
|
+
return res.json({ status: "queued", job_id: job.id });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
res.json(await handleAgentRequest(req.body));
|
|
738
|
+
} catch (error) {
|
|
739
|
+
res.status(500).json({ error: error.message });
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
app.post("/analyze", async (req, res) => {
|
|
744
|
+
try {
|
|
745
|
+
const type = req.body.type || "website";
|
|
746
|
+
let source = "";
|
|
747
|
+
|
|
748
|
+
if (req.body.html) source = String(req.body.html);
|
|
749
|
+
else if (req.body.url) source = await (await fetch(String(req.body.url))).text();
|
|
750
|
+
else return res.status(400).json({ error: "Provide html or url" });
|
|
751
|
+
|
|
752
|
+
const title = (source.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [, ""])[1].trim();
|
|
753
|
+
const h1s = [...source.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/ig)]
|
|
754
|
+
.map((match) => match[1].replace(/<[^>]+>/g, "").trim())
|
|
755
|
+
.filter(Boolean);
|
|
756
|
+
const links = [...source.matchAll(/<a\b/ig)].length;
|
|
757
|
+
const images = [...source.matchAll(/<img\b/ig)].length;
|
|
758
|
+
const analysis = [
|
|
759
|
+
`Seitentyp: ${type}`,
|
|
760
|
+
h1s.length ? `H1-Fokus: ${h1s[0]}` : "H1-Fokus: unklar",
|
|
761
|
+
"Kurzbewertung: Fokus auf Ziel, Hierarchie, CTA und Conversion statt technischem Head-Audit."
|
|
762
|
+
].join("\n");
|
|
763
|
+
|
|
764
|
+
res.json({
|
|
765
|
+
analysis,
|
|
766
|
+
meta: { title, h1s, links, images, responsive: /viewport/i.test(source), frameworks: [] }
|
|
767
|
+
});
|
|
768
|
+
} catch (error) {
|
|
769
|
+
res.status(500).json({ error: error.message });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
app.post("/classify", (req, res) => {
|
|
774
|
+
const routing = runtime.classifyTaskDetailed(String(req.body.message || ""), null);
|
|
775
|
+
const task = routing.task;
|
|
776
|
+
const identity = runtime.resolveIdentity(String(req.body.message || ""), task, null, getIdentityId(req));
|
|
777
|
+
res.json({
|
|
778
|
+
type: task.type,
|
|
779
|
+
role: task.role,
|
|
780
|
+
output: task.output,
|
|
781
|
+
reason: task.reason,
|
|
782
|
+
identity: identity.id,
|
|
783
|
+
confidence: routing.confidence,
|
|
784
|
+
scores: routing.scores
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
app.post("/learn", async (req, res) => {
|
|
789
|
+
try {
|
|
790
|
+
const content = req.body.url ? await (await fetch(String(req.body.url))).text() : String(req.body.content || "");
|
|
791
|
+
const title = String(req.body.title || "learn-input");
|
|
792
|
+
const category = String(req.body.category || "general");
|
|
793
|
+
const userId = String(req.body.user_id || req.body.session_id || "default");
|
|
794
|
+
const filePath = path.join(process.env.BLUN_HOME || path.join(os.homedir(), ".blun"), "knowledge", `${Date.now()}_${title.replace(/[^\w.-]/g, "_")}.txt`);
|
|
795
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
796
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
797
|
+
palace.palaceLearn(content.slice(0, 8000), category, userId);
|
|
798
|
+
|
|
799
|
+
const learned = userProfile.learnFromText(userId, content, { title, category });
|
|
800
|
+
if (title || category) {
|
|
801
|
+
storeFact({
|
|
802
|
+
scope: userId,
|
|
803
|
+
key: `learn:${category}:${title}`,
|
|
804
|
+
value: {
|
|
805
|
+
title,
|
|
806
|
+
category,
|
|
807
|
+
aliasesLearned: learned.aliasesLearned,
|
|
808
|
+
snippetsLearned: learned.snippetsLearned,
|
|
809
|
+
types: learned.types,
|
|
810
|
+
preview: learned.snippets.slice(0, 2)
|
|
811
|
+
},
|
|
812
|
+
tags: ["learn", category]
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
res.json({
|
|
817
|
+
stored: true,
|
|
818
|
+
category,
|
|
819
|
+
user_id: userId,
|
|
820
|
+
file: filePath,
|
|
821
|
+
content_length: content.length,
|
|
822
|
+
learned
|
|
823
|
+
});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
res.status(500).json({ error: error.message });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
app.get("/eval/noisy-intents", (req, res) => {
|
|
830
|
+
res.json(evaluateNoisyIntentCorpus());
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
app.get("/jobs", (req, res) => {
|
|
834
|
+
res.json({
|
|
835
|
+
jobs: jobs.listJobs({
|
|
836
|
+
sessionId: req.query.session_id ? String(req.query.session_id) : undefined,
|
|
837
|
+
limit: Number(req.query.limit || 20)
|
|
838
|
+
}).map((job) => ({
|
|
839
|
+
...job,
|
|
840
|
+
summary: jobs.summarizeJob(job)
|
|
841
|
+
}))
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
app.get("/jobs/:id", (req, res) => {
|
|
846
|
+
const job = jobs.getJob(String(req.params.id || ""));
|
|
847
|
+
if (!job) return res.status(404).json({ error: "Job not found" });
|
|
848
|
+
res.json({
|
|
849
|
+
...job,
|
|
850
|
+
summary: jobs.summarizeJob(job)
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
app.get("/jobs/:id/logs", (req, res) => {
|
|
855
|
+
const job = jobs.getJob(String(req.params.id || ""));
|
|
856
|
+
if (!job) return res.status(404).json({ error: "Job not found" });
|
|
857
|
+
res.json({ logs: jobs.getJobLogs(job.id) });
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
app.post("/jobs/:id/resume", (req, res) => {
|
|
861
|
+
const cloned = jobs.cloneJobForResume(String(req.params.id || ""));
|
|
862
|
+
if (!cloned) return res.status(404).json({ error: "Job not found" });
|
|
863
|
+
const handler = cloned.endpoint === "/chat" ? handleChatRequest : handleAgentRequest;
|
|
864
|
+
jobs.appendJobLog(cloned.id, "resumed", { from: req.params.id });
|
|
865
|
+
runBackgroundJob(cloned.id, cloned.endpoint, handler, cloned.payload);
|
|
866
|
+
res.json({ status: "queued", job_id: cloned.id, resumed_from: req.params.id });
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
app.post("/generate-file", async (req, res) => {
|
|
870
|
+
try {
|
|
871
|
+
const prompt = String(req.body.prompt || "").trim();
|
|
872
|
+
const format = String(req.body.format || "txt").toLowerCase();
|
|
873
|
+
const filename = String(req.body.filename || `artifact.${format}`);
|
|
874
|
+
const workdir = path.resolve(req.body.workdir || process.cwd());
|
|
875
|
+
|
|
876
|
+
let content = prompt;
|
|
877
|
+
if (format === "html") {
|
|
878
|
+
content = buildWebsiteProject(prompt, []).files["index.html"];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const write = await tools.executeTool("write_file", { path: filename, content }, workdir);
|
|
882
|
+
const artifact = fileHelper.writeArtifact(filename, content);
|
|
883
|
+
|
|
884
|
+
res.json({
|
|
885
|
+
artifact_id: artifact.artifact_id,
|
|
886
|
+
filename,
|
|
887
|
+
format,
|
|
888
|
+
size: artifact.size,
|
|
889
|
+
download: `/download/${artifact.artifact_id}`,
|
|
890
|
+
content,
|
|
891
|
+
files: [{ path: path.relative(workdir, write.path), content }],
|
|
892
|
+
workdir,
|
|
893
|
+
verified: write.verified
|
|
894
|
+
});
|
|
895
|
+
} catch (error) {
|
|
896
|
+
res.status(500).json({ error: error.message });
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
app.get("/download/:id", (req, res) => {
|
|
901
|
+
const artifact = fileHelper.findArtifact(req.params.id);
|
|
902
|
+
if (!artifact) return res.status(404).json({ error: "Artifact not found" });
|
|
903
|
+
res.download(artifact.full, artifact.filename);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
app.get("/artifacts", (req, res) => {
|
|
907
|
+
res.json({ artifacts: fileHelper.listArtifacts() });
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
app.post("/score", (req, res) => {
|
|
911
|
+
const task = { type: req.body.task_type || "chat", role: "default", output: "text" };
|
|
912
|
+
const result = runtime.scoreResponse(String(req.body.answer || ""), task, String(req.body.userMessage || ""));
|
|
913
|
+
res.json({ score: result.score, reasons: result.reasons });
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
app.post("/tools", async (req, res) => {
|
|
917
|
+
try {
|
|
918
|
+
const result = await tools.executeTool(req.body.tool, req.body.params || {}, req.body.workdir || process.cwd());
|
|
919
|
+
res.json(result);
|
|
920
|
+
} catch (error) {
|
|
921
|
+
res.status(500).json({ error: error.message });
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
app.get("/runtime/status", (req, res) => {
|
|
926
|
+
res.json({
|
|
927
|
+
model: MODEL,
|
|
928
|
+
fast_model: FAST_MODEL,
|
|
929
|
+
ollama: OLLAMA_URL,
|
|
930
|
+
uptime: process.uptime(),
|
|
931
|
+
prompt_registry: runtime.REGISTRY,
|
|
932
|
+
identities: runtime.listIdentities().map(({ id, name }) => ({ id, name })),
|
|
933
|
+
token_mode: TOKENS.length ? "token" : "local-only",
|
|
934
|
+
endpoints: ["/chat", "/chat/stream", "/agent", "/analyze", "/classify", "/learn", "/jobs", "/jobs/:id", "/jobs/:id/logs", "/jobs/:id/resume", "/generate-file", "/download/:id", "/artifacts", "/score", "/tools", "/runtime/status", "/versions", "/identities", "/memory/facts", "/memory/palace", "/memory/palace/search", "/reference/inspect", "/reference/screenshot", "/browser/status", "/browser/open", "/browser/click", "/browser/type", "/browser/press", "/browser/screenshot", "/browser/close", "/eval/noisy-intents", "/health"]
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
app.get("/versions", (req, res) => {
|
|
939
|
+
res.json(runtime.REGISTRY);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
app.get("/health", (req, res) => {
|
|
943
|
+
res.json({ status: "ok", model: MODEL, uptime: Math.round(process.uptime()) });
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
return app;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function startServer(port = PORT) {
|
|
950
|
+
const app = createApp();
|
|
951
|
+
const server = app.listen(port, "0.0.0.0", () => {
|
|
952
|
+
const authMode = TOKENS.length ? "token" : "local-only";
|
|
953
|
+
console.log(`BLUN King API listening on http://0.0.0.0:${port} (${authMode})`);
|
|
954
|
+
});
|
|
955
|
+
return { app, server };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (require.main === module) {
|
|
959
|
+
startServer();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
module.exports = {
|
|
963
|
+
createApp,
|
|
964
|
+
startServer
|
|
965
|
+
};
|