blun-king-cli 4.1.0 → 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 -3156
- package/setup.js +0 -30
package/blunking-api.js
ADDED
package/bot.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const TelegramBot = require("node-telegram-bot-api");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const runtime = require("./runtime");
|
|
5
|
+
const memory = require("./chat-memory");
|
|
6
|
+
const palace = require("./palace-memory");
|
|
7
|
+
const taskExecutor = require("./task-executor");
|
|
8
|
+
const { buildConversationMessages } = require("./message-builder");
|
|
9
|
+
|
|
10
|
+
const BOT_TOKEN = process.env.BLUN_TELEGRAM_TOKEN || process.env.BLUN_TELEGRAM_BOT_TOKEN || "";
|
|
11
|
+
const OLLAMA_URL = process.env.OLLAMA_URL || "http://127.0.0.1:11434/api/chat";
|
|
12
|
+
const MODEL = process.env.BLUN_MODEL || "blun-king-v500";
|
|
13
|
+
|
|
14
|
+
function stripThinking(text) {
|
|
15
|
+
return String(text || "").replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveSessionId(msg = {}) {
|
|
19
|
+
const chatId = String(msg.chat?.id || "default");
|
|
20
|
+
const chatType = String(msg.chat?.type || "private");
|
|
21
|
+
const userId = msg.from?.id ? String(msg.from.id) : "anonymous";
|
|
22
|
+
return chatType === "private" ? chatId : `${chatId}::${userId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parsePalaceCommand(text = "") {
|
|
26
|
+
const trimmed = String(text || "").trim();
|
|
27
|
+
if (!trimmed.startsWith("/palace")) return null;
|
|
28
|
+
|
|
29
|
+
const rest = trimmed.slice("/palace".length).trim();
|
|
30
|
+
if (!rest || rest === "status") return { action: "status", value: "" };
|
|
31
|
+
if (rest === "recall") return { action: "recall", value: "" };
|
|
32
|
+
if (rest.startsWith("search ")) return { action: "search", value: rest.slice(7).trim() };
|
|
33
|
+
if (rest.startsWith("store ")) return { action: "store", value: rest.slice(6).trim() };
|
|
34
|
+
if (rest.startsWith("learn ")) return { action: "learn", value: rest.slice(6).trim() };
|
|
35
|
+
return { action: "unknown", value: rest };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ollamaChat(messages) {
|
|
39
|
+
const resp = await fetch(OLLAMA_URL, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ model: MODEL, messages, stream: false })
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) throw new Error(`Ollama error ${resp.status}`);
|
|
45
|
+
const data = await resp.json();
|
|
46
|
+
return stripThinking(data.message?.content || "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function sendLongMessage(bot, chatId, text) {
|
|
50
|
+
const chunks = String(text || "").match(/[\s\S]{1,3900}/g) || [""];
|
|
51
|
+
for (const chunk of chunks) {
|
|
52
|
+
if (chunk.trim()) {
|
|
53
|
+
await bot.sendMessage(chatId, chunk);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handlePalaceCommand(bot, msg, command) {
|
|
59
|
+
const sessionId = resolveSessionId(msg);
|
|
60
|
+
const chatId = msg.chat.id;
|
|
61
|
+
|
|
62
|
+
switch (command.action) {
|
|
63
|
+
case "status": {
|
|
64
|
+
const wake = palace.palaceWakeUp(sessionId);
|
|
65
|
+
const stats = palace.palaceStats(sessionId);
|
|
66
|
+
return bot.sendMessage(chatId, `BLUN KING MEMORY PALACE\n\n${stats}\n\nWake-Up:\n${wake || "leer"}`);
|
|
67
|
+
}
|
|
68
|
+
case "search":
|
|
69
|
+
return sendLongMessage(bot, chatId, palace.palaceSearch(command.value, 5, sessionId));
|
|
70
|
+
case "store": {
|
|
71
|
+
const ok = palace.palaceStore(command.value, "telegram-user", sessionId);
|
|
72
|
+
return bot.sendMessage(chatId, ok ? "Gespeichert." : "Speichern fehlgeschlagen.");
|
|
73
|
+
}
|
|
74
|
+
case "learn": {
|
|
75
|
+
const ok = palace.palaceLearn(command.value, "telegram-user", sessionId);
|
|
76
|
+
return bot.sendMessage(chatId, ok ? "Gelernt und gespeichert." : "Lernen fehlgeschlagen.");
|
|
77
|
+
}
|
|
78
|
+
case "recall":
|
|
79
|
+
return sendLongMessage(bot, chatId, palace.palaceWakeUp(sessionId) || "Kein Kontext vorhanden.");
|
|
80
|
+
default:
|
|
81
|
+
return bot.sendMessage(chatId, "Unbekannter Palace-Befehl. Nutze /palace, /palace search <query>, /palace store <text>, /palace recall oder /palace learn <text>.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleMessage(bot, msg) {
|
|
86
|
+
const text = String(msg.text || "").trim();
|
|
87
|
+
if (!text) return;
|
|
88
|
+
|
|
89
|
+
if (text === "/start") {
|
|
90
|
+
await bot.sendMessage(msg.chat.id, "BLUN KING ist aktiv.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (text === "/tools") {
|
|
95
|
+
await bot.sendMessage(msg.chat.id, "Verfuegbar: Chat, Identitaeten, Memory Palace mit /palace, Browser- und Build-Faehigkeiten ueber die API/CLI.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const palaceCommand = parsePalaceCommand(text);
|
|
100
|
+
if (palaceCommand) {
|
|
101
|
+
await handlePalaceCommand(bot, msg, palaceCommand);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sessionId = resolveSessionId(msg);
|
|
106
|
+
const session = memory.loadSession(sessionId);
|
|
107
|
+
const task = runtime.classifyTask(text, session);
|
|
108
|
+
if (taskExecutor.isDirectActionTask(task)) {
|
|
109
|
+
const direct = await taskExecutor.executeDirectTask({
|
|
110
|
+
task,
|
|
111
|
+
message: text,
|
|
112
|
+
session,
|
|
113
|
+
workdir: path.join(process.env.BLUN_HOME || path.join(os.homedir(), ".blun"), "telegram-artifacts")
|
|
114
|
+
});
|
|
115
|
+
memory.appendHistory(sessionId, "user", text);
|
|
116
|
+
memory.appendHistory(sessionId, "assistant", direct.answer);
|
|
117
|
+
palace.palaceLogChat(sessionId, "user", text);
|
|
118
|
+
palace.palaceLogChat(sessionId, "assistant", direct.answer);
|
|
119
|
+
memory.setActiveTask(sessionId, {
|
|
120
|
+
type: task.type,
|
|
121
|
+
role: task.role,
|
|
122
|
+
output: task.output,
|
|
123
|
+
lastUserIntent: text
|
|
124
|
+
});
|
|
125
|
+
await bot.sendMessage(msg.chat.id, direct.answer);
|
|
126
|
+
if (task.type === "browser_capture" && Array.isArray(direct.references) && direct.references[0]?.screenshotPath) {
|
|
127
|
+
await bot.sendDocument(msg.chat.id, direct.references[0].screenshotPath).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const identity = runtime.resolveIdentity(text, task, session);
|
|
132
|
+
const palaceContext = palace.palaceWakeUp(sessionId);
|
|
133
|
+
const prompt = runtime.composeSystemPrompt(task, session, text, identity.id) +
|
|
134
|
+
(palaceContext ? `\n\n[Langzeitgedaechtnis]\n${palaceContext.slice(0, 2000)}` : "");
|
|
135
|
+
|
|
136
|
+
palace.palaceLogChat(sessionId, "user", text);
|
|
137
|
+
|
|
138
|
+
const answer = await ollamaChat(buildConversationMessages({ prompt, session, message: text }));
|
|
139
|
+
|
|
140
|
+
memory.appendHistory(sessionId, "user", text);
|
|
141
|
+
memory.appendHistory(sessionId, "assistant", answer);
|
|
142
|
+
palace.palaceLogChat(sessionId, "assistant", answer);
|
|
143
|
+
|
|
144
|
+
if ((task.type === "website_builder" || task.type === "file_generation" || task.type === "browser_capture" || task.type === "installation") && !task.followup) {
|
|
145
|
+
memory.setActiveTask(sessionId, {
|
|
146
|
+
type: task.type,
|
|
147
|
+
role: task.role,
|
|
148
|
+
output: task.output,
|
|
149
|
+
lastUserIntent: text,
|
|
150
|
+
identityId: identity.id
|
|
151
|
+
});
|
|
152
|
+
} else if (!task.followup && task.type === "chat") {
|
|
153
|
+
memory.clearActiveTask(sessionId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await sendLongMessage(bot, msg.chat.id, answer);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function startBot(token = BOT_TOKEN) {
|
|
160
|
+
if (!token) {
|
|
161
|
+
console.log("Telegram bot disabled (BLUN_TELEGRAM_TOKEN missing).");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const bot = new TelegramBot(token, { polling: true });
|
|
166
|
+
bot.on("message", async (msg) => {
|
|
167
|
+
try {
|
|
168
|
+
await handleMessage(bot, msg);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error("[telegram]", error);
|
|
171
|
+
await bot.sendMessage(msg.chat.id, `Fehler bei der Verarbeitung: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return bot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (require.main === module) {
|
|
178
|
+
startBot();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
stripThinking,
|
|
183
|
+
resolveSessionId,
|
|
184
|
+
parsePalaceCommand,
|
|
185
|
+
handlePalaceCommand,
|
|
186
|
+
handleMessage,
|
|
187
|
+
startBot
|
|
188
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { chromium } = require("playwright");
|
|
2
|
+
|
|
3
|
+
let browserInstance = null;
|
|
4
|
+
let pageInstance = null;
|
|
5
|
+
|
|
6
|
+
async function ensurePage() {
|
|
7
|
+
if (!browserInstance) {
|
|
8
|
+
browserInstance = await chromium.launch({ headless: true });
|
|
9
|
+
}
|
|
10
|
+
if (!pageInstance) {
|
|
11
|
+
pageInstance = await browserInstance.newPage({ viewport: { width: 1440, height: 960 } });
|
|
12
|
+
}
|
|
13
|
+
return pageInstance;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function open(url) {
|
|
17
|
+
const page = await ensurePage();
|
|
18
|
+
await page.goto(String(url), { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
19
|
+
return snapshot();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function click(selector) {
|
|
23
|
+
const page = await ensurePage();
|
|
24
|
+
await page.locator(String(selector)).first().click();
|
|
25
|
+
return snapshot();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function type(selector, text) {
|
|
29
|
+
const page = await ensurePage();
|
|
30
|
+
await page.locator(String(selector)).first().fill(String(text));
|
|
31
|
+
return snapshot();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function press(key) {
|
|
35
|
+
const page = await ensurePage();
|
|
36
|
+
await page.keyboard.press(String(key));
|
|
37
|
+
return snapshot();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function snapshot() {
|
|
41
|
+
const page = await ensurePage();
|
|
42
|
+
const text = await page.locator("body").innerText().catch(() => "");
|
|
43
|
+
return {
|
|
44
|
+
url: page.url(),
|
|
45
|
+
title: await page.title(),
|
|
46
|
+
text: String(text || "").trim().slice(0, 4000)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function screenshot(targetPath) {
|
|
51
|
+
const page = await ensurePage();
|
|
52
|
+
await page.screenshot({ path: String(targetPath), fullPage: true });
|
|
53
|
+
return { path: String(targetPath) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function close() {
|
|
57
|
+
if (pageInstance) {
|
|
58
|
+
await pageInstance.close().catch(() => {});
|
|
59
|
+
pageInstance = null;
|
|
60
|
+
}
|
|
61
|
+
if (browserInstance) {
|
|
62
|
+
await browserInstance.close().catch(() => {});
|
|
63
|
+
browserInstance = null;
|
|
64
|
+
}
|
|
65
|
+
return { closed: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
open,
|
|
70
|
+
click,
|
|
71
|
+
type,
|
|
72
|
+
press,
|
|
73
|
+
snapshot,
|
|
74
|
+
screenshot,
|
|
75
|
+
close
|
|
76
|
+
};
|
package/chat-memory.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const BASE_DIR = process.env.BLUN_HOME || path.join(os.homedir(), ".blun");
|
|
6
|
+
const MEMORY_DIR = path.join(BASE_DIR, "memory");
|
|
7
|
+
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
8
|
+
|
|
9
|
+
function fileFor(id) {
|
|
10
|
+
return path.join(MEMORY_DIR, `${String(id).replace(/[^a-zA-Z0-9._-]/g, "_")}.json`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function defaultSession(id) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
history: [],
|
|
17
|
+
activeTask: null,
|
|
18
|
+
summary: "",
|
|
19
|
+
createdAt: Date.now(),
|
|
20
|
+
updatedAt: Date.now()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadSession(id) {
|
|
25
|
+
const file = fileFor(id);
|
|
26
|
+
if (!fs.existsSync(file)) return defaultSession(id);
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return defaultSession(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveSession(id, data) {
|
|
35
|
+
data.updatedAt = Date.now();
|
|
36
|
+
fs.writeFileSync(fileFor(id), JSON.stringify(data, null, 2), "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendHistory(id, role, content) {
|
|
40
|
+
const s = loadSession(id);
|
|
41
|
+
s.history.push({ role, content, ts: Date.now() });
|
|
42
|
+
s.history = s.history.slice(-30);
|
|
43
|
+
saveSession(id, s);
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setActiveTask(id, task) {
|
|
48
|
+
const s = loadSession(id);
|
|
49
|
+
s.activeTask = task || null;
|
|
50
|
+
saveSession(id, s);
|
|
51
|
+
return s.activeTask;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clearActiveTask(id) {
|
|
55
|
+
return setActiveTask(id, null);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getContextMessages(id) {
|
|
59
|
+
const session = loadSession(id);
|
|
60
|
+
return (session.history || []).map((item) => ({
|
|
61
|
+
role: item.role,
|
|
62
|
+
content: item.content
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addMessage(id, role, content) {
|
|
67
|
+
return appendHistory(id, role, content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function autoCompact(id) {
|
|
71
|
+
const session = loadSession(id);
|
|
72
|
+
if ((session.history || []).length <= 24) return;
|
|
73
|
+
session.history = session.history.slice(-20);
|
|
74
|
+
saveSession(id, session);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveHistory(id, data = {}) {
|
|
78
|
+
const session = loadSession(id);
|
|
79
|
+
session.summary = String(data.summary || "");
|
|
80
|
+
session.history = Array.isArray(data.messages)
|
|
81
|
+
? data.messages
|
|
82
|
+
.filter((item) => item && item.role && typeof item.content === "string")
|
|
83
|
+
.map((item) => ({
|
|
84
|
+
role: item.role,
|
|
85
|
+
content: item.content,
|
|
86
|
+
ts: Date.now()
|
|
87
|
+
}))
|
|
88
|
+
.slice(-30)
|
|
89
|
+
: [];
|
|
90
|
+
saveSession(id, session);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
loadSession,
|
|
95
|
+
saveSession,
|
|
96
|
+
appendHistory,
|
|
97
|
+
setActiveTask,
|
|
98
|
+
clearActiveTask,
|
|
99
|
+
getContextMessages,
|
|
100
|
+
addMessage,
|
|
101
|
+
autoCompact,
|
|
102
|
+
saveHistory
|
|
103
|
+
};
|
package/file-helper.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
const BLUN_HOME = process.env.BLUN_HOME || path.join(os.homedir(), ".blun");
|
|
7
|
+
const ARTIFACT_DIR = path.join(BLUN_HOME, "artifacts");
|
|
8
|
+
if (!fs.existsSync(ARTIFACT_DIR)) fs.mkdirSync(ARTIFACT_DIR, { recursive: true });
|
|
9
|
+
|
|
10
|
+
function ensureInside(workdir, target) {
|
|
11
|
+
const root = path.resolve(workdir);
|
|
12
|
+
const dest = path.resolve(root, target);
|
|
13
|
+
const relative = path.relative(root, dest);
|
|
14
|
+
|
|
15
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
16
|
+
throw new Error("Path escapes workdir");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return dest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeArtifact(filename, content, encoding = "utf8") {
|
|
23
|
+
const id = crypto.randomBytes(12).toString("hex");
|
|
24
|
+
const safeName = filename.replace(/[^\w.\-]/g, "_");
|
|
25
|
+
const filePath = path.join(ARTIFACT_DIR, `${id}__${safeName}`);
|
|
26
|
+
fs.writeFileSync(filePath, content, encoding);
|
|
27
|
+
return {
|
|
28
|
+
artifact_id: id,
|
|
29
|
+
filename: safeName,
|
|
30
|
+
path: filePath,
|
|
31
|
+
size: fs.statSync(filePath).size
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function listArtifacts() {
|
|
36
|
+
return fs.readdirSync(ARTIFACT_DIR).map((entry) => {
|
|
37
|
+
const full = path.join(ARTIFACT_DIR, entry);
|
|
38
|
+
const stat = fs.statSync(full);
|
|
39
|
+
const [id, ...rest] = entry.split("__");
|
|
40
|
+
return {
|
|
41
|
+
artifact_id: id,
|
|
42
|
+
filename: rest.join("__"),
|
|
43
|
+
size: stat.size,
|
|
44
|
+
created: stat.mtimeMs,
|
|
45
|
+
download: `/download/${id}`
|
|
46
|
+
};
|
|
47
|
+
}).sort((a, b) => b.created - a.created);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findArtifact(id) {
|
|
51
|
+
const match = fs.readdirSync(ARTIFACT_DIR).find((entry) => entry.startsWith(`${id}__`));
|
|
52
|
+
if (!match) return null;
|
|
53
|
+
const full = path.join(ARTIFACT_DIR, match);
|
|
54
|
+
return { full, filename: match.split("__").slice(1).join("__") };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
ARTIFACT_DIR,
|
|
59
|
+
ensureInside,
|
|
60
|
+
writeArtifact,
|
|
61
|
+
listArtifacts,
|
|
62
|
+
findArtifact
|
|
63
|
+
};
|
package/fuzzy-match.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
function normalizeLoose(text) {
|
|
2
|
+
return String(text || "")
|
|
3
|
+
.normalize("NFD")
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/ß/g, "ss")
|
|
7
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
8
|
+
.replace(/\s+/g, " ")
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tokenize(text) {
|
|
13
|
+
return normalizeLoose(text)
|
|
14
|
+
.split(/[^a-z0-9]+/i)
|
|
15
|
+
.map((part) => part.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function editDistance(a, b) {
|
|
20
|
+
const left = String(a || "");
|
|
21
|
+
const right = String(b || "");
|
|
22
|
+
const rows = Array.from({ length: left.length + 1 }, () => new Array(right.length + 1).fill(0));
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i <= left.length; i++) rows[i][0] = i;
|
|
25
|
+
for (let j = 0; j <= right.length; j++) rows[0][j] = j;
|
|
26
|
+
|
|
27
|
+
for (let i = 1; i <= left.length; i++) {
|
|
28
|
+
for (let j = 1; j <= right.length; j++) {
|
|
29
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
30
|
+
rows[i][j] = Math.min(
|
|
31
|
+
rows[i - 1][j] + 1,
|
|
32
|
+
rows[i][j - 1] + 1,
|
|
33
|
+
rows[i - 1][j - 1] + cost
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return rows[left.length][right.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function approxWordMatch(token, target) {
|
|
42
|
+
const left = normalizeLoose(token);
|
|
43
|
+
const right = normalizeLoose(target);
|
|
44
|
+
if (!left || !right) return false;
|
|
45
|
+
if (left === right) return true;
|
|
46
|
+
|
|
47
|
+
const maxDistance = right.length >= 8 ? 2 : 1;
|
|
48
|
+
if (Math.abs(left.length - right.length) > maxDistance) return false;
|
|
49
|
+
return editDistance(left, right) <= maxDistance;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasApproxWord(text, candidates) {
|
|
53
|
+
const tokens = tokenize(text);
|
|
54
|
+
return candidates.some((candidate) => tokens.some((token) => approxWordMatch(token, candidate)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasApproxPhrase(text, phrase) {
|
|
58
|
+
const tokens = tokenize(text);
|
|
59
|
+
const parts = tokenize(phrase);
|
|
60
|
+
if (parts.length === 0 || tokens.length < parts.length) return false;
|
|
61
|
+
|
|
62
|
+
for (let index = 0; index <= tokens.length - parts.length; index++) {
|
|
63
|
+
const window = tokens.slice(index, index + parts.length);
|
|
64
|
+
const matches = window.every((token, partIndex) => approxWordMatch(token, parts[partIndex]));
|
|
65
|
+
if (matches) return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
normalizeLoose,
|
|
73
|
+
tokenize,
|
|
74
|
+
editDistance,
|
|
75
|
+
approxWordMatch,
|
|
76
|
+
hasApproxWord,
|
|
77
|
+
hasApproxPhrase
|
|
78
|
+
};
|
package/identities.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const IDENTITIES = {
|
|
2
|
+
default: {
|
|
3
|
+
id: "default",
|
|
4
|
+
name: "Core",
|
|
5
|
+
aliases: ["default", "core", "standard"],
|
|
6
|
+
prompt: "Arbeite direkt, klar und ohne Show. Antworte belastbar und bleibe nah an der Aufgabe."
|
|
7
|
+
},
|
|
8
|
+
architect: {
|
|
9
|
+
id: "architect",
|
|
10
|
+
name: "Architect",
|
|
11
|
+
aliases: ["architect", "architekt", "systemarchitekt"],
|
|
12
|
+
prompt: "Denke in Systemen, Verantwortlichkeiten, Schnittstellen und langfristiger Wartbarkeit."
|
|
13
|
+
},
|
|
14
|
+
analyst: {
|
|
15
|
+
id: "analyst",
|
|
16
|
+
name: "Analyst",
|
|
17
|
+
aliases: ["analyst", "analyse", "reviewer"],
|
|
18
|
+
prompt: "Bewerte Starken, Schwachen, Risiken und Muster sauber. Trenne Beobachtung von Vermutung."
|
|
19
|
+
},
|
|
20
|
+
debugger: {
|
|
21
|
+
id: "debugger",
|
|
22
|
+
name: "Debugger",
|
|
23
|
+
aliases: ["debugger", "debug", "fehlerjaeger", "fehlerjager"],
|
|
24
|
+
prompt: "Suche die Ursache statt Symptome. Arbeite reproduzierbar, technisch und testnah."
|
|
25
|
+
},
|
|
26
|
+
builder: {
|
|
27
|
+
id: "builder",
|
|
28
|
+
name: "Builder",
|
|
29
|
+
aliases: ["builder", "maker", "umsetzer"],
|
|
30
|
+
prompt: "Baue konkret, liefere nutzbare Artefakte und halte den Fokus auf funktionierenden Ergebnissen."
|
|
31
|
+
},
|
|
32
|
+
operator: {
|
|
33
|
+
id: "operator",
|
|
34
|
+
name: "Operator",
|
|
35
|
+
aliases: ["operator", "ops", "dispatcher"],
|
|
36
|
+
prompt: "Ordne Arbeit, erkenne Blocker, priorisiere sauber und halte Ablaufe knapp und eindeutig."
|
|
37
|
+
},
|
|
38
|
+
installer: {
|
|
39
|
+
id: "installer",
|
|
40
|
+
name: "Installer",
|
|
41
|
+
aliases: ["installer", "setup", "deployer"],
|
|
42
|
+
prompt: "Fuehre lokale Installationen, Setups und verifizierbare Aktionsschritte direkt aus statt darueber zu reden."
|
|
43
|
+
},
|
|
44
|
+
critic: {
|
|
45
|
+
id: "critic",
|
|
46
|
+
name: "Critic",
|
|
47
|
+
aliases: ["critic", "kritiker", "harte review"],
|
|
48
|
+
prompt: "Zeige Schwachstellen, Regressionen und naive Annahmen ohne Schonung, aber mit technischer Begrundung."
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function getIdentity(id) {
|
|
53
|
+
if (!id) return IDENTITIES.default;
|
|
54
|
+
return IDENTITIES[id] || IDENTITIES.default;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listIdentities() {
|
|
58
|
+
return Object.values(IDENTITIES).map(({ id, name, aliases, prompt }) => ({
|
|
59
|
+
id,
|
|
60
|
+
name,
|
|
61
|
+
aliases,
|
|
62
|
+
prompt
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findIdentityInText(text) {
|
|
67
|
+
const normalized = String(text || "").toLowerCase();
|
|
68
|
+
|
|
69
|
+
for (const identity of Object.values(IDENTITIES)) {
|
|
70
|
+
for (const alias of identity.aliases) {
|
|
71
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
if (new RegExp(`\\b${escaped}\\b`, "i").test(normalized)) {
|
|
73
|
+
return identity;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recommendIdentity(task) {
|
|
82
|
+
switch (task?.type) {
|
|
83
|
+
case "website_builder":
|
|
84
|
+
return IDENTITIES.builder;
|
|
85
|
+
case "debugging":
|
|
86
|
+
return IDENTITIES.debugger;
|
|
87
|
+
case "analysis":
|
|
88
|
+
return task.role === "web_ui" ? IDENTITIES.critic : IDENTITIES.analyst;
|
|
89
|
+
case "operator":
|
|
90
|
+
return IDENTITIES.operator;
|
|
91
|
+
case "coding":
|
|
92
|
+
return IDENTITIES.architect;
|
|
93
|
+
case "installation":
|
|
94
|
+
return IDENTITIES.installer;
|
|
95
|
+
default:
|
|
96
|
+
return IDENTITIES.default;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
IDENTITIES,
|
|
102
|
+
getIdentity,
|
|
103
|
+
listIdentities,
|
|
104
|
+
findIdentityInText,
|
|
105
|
+
recommendIdentity
|
|
106
|
+
};
|