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.
Files changed (51) hide show
  1. package/api.js +965 -0
  2. package/blun-cli.js +763 -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,66 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ test("user profile learns and resolves install aliases", () => {
8
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "blun-profile-"));
9
+ process.env.BLUN_HOME = base;
10
+ delete require.cache[require.resolve("../user-profile")];
11
+ const profile = require("../user-profile");
12
+
13
+ profile.learnInstallAlias("user-1", "telegarm bot", "node-telegram-bot-api");
14
+ const resolved = profile.resolveInstallAlias("user-1", "mach mal telegarm bot");
15
+
16
+ assert.equal(resolved, "node-telegram-bot-api");
17
+ });
18
+
19
+ test("user profile learns aliases and reusable snippets from /learn content", () => {
20
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "blun-profile-"));
21
+ process.env.BLUN_HOME = base;
22
+ delete require.cache[require.resolve("../user-profile")];
23
+ const profile = require("../user-profile");
24
+
25
+ const learned = profile.learnFromText(
26
+ "user-2",
27
+ "telegramm bot = node-telegram-bot-api\n\nNode ist mein Standard fuer Bots und Automationen.",
28
+ { title: "preferences", category: "system" }
29
+ );
30
+
31
+ const installResolved = profile.resolveInstallAlias("user-2", "installier telegramm bot");
32
+ const promptContext = profile.buildPromptContext("user-2", "ich will einen bot installieren");
33
+
34
+ assert.equal(learned.aliasesLearned >= 1, true);
35
+ assert.equal(learned.snippetsLearned >= 1, true);
36
+ assert.equal(installResolved, "node-telegram-bot-api");
37
+ assert.match(promptContext, /Node ist mein Standard/);
38
+ });
39
+
40
+ test("user profile classifies learned text into preferences workflows and guardrails", () => {
41
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "blun-profile-"));
42
+ process.env.BLUN_HOME = base;
43
+ delete require.cache[require.resolve("../user-profile")];
44
+ const profile = require("../user-profile");
45
+
46
+ const learned = profile.learnFromText(
47
+ "user-3",
48
+ [
49
+ "Ich nutze immer Node fuer Bots.",
50
+ "Wenn ich deploy sage, meine ich SSH Deploy auf meinen Server.",
51
+ "Mach niemals Landingpages wenn ich installieren sage.",
52
+ "Ich nutze Gemma 4 27b als Modell."
53
+ ].join("\n\n"),
54
+ { title: "rules", category: "system" }
55
+ );
56
+
57
+ const context = profile.buildPromptContext("user-3", "deploy und installieren");
58
+
59
+ assert.equal(learned.types.preferences >= 1, true);
60
+ assert.equal(learned.types.workflows >= 1, true);
61
+ assert.equal(learned.types.guardrails >= 1, true);
62
+ assert.equal(learned.types.facts >= 1, true);
63
+ assert.match(context, /Praeferenz/);
64
+ assert.match(context, /Workflow/);
65
+ assert.match(context, /Verbot|Guardrail/);
66
+ });
@@ -0,0 +1,66 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+
4
+ const { detectIndustry, buildWebsiteProject, validateWebsiteOutput } = require("../website-builder");
5
+
6
+ test("detects fashion-like references and does not drift into florist output", () => {
7
+ const industry = detectIndustry("bau eine landingpage fuer nicrazy", [
8
+ {
9
+ url: "https://nicrazy.com",
10
+ title: "Nicrazy Streetwear",
11
+ h1s: ["New Drop"],
12
+ text: "streetwear collection hoodie tee drop"
13
+ }
14
+ ]);
15
+
16
+ assert.equal(industry, "fashion");
17
+ });
18
+
19
+ test("keeps nicrazy as fashion even when the reference page is generic", () => {
20
+ const industry = detectIndustry("bau eine landingpage fuer nicrazy mit referenz https://example.com", [
21
+ {
22
+ url: "https://example.com",
23
+ title: "Example Domain",
24
+ h1s: ["Example Domain"],
25
+ text: "This domain is for use in documentation examples."
26
+ }
27
+ ]);
28
+
29
+ assert.equal(industry, "fashion");
30
+ });
31
+
32
+ test("build output for fashion references contains brand-specific commerce language", () => {
33
+ const built = buildWebsiteProject("bau eine landingpage fuer nicrazy", [
34
+ {
35
+ url: "https://nicrazy.com",
36
+ title: "Nicrazy Streetwear",
37
+ h1s: ["New Drop"],
38
+ text: "streetwear collection hoodie tee drop"
39
+ }
40
+ ]);
41
+
42
+ assert.equal(built.industry, "fashion");
43
+ assert.equal(/drop|collection|shop/i.test(built.files["index.html"]), true);
44
+ assert.equal(/flower|bouquet|florist/i.test(built.files["index.html"]), false);
45
+ });
46
+
47
+ test("validity gate blocks malformed html css and generic drift", () => {
48
+ const result = validateWebsiteOutput(
49
+ {
50
+ "index.html": '<html><body><p="broken"></p><section>Featured Pieces</section></></body>',
51
+ "style.css": "body{font-family:sans-hard;} .x{z-top:1000;color:rgba(255,2HT,255,0.7);}"
52
+ },
53
+ {
54
+ industry: "fashion",
55
+ config: {
56
+ requiredTerms: ["drop", "collection"],
57
+ bannedTerms: ["flower", "bouquet", "florist"]
58
+ }
59
+ }
60
+ );
61
+
62
+ assert.equal(result.ok, false);
63
+ assert.equal(result.issues.includes("invalid_html_attributes"), true);
64
+ assert.equal(result.issues.includes("invalid_css_tokens"), true);
65
+ assert.equal(result.issues.includes("generic_template_detected"), true);
66
+ });
@@ -0,0 +1,53 @@
1
+ <!doctype html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>nicrazy - Reference Build</title>
7
+ <link rel="stylesheet" href="./style.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div class="logo">nicrazy</div>
12
+ <nav>
13
+ <a href="#showcase">Drop</a>
14
+ <a href="#about">Collection</a>
15
+ <a href="#contact">Story</a>
16
+ </nav>
17
+ </header>
18
+
19
+ <main>
20
+ <section class="hero">
21
+ <div class="hero-copy">
22
+ <p class="eyebrow">Reference Build</p>
23
+ <h1>nicrazy baut Spannung statt Deko.</h1>
24
+ <p>Eine fokussierte Fashion-Landingpage mit klarer Markenhaltung, starkem Hero und sauberer Shop-Logik.</p>
25
+ <p class="reference-note">Referenz gelesen: Example Domain. Business-Type-Lock: fashion.</p>
26
+ <a class="btn" href="#showcase">Zum Drop</a>
27
+ </div>
28
+ </section>
29
+
30
+ <section class="benefits" id="about">
31
+ <article><h2>Drop-Fokus</h2><p>Produkt, Kampagne und Kaufimpuls greifen direkt ineinander.</p></article><article><h2>Markenwelt</h2><p>Typografie, Bildsprache und Copy bleiben nah an einer modernen Label-Ästhetik.</p></article><article><h2>Conversion</h2><p>Hero, Collection-Teaser und CTA führen ohne Streuverlust in den Shop.</p></article>
32
+ </section>
33
+
34
+ <section class="products" id="showcase">
35
+ <h2>Current Collection</h2>
36
+ <div class="grid">
37
+ <div class="card"><div class="thumb"></div><h3>Hero Product</h3><p>nicrazy bleibt in einer fashion-nahen Markenlogik statt in generischen Platzhaltern.</p></div><div class="card"><div class="thumb"></div><h3>Signature Layer</h3><p>nicrazy bleibt in einer fashion-nahen Markenlogik statt in generischen Platzhaltern.</p></div><div class="card"><div class="thumb"></div><h3>New Arrival</h3><p>nicrazy bleibt in einer fashion-nahen Markenlogik statt in generischen Platzhaltern.</p></div>
38
+ </div>
39
+ </section>
40
+
41
+ <section class="social-proof">
42
+ <h2>Warum das zur Referenz passt</h2>
43
+ <p>Die Seite bleibt in der Welt eines Fashion- oder Brand-Auftritts und driftet nicht in branchenfremde Templates ab.</p>
44
+ </section>
45
+
46
+ <section class="cta" id="contact">
47
+ <h2>Zum Drop</h2>
48
+ <p>Klare CTA-Logik, saubere Struktur und keine Branchen-Drifts.</p>
49
+ <a class="btn" href="#showcase">Zum Drop</a>
50
+ </section>
51
+ </main>
52
+ </body>
53
+ </html>
@@ -0,0 +1,110 @@
1
+ :root{
2
+ --bg:#09090b;
3
+ --surface:#121826;
4
+ --text:#f5f5f5;
5
+ --muted:#a1a1aa;
6
+ --line:rgba(255,255,255,0.08);
7
+ --accent:#ff4d6d;
8
+ }
9
+ *{box-sizing:border-box}
10
+ html,body{margin:0;padding:0}
11
+ body{
12
+ font-family:Inter,system-ui,sans-serif;
13
+ background:radial-gradient(circle at top right, rgba(255,77,109,0.18), transparent 28%), var(--bg);
14
+ color:var(--text);
15
+ }
16
+ .site-header{
17
+ position:sticky;
18
+ top:0;
19
+ z-index:20;
20
+ display:flex;
21
+ justify-content:space-between;
22
+ align-items:center;
23
+ padding:18px 32px;
24
+ backdrop-filter:blur(12px);
25
+ background:rgba(9,9,11,0.88);
26
+ border-bottom:1px solid var(--line);
27
+ }
28
+ .site-header nav a{
29
+ color:var(--muted);
30
+ text-decoration:none;
31
+ margin-left:18px;
32
+ }
33
+ .logo{
34
+ font-weight:800;
35
+ letter-spacing:0.08em;
36
+ text-transform:uppercase;
37
+ }
38
+ .hero{
39
+ min-height:72vh;
40
+ display:grid;
41
+ place-items:center;
42
+ padding:80px 24px 56px;
43
+ }
44
+ .hero-copy{
45
+ max-width:820px;
46
+ text-align:center;
47
+ }
48
+ .eyebrow{
49
+ text-transform:uppercase;
50
+ letter-spacing:0.2em;
51
+ color:var(--muted);
52
+ font-size:12px;
53
+ }
54
+ h1{
55
+ font-size:clamp(42px,8vw,84px);
56
+ line-height:0.96;
57
+ margin:10px 0 18px;
58
+ }
59
+ .reference-note{
60
+ color:var(--muted);
61
+ font-size:14px;
62
+ }
63
+ .btn{
64
+ display:inline-block;
65
+ margin-top:22px;
66
+ padding:14px 22px;
67
+ border-radius:999px;
68
+ background:var(--accent);
69
+ color:#fff;
70
+ text-decoration:none;
71
+ font-weight:700;
72
+ }
73
+ .benefits,.products,.social-proof,.cta{
74
+ max-width:1200px;
75
+ margin:0 auto;
76
+ padding:72px 24px;
77
+ }
78
+ .benefits,.grid{
79
+ display:grid;
80
+ grid-template-columns:repeat(3,minmax(0,1fr));
81
+ gap:18px;
82
+ }
83
+ .benefits article,.card,.social-proof{
84
+ background:var(--surface);
85
+ border:1px solid var(--line);
86
+ border-radius:20px;
87
+ padding:24px;
88
+ }
89
+ .thumb{
90
+ height:240px;
91
+ border-radius:16px;
92
+ background:linear-gradient(135deg,#1f2937,#0f172a);
93
+ }
94
+ .cta{
95
+ text-align:center;
96
+ }
97
+ .hero p,.benefits p,.card p,.social-proof p,.cta p{
98
+ color:var(--muted);
99
+ }
100
+ @media (max-width:900px){
101
+ .benefits,.grid{
102
+ grid-template-columns:1fr;
103
+ }
104
+ .site-header{
105
+ padding:16px 18px;
106
+ }
107
+ h1{
108
+ font-size:44px;
109
+ }
110
+ }
@@ -0,0 +1,66 @@
1
+ <!doctype html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>nicrazy - Neue Landingpage</title>
7
+ <link rel="stylesheet" href="./style.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div class="logo">nicrazy</div>
12
+ <nav>
13
+ <a href="#shop">Shop</a>
14
+ <a href="#about">About</a>
15
+ <a href="#contact">Kontakt</a>
16
+ </nav>
17
+ </header>
18
+
19
+ <main>
20
+ <section class="hero">
21
+ <div class="hero-copy">
22
+ <p class="eyebrow">New Energy</p>
23
+ <h1>Mehr Haltung. Weniger Chaos.</h1>
24
+ <p>Eine neue Landingpage, klarer, hochwertiger und deutlich starker auf Conversion gebaut.</p>
25
+ <p class="reference-note">Inspiriert von https://example.com mit Fokus auf Example Domain.</p>
26
+ <a class="btn" href="#shop">Jetzt entdecken</a>
27
+ </div>
28
+ </section>
29
+
30
+ <section class="benefits" id="about">
31
+ <article>
32
+ <h2>Starkerer Auftritt</h2>
33
+ <p>Mehr Fokus, bessere Hierarchie und klarere Botschaft statt visuellem Chaos.</p>
34
+ </article>
35
+ <article>
36
+ <h2>Bessere Conversion</h2>
37
+ <p>CTA, Vertrauen und Struktur greifen sauber ineinander und fuhren schneller zum Klick.</p>
38
+ </article>
39
+ <article>
40
+ <h2>Mobile First</h2>
41
+ <p>Die Seite wirkt auf dem Handy genauso stark wie auf Desktop und bleibt sofort verstandlich.</p>
42
+ </article>
43
+ </section>
44
+
45
+ <section class="products" id="shop">
46
+ <h2>Featured Pieces</h2>
47
+ <div class="grid">
48
+ <div class="card"><div class="thumb"></div><h3>Signature Piece</h3><p>Klare Produktdarstellung mit mehr Wirkung.</p></div>
49
+ <div class="card"><div class="thumb"></div><h3>New Drop</h3><p>Mehr Fokus auf Produkt und Stil, weniger Ablenkung.</p></div>
50
+ <div class="card"><div class="thumb"></div><h3>Daily Layer</h3><p>Starkeres Vertrauen, bessere Lesbarkeit, sauberer Flow.</p></div>
51
+ </div>
52
+ </section>
53
+
54
+ <section class="social-proof">
55
+ <h2>Warum das besser funktioniert</h2>
56
+ <p>Die neue Seite bleibt visuell stark, gibt aber klare Fuhrung, eine saubere Botschaft und sichtbare CTAs.</p>
57
+ </section>
58
+
59
+ <section class="cta" id="contact">
60
+ <h2>Bereit fur den nachsten Drop?</h2>
61
+ <p>Klare Navigation, klare Botschaft, sauberer Abschluss.</p>
62
+ <a class="btn" href="#shop">Zum Shop</a>
63
+ </section>
64
+ </main>
65
+ </body>
66
+ </html>
@@ -0,0 +1,104 @@
1
+ :root{
2
+ --bg:#09090b;
3
+ --card:#111827;
4
+ --text:#f5f5f5;
5
+ --muted:#a1a1aa;
6
+ --accent:#ff4d6d;
7
+ }
8
+ *{box-sizing:border-box}
9
+ html,body{margin:0;padding:0}
10
+ body{
11
+ font-family:Inter,system-ui,sans-serif;
12
+ background:var(--bg);
13
+ color:var(--text);
14
+ }
15
+ .site-header{
16
+ position:sticky;top:0;z-index:20;
17
+ display:flex;justify-content:space-between;align-items:center;
18
+ padding:18px 32px;
19
+ background:rgba(9,9,11,.88);
20
+ backdrop-filter:blur(10px);
21
+ border-bottom:1px solid rgba(255,255,255,.06);
22
+ }
23
+ .site-header nav a{
24
+ color:var(--muted);
25
+ text-decoration:none;
26
+ margin-left:18px;
27
+ }
28
+ .logo{
29
+ font-weight:800;
30
+ letter-spacing:.08em;
31
+ }
32
+ .hero{
33
+ min-height:72vh;
34
+ display:grid;
35
+ place-items:center;
36
+ padding:64px 24px;
37
+ background:radial-gradient(circle at top right, rgba(255,77,109,.35), transparent 35%);
38
+ }
39
+ .hero-copy{
40
+ max-width:760px;
41
+ text-align:center;
42
+ }
43
+ .reference-note{
44
+ margin:18px 0 0;
45
+ color:var(--muted);
46
+ font-size:14px;
47
+ }
48
+ .eyebrow{
49
+ text-transform:uppercase;
50
+ letter-spacing:.18em;
51
+ color:var(--muted);
52
+ font-size:12px;
53
+ }
54
+ h1{
55
+ font-size:clamp(42px,8vw,86px);
56
+ line-height:.96;
57
+ margin:10px 0 18px;
58
+ }
59
+ .btn{
60
+ display:inline-block;
61
+ padding:14px 22px;
62
+ border-radius:999px;
63
+ background:var(--accent);
64
+ color:white;
65
+ text-decoration:none;
66
+ font-weight:700;
67
+ }
68
+ .benefits,.products,.cta,.social-proof{
69
+ padding:72px 24px;
70
+ max-width:1200px;
71
+ margin:0 auto;
72
+ }
73
+ .benefits{
74
+ display:grid;
75
+ grid-template-columns:repeat(3,1fr);
76
+ gap:18px;
77
+ }
78
+ .benefits article,.card,.social-proof{
79
+ background:var(--card);
80
+ border:1px solid #202938;
81
+ border-radius:20px;
82
+ padding:24px;
83
+ }
84
+ .grid{
85
+ display:grid;
86
+ grid-template-columns:repeat(3,1fr);
87
+ gap:18px;
88
+ }
89
+ .thumb{
90
+ height:280px;
91
+ border-radius:16px;
92
+ background:linear-gradient(135deg,#1f2937,#0f172a);
93
+ }
94
+ .cta{
95
+ text-align:center;
96
+ }
97
+ .cta p,.hero p,.card p,.benefits p,.social-proof p{
98
+ color:var(--muted);
99
+ }
100
+ @media (max-width:900px){
101
+ .benefits,.grid{grid-template-columns:1fr}
102
+ .site-header{padding:16px 18px}
103
+ h1{font-size:44px}
104
+ }
package/tools.js ADDED
@@ -0,0 +1,177 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { execSync } = require("child_process");
4
+ const fileHelper = require("./file-helper");
5
+ const browser = require("./browser-controller");
6
+
7
+ function safePath(workdir, rel) {
8
+ return fileHelper.ensureInside(workdir, rel);
9
+ }
10
+
11
+ function listFilesRecursive(dir, maxDepth = 3, prefix = "", level = 0) {
12
+ if (level > maxDepth) return [];
13
+ const out = [];
14
+
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
16
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
17
+ const rel = path.join(prefix, entry.name);
18
+ out.push(rel + (entry.isDirectory() ? "/" : ""));
19
+ if (entry.isDirectory()) {
20
+ out.push(...listFilesRecursive(path.join(dir, entry.name), maxDepth, rel, level + 1));
21
+ }
22
+ }
23
+
24
+ return out;
25
+ }
26
+
27
+ function isBlockedShellCommand(command) {
28
+ const cmd = String(command || "").trim().toLowerCase();
29
+ if (!cmd) return false;
30
+
31
+ const blockedPatterns = [
32
+ /(^|\s)rm\s+-rf(\s|$)/i,
33
+ /(^|\s)(shutdown|reboot|halt|poweroff)(\s|$)/i,
34
+ /(^|\s)(mkfs|format)(\s|$)/i,
35
+ /(^|\s)dd(\s|$)/i,
36
+ /(^|\s)(diskpart|bcdedit)(\s|$)/i,
37
+ /(^|\s)(passwd|useradd|userdel|net\s+user|deluser)(\s|$)/i,
38
+ /(^|\s)(iptables|ufw|netsh\s+advfirewall)(\s|$)/i,
39
+ /(^|\s)(scp|sftp|ssh)(\s|$)/i,
40
+ /curl\s+.+\|\s*(sh|bash|zsh|powershell|pwsh)(\s|$)/i,
41
+ /wget\s+.+\|\s*(sh|bash|zsh|powershell|pwsh)(\s|$)/i,
42
+ /(^|\s)(del|erase)\s+\/[sqf]/i,
43
+ /(^|\s)rd\s+\/s(\s|$)/i
44
+ ];
45
+
46
+ return blockedPatterns.some((pattern) => pattern.test(cmd));
47
+ }
48
+
49
+ async function executeTool(tool, params = {}, workdir) {
50
+ const ws = path.resolve(workdir || process.cwd());
51
+ if (!fs.existsSync(ws)) fs.mkdirSync(ws, { recursive: true });
52
+
53
+ switch (tool) {
54
+ case "read_file": {
55
+ const target = safePath(ws, params.path);
56
+ return { ok: true, tool, path: target, content: fs.readFileSync(target, "utf8") };
57
+ }
58
+ case "write_file": {
59
+ const target = safePath(ws, params.path);
60
+ fs.mkdirSync(path.dirname(target), { recursive: true });
61
+ fs.writeFileSync(target, params.content || "", "utf8");
62
+ return {
63
+ ok: true,
64
+ tool,
65
+ path: target,
66
+ verified: fs.existsSync(target),
67
+ size: fs.statSync(target).size,
68
+ content: String(params.content || "")
69
+ };
70
+ }
71
+ case "list_files":
72
+ return { ok: true, tool, workdir: ws, files: listFilesRecursive(ws, params.maxDepth || 3) };
73
+ case "create_project": {
74
+ const rootName = params.name ? String(params.name).replace(/[^\w.\-]/g, "_") : "project";
75
+ const root = safePath(ws, rootName);
76
+ fs.mkdirSync(root, { recursive: true });
77
+ const written = [];
78
+ const files = [];
79
+ const inputFiles = params.files || {};
80
+
81
+ for (const [rel, content] of Object.entries(inputFiles)) {
82
+ const target = safePath(root, rel);
83
+ fs.mkdirSync(path.dirname(target), { recursive: true });
84
+ fs.writeFileSync(target, String(content), "utf8");
85
+ const relFromWs = path.relative(ws, target);
86
+ written.push(relFromWs);
87
+ files.push({ path: relFromWs, content: String(content) });
88
+ }
89
+
90
+ const verified = written.every((entry) => fs.existsSync(path.join(ws, entry)));
91
+ return { ok: true, tool, projectRoot: root, files: written, fileObjects: files, verified };
92
+ }
93
+ case "playwright_screenshot": {
94
+ const url = String(params.url || params.target || "").trim();
95
+ if (!url) throw new Error("Missing url");
96
+
97
+ const filename = String(params.path || params.output || `reference-shot-${Date.now()}.png`);
98
+ const target = path.isAbsolute(filename) ? filename : safePath(ws, filename);
99
+
100
+ await browser.open(url);
101
+ try {
102
+ const shot = await browser.screenshot(target);
103
+ const snapshot = await browser.snapshot();
104
+ return {
105
+ ok: true,
106
+ tool,
107
+ url,
108
+ path: shot.path,
109
+ snapshot
110
+ };
111
+ } finally {
112
+ await browser.close().catch(() => {});
113
+ }
114
+ }
115
+ case "run_shell": {
116
+ const cmd = String(params.command || "").trim();
117
+ if (!cmd) throw new Error("Missing command");
118
+ if (isBlockedShellCommand(cmd)) throw new Error("Blocked command");
119
+ const out = execSync(cmd, {
120
+ cwd: ws,
121
+ encoding: "utf8",
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ timeout: params.timeout || 20000
124
+ });
125
+ return { ok: true, tool, output: out };
126
+ }
127
+ case "git_status":
128
+ return { ok: true, tool, output: execSync("git status --short --branch", { cwd: ws, encoding: "utf8" }) };
129
+ case "git_add_all":
130
+ execSync("git add -A", { cwd: ws, encoding: "utf8" });
131
+ return { ok: true, tool };
132
+ case "git_commit": {
133
+ const message = String(params.message || "BLUN KING update");
134
+ const out = execSync(`git add -A && git commit -m ${JSON.stringify(message)}`, { cwd: ws, encoding: "utf8" });
135
+ return { ok: true, tool, output: out, message };
136
+ }
137
+ case "git_push": {
138
+ const remote = params.remote || "origin";
139
+ const branch = params.branch || "HEAD";
140
+ const out = execSync(`git push ${remote} ${branch}`, { cwd: ws, encoding: "utf8" });
141
+ return { ok: true, tool, output: out };
142
+ }
143
+ case "ssh_exec":
144
+ case "deploy_script":
145
+ throw new Error(`${tool} is disabled on the server. Run deployment commands from the CLI locally.`);
146
+ default:
147
+ throw new Error(`Unknown tool: ${tool}`);
148
+ }
149
+ }
150
+
151
+ function parseToolCall(text) {
152
+ const fenced = String(text || "").match(/```tool\s*\n?\s*(\{[\s\S]*?\})\s*\n?\s*```/i);
153
+ if (fenced) {
154
+ try {
155
+ return JSON.parse(fenced[1]);
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ const inline = String(text || "").match(/\{\s*"tool"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[\s\S]*?\}\s*\}/);
162
+ if (inline) {
163
+ try {
164
+ return JSON.parse(inline[0]);
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ module.exports = {
174
+ executeTool,
175
+ parseToolCall,
176
+ isBlockedShellCommand
177
+ };