mythos-code 1.7.8 → 1.10.14

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 (3) hide show
  1. package/README.md +2 -0
  2. package/bin/mythos.js +657 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,3 +7,5 @@ npm install -g mythos-code
7
7
  mythos login
8
8
  mythos
9
9
  ```
10
+
11
+ Updates kommen automatisch beim Start (abschalten: `MYTHOS_NO_UPDATE=1`). Manuell: `mythos update`.
package/bin/mythos.js CHANGED
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import readline from "node:readline";
7
+ import http from "node:http";
7
8
  import { exec, spawn, execFileSync } from "node:child_process";
8
9
 
9
10
  // ---------- Gemeinsame Werkzeuge (CLI + Desktop-App) ----------
@@ -16,7 +17,7 @@ import { exec, spawn, execFileSync } from "node:child_process";
16
17
  // {"name":…,"arguments":{…}} statt flacher Felder, echte Zeilenumbrüche in Strings oder abgeschnittenes JSON.
17
18
  // Nichts davon darf als Rohtext im Chat landen.
18
19
 
19
- const TOOL_NAMES = ["run", "read", "edit", "write", "search", "ls", "websearch", "fetch", "cloud", "todo"];
20
+ const TOOL_NAMES = ["run", "start", "logs", "stop", "serve", "check", "verify", "read", "edit", "write", "search", "ls", "websearch", "fetch", "cloud", "todo"];
20
21
  const TOOL_OPEN_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>/i;
21
22
  // Ein ganzer Block – auch nicht geschlossen (dann bis zum Textende).
22
23
  const TOOL_BLOCK_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>[\s\S]*?(?:<\/\1\s*>|$)/gi;
@@ -110,10 +111,68 @@ function toolProblem(t, extra) {
110
111
  return TOOL_NAMES.includes(t.name) ? "" : "FEHLER: Werkzeug „" + t.name + "“ gibt es nicht. " + list + tail;
111
112
  }
112
113
 
114
+ // Befehle, die nicht von selbst enden (Dev-Server, Watcher, Bots) → im Hintergrund statt mit run.
115
+ const SERVER_RES = [
116
+ /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview|watch)\b(?!\s*:?\s*(?:build|test))/i,
117
+ /\b(?:npx\s+|bunx\s+)?vite(?:\s+(?:dev|serve|preview))?(?:\s+--?\w[\w-]*(?:[ =]\S+)?)*\s*(?:$|[;&|])/i,
118
+ /\b(?:next|nuxt|nuxi|astro|remix|gatsby)\s+(?:dev|start|develop|preview)\b/i,
119
+ /\b(?:webpack(?:-dev-server|\s+serve)|react-scripts\s+start|ng\s+serve|vue-cli-service\s+serve|parcel(?!\s+build)\b)/i,
120
+ /\b(?:live-server|http-server|lite-server|browser-sync)\b|\bnpx\s+serve\b|\bserve\s+(?:-[sl]\b|\.|\S*dist|\S*build|\S*public)/i,
121
+ /\b(?:nodemon|ts-node-dev|tsx\s+watch|node\s+--watch)\b|\s--watch\b/i,
122
+ /\bpython\d?(?:\.exe)?\s+-m\s+http\.server\b|\bphp\s+-S\b|\bflask\s+run\b|\buvicorn\b|\bgunicorn\b|\bmanage\.py\s+runserver\b|\brails\s+s(?:erver)?\b|\b(?:jekyll|hugo)\s+serve(?:r)?\b/i,
123
+ /\bnode\s+(?:\S*[\\/])?(?:server|app|index|bot)\.(?:c|m)?js\b/i,
124
+ /\belectron\s+\./i,
125
+ ];
126
+ const looksLikeServer = (cmd) => SERVER_RES.some((re) => re.test(String(cmd || "")));
127
+
128
+ /** Antwort sieht aus wie eine Schritt-für-Schritt-Anleitung zum Selbermachen (statt erledigter Arbeit)? */
129
+ function looksLikeTutorial(text) {
130
+ const t = String(text || "");
131
+ const steps = (t.match(/^\s*(?:\d+[.)]|schritt\s*\d+|step\s*\d+)/gim) || []).length;
132
+ const shell = /```(?:bash|sh|shell|cmd|powershell|ps1|bat|console)?\s*\n\s*(?:npm|npx|pip|pip3|python|py|pyinstaller|node|yarn|pnpm|git|cargo|go|dotnet|mvn|gradle|winget|choco|cd|mkdir|electron-builder)\b/i.test(t);
133
+ const youDo = /\b(?:führe|öffne|installiere|gib|tippe|kopiere|speichere|erstelle|lade)\b[^.\n]{0,60}\b(?:aus|ein|dann|folgende|den befehl|im terminal|die datei)\b|\bdu (?:musst|kannst|solltest)\b/i.test(t);
134
+ return (shell && (steps >= 2 || youDo)) || (steps >= 3 && youDo);
135
+ }
136
+ // Gegen oberflächliche Arbeit: Hat Mythos nach seinen Änderungen wirklich geprüft?
137
+ const WEB_FILE_RE = /\.(?:html?|css|s[ac]ss|jsx?|tsx?|mjs|vue|svelte)$/i;
138
+ function verifyTracker() {
139
+ const st = { changed: false, web: false, verified: false, checked: false, nudges: 0, acted: false, tutorNudged: false };
140
+ return {
141
+ note(t, result) {
142
+ if (t.name !== "todo") st.acted = true;
143
+ const ok = !/^(?:FEHLER|BLOCKIERT|Vom Nutzer)/.test(String(result || ""));
144
+ if ((t.name === "write" || t.name === "edit") && ok) { st.changed = true; st.verified = false; if (WEB_FILE_RE.test(String(t.path || ""))) { st.web = true; st.checked = false; } }
145
+ if (t.name === "verify") st.verified = true;
146
+ if (t.name === "run" && /\b(?:build|test|tsc|lint|pytest|cargo|go\s+(?:build|test)|mvn)\b/i.test(String(t.cmd || ""))) st.verified = true;
147
+ if (t.name === "check" && /ERGEBNIS: ✓/.test(String(result || ""))) st.checked = true;
148
+ },
149
+ /** Hinweis fürs Modell, falls noch nicht geprüft – oder "". Höchstens 3-mal. */
150
+ nudge(answer, question) {
151
+ // Anleitung statt Tat: Antwort ohne ein einziges Werkzeug, die dem Nutzer Befehle/Schritte zum Selbermachen gibt
152
+ // (außer der Nutzer hat ausdrücklich nach einer Erklärung gefragt).
153
+ const wantsHowTo = /\b(?:wie (?:geht|mache|kann ich|funktioniert)|erklär|anleitung|tutorial|zeig mir wie|how to|explain)\b/i.test(String(question || ""));
154
+ if (!st.acted && !st.tutorNudged && answer && !wantsHowTo && looksLikeTutorial(answer)) {
155
+ st.tutorNudged = true;
156
+ return "Du hast eine Anleitung geschrieben, statt es selbst zu tun. Der Nutzer will das ERGEBNIS, keine Anleitung – du hast vollen Zugriff auf seinen PC. Führe die Schritte jetzt selbst mit deinen Werkzeugen aus (Dateien anlegen, Pakete installieren, bauen, z. B. die .exe erzeugen), prüfe das Ergebnis (Datei existiert, startet) und antworte dann im Antwortformat mit dem Pfad zum fertigen Ergebnis.";
157
+ }
158
+ if (st.nudges >= 3) return "";
159
+ if (st.changed && !st.verified) { st.nudges++; st.verified = true; return "Bevor du fertig bist: Du hast Dateien geändert, aber noch nicht geprüft. Führe jetzt verify aus (oder passende Build-/Test-Befehle), behebe jedes ✗ und antworte erst danach."; }
160
+ if (st.web && !st.checked) { st.nudges++; st.checked = true; return "Bevor du fertig bist: Du hast an einer Webseite gearbeitet, sie aber nicht im Browser geprüft. Starte sie (serve für statische Seiten, start für Dev-Server/Backends) und prüfe sie mit check. Behebe alles, was check als ✗ meldet (weiße Seite, Konsolenfehler, fehlende Dateien, „Cannot GET /“), prüfe erneut und antworte dann im Antwortformat mit der Adresse."; }
161
+ return "";
162
+ },
163
+ };
164
+ }
165
+
113
166
 
114
167
  const TOOL_PROMPT = [
115
168
  "Werkzeuge – antworte pro Schritt mit GENAU EINEM Block:",
116
- '<tool>{"name":"run","cmd":"..."}</tool> Shell-Befehl ausführen (Ausgabe läuft live mit)',
169
+ '<tool>{"name":"run","cmd":"..."}</tool> Shell-Befehl ausführen und warten, bis er ENDET (Ausgabe läuft live mit). Nicht für Server!',
170
+ '<tool>{"name":"start","cmd":"npm run dev"}</tool> Lang laufenden Prozess (Dev-Server, node server.js, Watcher, Bot) im HINTERGRUND starten. Wartet, bis die Adresse (z. B. http://localhost:5173) in der Ausgabe steht, und kehrt dann zurück – der Prozess läuft weiter.',
171
+ '<tool>{"name":"logs","id":"p1"}</tool> Ausgabe eines Hintergrundprozesses lesen (ohne id: alle laufenden auflisten)',
172
+ '<tool>{"name":"stop","id":"p1"}</tool> Hintergrundprozess beenden ("all" = alle)',
173
+ '<tool>{"name":"serve","path":"."}</tool> Eingebauter Webserver für statische Seiten (Ordner mit index.html, z. B. "." oder "dist"). Liefert die Adresse. Kein npx/http-server/Express nötig.',
174
+ '<tool>{"name":"check","url":"http://localhost:5173"}</tool> Webseite WIRKLICH prüfen: HTTP-Status, lädt jedes Skript/CSS, Browser-Test mit Konsolenfehlern, erkennt weiße Seiten und „Cannot GET /“, macht (in der App) einen Screenshot. url darf auch ein Pfad sein ("index.html").',
175
+ '<tool>{"name":"verify"}</tool> Projekt automatisch prüfen: installiert fehlende Pakete, führt Typecheck, Lint, Build und Tests aus (was es im Projekt gibt) und meldet ✓/✗ je Prüfung mit Fehlerausgabe.',
117
176
  '<tool>{"name":"read","path":"..."}</tool> Datei lesen',
118
177
  '<tool>{"name":"edit","path":"...","old":"exakter alter Text","new":"neuer Text"}</tool> Datei gezielt ändern (bevorzugt! "old" muss genau einmal vorkommen, sonst "all":true)',
119
178
  '<tool>{"name":"write","path":"...","content":"..."}</tool> Neue Datei anlegen oder komplett neu schreiben',
@@ -125,7 +184,38 @@ const TOOL_PROMPT = [
125
184
  '<tool>{"name":"todo","items":[{"text":"Schritt","done":false}]}</tool> Aufgabenliste anlegen/aktualisieren (bei größeren Aufgaben zuerst planen, dann Punkte abhaken; immer die komplette Liste senden)',
126
185
  "Es gibt NUR diese Werkzeuge (plus die unten genannten MCP-/Browser-Werkzeuge, falls welche aufgeführt sind). Erfinde keine Werkzeuge, Connectoren oder MCP-Server. Fehlt etwas (z. B. ein nicht verbundener Connector), sag das dem Nutzer kurz und mach mit dem Rest weiter. Schließe jeden Block mit </tool>.",
127
186
  "Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
128
- "Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool> (Markdown erlaubt).",
187
+ "",
188
+ "SO ARBEITEST DU (gründlich wie ein Senior-Entwickler):",
189
+ "1. Verstehen: Projekt erkunden (ls, package.json, relevante Dateien lesen), bevor du etwas änderst.",
190
+ "2. Planen: bei mehr als zwei Schritten zuerst eine todo-Liste.",
191
+ "3. Bauen: vollständig und lauffähig – keine Platzhalter, kein „TODO“, keine „…“ im Code, keine erfundenen Pakete oder Importe. Neue Pakete wirklich installieren. Jede Datei, auf die du verweist, auch anlegen.",
192
+ "4. Prüfen (PFLICHT, bevor du „fertig“ sagst): verify ausführen und jedes ✗ beheben. Bei Webseiten/Web-Apps zusätzlich starten (serve für statische Seiten, start für Dev-Server/Backends) und mit check prüfen; Fehler beheben und erneut prüfen, bis check „ERGEBNIS: ✓“ meldet. Behaupte nie, dass etwas funktioniert, das du nicht geprüft hast.",
193
+ "5. Erst dann die Abschlussantwort.",
194
+ "",
195
+ "ADRESSEN: Läuft etwas (Server, Webseite, Vorschau), nenne die Adresse in der Abschlussantwort ganz oben als klickbaren Link, z. B. **[http://localhost:3000](http://localhost:3000)**.",
196
+ "ÖFFNEN/STARTEN ≠ BAUEN: „öffne/starte/mach auf/führ aus <Programm/Spiel/Datei>“ heißt: das VORHANDENE Programm auf dem PC finden und starten – NIEMALS Code schreiben, ändern oder ein Projekt umbauen. " +
197
+ "Suche (Windows, mit Teilen des Namens, z. B. launcher oder mythos) nacheinander: 1) Startmenü-Apps per PowerShell: Get-StartApps | Where-Object Name -like '*launcher*' – starten mit Start-Process ('shell:AppsFolder\\' + AppID). " +
198
+ "2) Verknüpfungen (.lnk) auf dem Desktop und im Startmenü (%APPDATA% und %PROGRAMDATA% \\Microsoft\\Windows\\Start Menu) per dir /s /b. 3) .exe-Dateien in %LOCALAPPDATA%\\Programs und %PROGRAMFILES% per where /r. " +
199
+ "Gefunden → mit start \"\" \"<voller Pfad>\" starten und nur kurz melden („✓ Mythos Launcher gestartet“). Nichts gefunden → kurz sagen, wo du gesucht hast, und nach dem Ort fragen. Gebaut oder geändert wird nur, wenn der Nutzer es ausdrücklich will.",
200
+ "",
201
+ "HANDELN STATT ERKLÄREN: Du hast vollen Zugriff auf den PC des Nutzers. Wenn er etwas haben will (eine .exe, ein Programm, eine Webseite, ein Skript, eine Datei, ein Build), dann BAUST du es selbst mit deinen Werkzeugen und lieferst das fertige Ergebnis mit Pfad – schreibe NIE nur eine Anleitung, Befehle zum Abtippen oder „so geht's“. Fehlt ein Programm (z. B. pyinstaller, electron-builder, pkg, dotnet), installiere es selbst. Anleitungen nur, wenn der Nutzer ausdrücklich danach fragt.",
202
+ "Beispiele: „mach eine exe“ → Python: run \"pip install pyinstaller\", dann run \"pyinstaller --onefile --noconfirm main.py\" → dist\\main.exe; Node-Skript: run \"npx --yes @yao-pkg/pkg . --targets node18-win-x64 --output dist/app.exe\"; Electron-App: run \"npx --yes electron-builder --win portable\"; C#: run \"dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true\". Danach mit ls/run prüfen, dass die .exe existiert (und wenn möglich startet), und den vollen Pfad nennen.",
203
+ "",
204
+ "SERVER: Dev-Server, „npm start“, „node server.js“, Watcher u. Ä. NIE mit run starten – run wartet, bis der Befehl endet, und ein Server endet nie. Immer start (oder serve) nutzen. Der Server läuft nach deiner Antwort weiter, damit der Nutzer die Seite öffnen kann.",
205
+ "",
206
+ "WEBSEITEN RICHTIG AUFSETZEN (gegen weiße Seiten und „Cannot GET /“):",
207
+ "- Einfache Webseite/Spiel ohne Framework: index.html direkt im Projektordner, CSS/JS daneben mit relativen Pfaden (./style.css, ./script.js), normales <script src> ohne import von npm-Paketen. Starten mit serve. Kein Express, kein Build nötig.",
208
+ "- React/Vue/Svelte/TypeScript: als Vite-Projekt (package.json mit vite, index.html im Projektroot mit <script type=\"module\" src=\"/src/main.tsx\">, <div id=\"root\">), dann run \"npm install\", dann start \"npm run dev\". Eine index.html, die .tsx/.ts/.jsx lädt, NIE direkt öffnen oder mit serve ausliefern – das ergibt eine weiße Seite. Fertiger Build: run \"npm run build\", dann serve \"dist\".",
209
+ "- Eigener Node-Server (Express o. Ä.): MUSS „/“ beantworten – app.use(express.static(path.join(__dirname, \"public\"))) mit public/index.html, oder app.get(\"/\", …). Port = process.env.PORT || 3000, beim Start die volle Adresse ausgeben: console.log(\"Server läuft: http://localhost:\" + port).",
210
+ "- Weiße Seite heißt fast immer: JavaScript-Fehler, eine Datei lädt nicht (404) oder falscher Pfad. check zeigt dir Konsolenfehler und fehlende Dateien – behebe die Ursache.",
211
+ "",
212
+ "ANTWORTEN: Vor einem Werkzeug höchstens ein kurzer Satz. Die Abschlussantwort (ohne Werkzeug) knapp und strukturiert in Markdown:",
213
+ "### ✅ Ergebnis – ein bis zwei Sätze, was jetzt geht",
214
+ "**Geändert** – Liste: `datei` – was",
215
+ "**Geprüft** – Liste mit ✓/✗: was du ausgeführt bzw. angesehen hast",
216
+ "**Ansehen/Starten** – Adresse (läuft schon) oder Befehl, falls relevant",
217
+ "**Offen** – nur, wenn etwas fehlt oder der Nutzer etwas tun muss",
218
+ "Keine langen Fließtexte, keinen Code wiederholen. Bei reinen Fragen ohne Codeänderung einfach direkt und gegliedert antworten.",
129
219
  ].join("\n");
130
220
 
131
221
  // Modelle, die v1-messages versteht (siehe mapModel dort).
@@ -174,7 +264,7 @@ function streamActivity(full) {
174
264
  return "🔧 bereitet " + (name || "Werkzeug") + " vor…";
175
265
  }
176
266
  const GOAL_NUDGE = "Das Ziel ist noch nicht als erreicht gemeldet. Der Auftraggeber ist nicht da – gib bei Problemen nicht auf, sondern versuche einen anderen Weg, nutze websearch/fetch für Lösungen und arbeite selbstständig weiter, ohne Rückfragen. Wenn es wirklich vollständig erledigt und geprüft ist, schreibe ZIEL ERREICHT.";
177
- const SUMMARY_PROMPT = "Fasse jetzt kurz auf Deutsch zusammen, OHNE Werkzeuge:\n**Geändert:** welche Dateien und was\n**Geklappt:** was funktioniert (und wie geprüft)\n**Offen:** was noch fehlt oder beachtet werden muss\nMaximal 12 Zeilen.";
267
+ const SUMMARY_PROMPT = "Fasse jetzt kurz auf Deutsch zusammen, OHNE Werkzeuge, genau in diesem Format:\n### ✅ Ergebnis\nein bis zwei Sätze\n**Geändert** – `datei` – was\n**Geprüft** – ✓/✗ je Prüfung (verify, check, Tests)\n**Ansehen/Starten** – Adresse oder Befehl (falls relevant)\n**Offen** – was noch fehlt (nur falls etwas fehlt)\nMaximal 15 Zeilen.";
178
268
 
179
269
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
180
270
 
@@ -512,6 +602,427 @@ async function pushNotify(url, title, message) {
512
602
  } catch { return false; }
513
603
  }
514
604
 
605
+ // ---------- Prüfen, Hintergrund-Prozesse, eingebauter Webserver ----------
606
+ // Braucht zusätzlich `spawn`, `exec` (child_process) und `http` im umgebenden Code.
607
+
608
+ const stripAnsi = (s) => String(s || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
609
+ const tailText = (s, n) => { s = stripAnsi(s).trim(); return s.length > n ? "…" + s.slice(-n) : s; };
610
+ function killProc(p) {
611
+ if (!p || p.exitCode != null) return;
612
+ try {
613
+ if (process.platform === "win32") exec("taskkill /pid " + p.pid + " /T /F");
614
+ else { try { process.kill(-p.pid, "SIGTERM"); } catch { p.kill("SIGTERM"); } }
615
+ } catch { /* schon beendet */ }
616
+ }
617
+
618
+
619
+
620
+ /** Lokale Adresse aus Server-Ausgabe lesen („Local: http://localhost:5173/“, „listening on port 3000“ …). */
621
+ function findLocalUrl(out) {
622
+ const s = stripAnsi(out);
623
+ const m = s.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\])(?::\d+)?(?:\/[^\s"'<>)\]]*)?/i);
624
+ if (m) return m[0].replace(/0\.0\.0\.0|\[::1?\]/, "localhost").replace(/[.,;:]+$/, "");
625
+ const p = s.match(/(?:listening|running|started|läuft|serving|server|bound)[^\n]{0,40}?\bport\s*:?\s*(\d{2,5})\b/i) || s.match(/\bport\s*:?\s*(\d{4,5})\b[^\n]{0,30}(?:listening|running|ready|läuft)/i);
626
+ return p ? "http://localhost:" + p[1] + "/" : "";
627
+ }
628
+
629
+ const BG = new Map(); let bgSeq = 0;
630
+ /** Langlaufenden Befehl im Hintergrund starten. Wartet, bis eine Adresse erscheint, der Prozess endet oder nichts mehr passiert. */
631
+ function bgStart(cmd, cwd, opts) {
632
+ opts = opts || {};
633
+ // Derselbe Befehl im selben Ordner läuft schon → erst beenden (sonst „Port belegt“).
634
+ for (const j of BG.values()) if (j.cmd === cmd && j.cwd === cwd && j.code == null) { killProc(j.p); BG.delete(j.id); }
635
+ return new Promise((resolve) => {
636
+ const id = "p" + (++bgSeq);
637
+ let p;
638
+ try {
639
+ p = spawn(cmd, { cwd: cwd || undefined, shell: true, windowsHide: true, detached: process.platform !== "win32",
640
+ env: { ...process.env, BROWSER: "none", FORCE_COLOR: "0", NO_COLOR: "1", ...(opts.env || {}) } });
641
+ } catch (e) { resolve({ result: "FEHLER: " + e.message }); return; }
642
+ const job = { id, cmd, cwd, p, out: "", url: "", code: null, t0: Date.now(), last: Date.now() };
643
+ BG.set(id, job);
644
+ let done = false;
645
+ const max = Math.min(300, Math.max(5, +opts.wait || 90)) * 1000;
646
+ const finish = () => {
647
+ if (done) return; done = true; clearInterval(timer);
648
+ const out = tailText(job.out, 3000);
649
+ if (job.code != null) {
650
+ BG.delete(id);
651
+ const busy = /EADDRINUSE|address already in use|port \d+ is (?:already )?in use/i.test(job.out);
652
+ resolve({ result: (job.code === 0 ? "Befehl ist fertig (Exit 0) – er lief nicht dauerhaft weiter.\n" : "FEHLER: Prozess beendet mit Exit " + job.code + ".\n") +
653
+ (busy ? "Der Port ist belegt – stoppe den alten Prozess (logs/stop) oder nimm einen anderen Port.\n" : "") + (out || "(keine Ausgabe)"), code: job.code });
654
+ return;
655
+ }
656
+ resolve({ id, url: job.url, result: (job.url
657
+ ? "✓ Läuft im Hintergrund (id " + id + "): " + job.url + "\nAls Nächstes: Seite mit check prüfen."
658
+ : "Läuft im Hintergrund (id " + id + "), noch keine Adresse in der Ausgabe erkannt. Mit logs nachsehen.") +
659
+ "\nAusgabe ansehen: {\"name\":\"logs\",\"id\":\"" + id + "\"} · beenden: {\"name\":\"stop\",\"id\":\"" + id + "\"}\n\nAusgabe bisher:\n" + (out || "(keine)") });
660
+ };
661
+ const on = (dec) => (d) => {
662
+ const s = dec(d); job.out += s; job.last = Date.now();
663
+ if (job.out.length > 200000) job.out = job.out.slice(-100000);
664
+ if (!done && opts.onChunk) opts.onChunk(s);
665
+ if (!job.url) { const u = findLocalUrl(job.out); if (u) { job.url = u; setTimeout(finish, 1200); } }
666
+ };
667
+ p.stdout.on("data", on(outputDecoder())); p.stderr.on("data", on(outputDecoder()));
668
+ p.on("error", (e) => { job.out += "\nFEHLER: " + e.message; });
669
+ p.on("close", (code) => { job.code = code == null ? 1 : code; setTimeout(finish, 100); });
670
+ const timer = setInterval(() => {
671
+ const el = Date.now() - job.t0;
672
+ if (el > max || (el > 25000 && Date.now() - job.last > 25000)) finish();
673
+ }, 500);
674
+ });
675
+ }
676
+ function bgLogs(id) {
677
+ if (!id) {
678
+ const list = [...BG.values()].filter((j) => j.code == null);
679
+ return list.length ? "Laufende Hintergrundprozesse:\n" + list.map((j) => "- " + j.id + ": " + j.cmd + (j.url ? " → " + j.url : "") + " (seit " + Math.round((Date.now() - j.t0) / 1000) + " s)").join("\n") : "Keine Hintergrundprozesse.";
680
+ }
681
+ const j = BG.get(String(id));
682
+ if (!j) return "FEHLER: Kein Hintergrundprozess mit id " + id + ". " + bgLogs("");
683
+ return (j.code == null ? "läuft" : "beendet (Exit " + j.code + ")") + " · " + j.cmd + (j.url ? " · " + j.url : "") + "\n" + (tailText(j.out, 6000) || "(keine Ausgabe)");
684
+ }
685
+ function bgStop(id) {
686
+ const list = id === "all" || !id ? [...BG.values()] : [BG.get(String(id))].filter(Boolean);
687
+ if (!list.length) return "FEHLER: Kein Hintergrundprozess mit id " + id + ".";
688
+ list.forEach((j) => { killProc(j.p); BG.delete(j.id); });
689
+ return "Beendet: " + list.map((j) => j.id + " (" + j.cmd + ")").join(", ");
690
+ }
691
+ function stopAllBackground() {
692
+ for (const j of BG.values()) killProc(j.p);
693
+ BG.clear();
694
+ for (const s of SERVERS.values()) try { s.server.close(); } catch { /* egal */ }
695
+ SERVERS.clear();
696
+ }
697
+ process.on("exit", stopAllBackground);
698
+
699
+ // ---------- Eingebauter Webserver (für statische Seiten) ----------
700
+ const MIME = { html: "text/html; charset=utf-8", htm: "text/html; charset=utf-8", css: "text/css; charset=utf-8", js: "text/javascript; charset=utf-8", mjs: "text/javascript; charset=utf-8",
701
+ json: "application/json; charset=utf-8", map: "application/json", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", avif: "image/avif",
702
+ ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4", webm: "video/webm",
703
+ txt: "text/plain; charset=utf-8", xml: "application/xml", pdf: "application/pdf", wasm: "application/wasm", webmanifest: "application/manifest+json" };
704
+ const SERVERS = new Map();
705
+ /** Braucht diese index.html einen Build-Schritt (TS/JSX/Vite)? */
706
+ const needsBundler = (html) => /<script[^>]+src=["'][^"']*\.(?:tsx?|jsx|vue|svelte)["']/i.test(html);
707
+ function listenFree(server, port) {
708
+ return new Promise((res, rej) => {
709
+ const tryPort = (p) => {
710
+ server.once("error", (e) => (e.code === "EADDRINUSE" && p < port + 30 ? tryPort(p + 1) : rej(e)));
711
+ server.listen(p, "127.0.0.1", () => res(server.address().port));
712
+ };
713
+ tryPort(port);
714
+ });
715
+ }
716
+ async function serveDir(root, rel) {
717
+ let target = path.resolve(root, rel || ".");
718
+ if (!fs.existsSync(target)) return { result: "FEHLER: Nicht gefunden: " + (rel || ".") };
719
+ let page = "";
720
+ if (fs.statSync(target).isFile()) { page = path.basename(target); target = path.dirname(target); }
721
+ if (!page && !fs.existsSync(path.join(target, "index.html"))) {
722
+ const sub = ["dist", "build", "public", "out", "www", "site", "docs"].find((d) => fs.existsSync(path.join(target, d, "index.html")));
723
+ const pkg = readText(path.join(target, "package.json")) || "";
724
+ if (!sub) return { result: "FEHLER: In „" + (rel || ".") + "“ gibt es keine index.html – darum zeigt ein Server dort „Cannot GET /“. Lege eine index.html an" + (pkg ? " oder starte das Projekt mit start (z. B. npm run dev)" : "") + "." };
725
+ if (/"(?:vite|react-scripts|next|@vue\/cli-service|parcel)"/.test(pkg) && sub === "public") return { result: "FEHLER: Das ist ein Projekt mit Build-Schritt. Starte es mit start \"npm run dev\" oder baue es (npm run build) und nutze dann serve \"dist\"." };
726
+ target = path.join(target, sub);
727
+ }
728
+ const indexHtml = readText(path.join(target, page || "index.html")) || "";
729
+ if (needsBundler(indexHtml)) return { result: "FEHLER: " + (page || "index.html") + " lädt TypeScript/JSX direkt (z. B. /src/main.tsx). Das kann ein Browser nicht ausführen → weiße Seite. Nutze start \"npm run dev\" (Vite) oder npm run build und dann serve \"dist\"." };
730
+ const dir = target;
731
+ let srv = SERVERS.get(dir);
732
+ if (!srv) {
733
+ const server = http.createServer((req, res) => {
734
+ let p; try { p = decodeURIComponent(new URL(req.url, "http://x").pathname); } catch { p = "/"; }
735
+ let f = path.join(dir, p);
736
+ if (path.relative(dir, f).startsWith("..")) { res.writeHead(403); res.end("Verboten"); return; }
737
+ try { if (fs.statSync(f).isDirectory()) f = path.join(f, "index.html"); } catch { /* gibt es nicht */ }
738
+ // Einzelseiten-Apps: unbekannte Pfade ohne Dateiendung → index.html.
739
+ if (!fs.existsSync(f) && !path.extname(p) && fs.existsSync(path.join(dir, "index.html"))) f = path.join(dir, "index.html");
740
+ fs.readFile(f, (err, data) => {
741
+ if (err) { res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); res.end("404 – Datei nicht gefunden: " + p); return; }
742
+ res.writeHead(200, { "content-type": MIME[path.extname(f).slice(1).toLowerCase()] || "application/octet-stream", "cache-control": "no-store" });
743
+ res.end(data);
744
+ });
745
+ });
746
+ let port;
747
+ try { port = await listenFree(server, 5500); } catch (e) { return { result: "FEHLER: Webserver konnte nicht starten: " + e.message }; }
748
+ srv = { server, url: "http://127.0.0.1:" + port + "/" };
749
+ SERVERS.set(dir, srv);
750
+ }
751
+ const url = srv.url + (page && page !== "index.html" ? encodeURI(page) : "");
752
+ return { url, result: "✓ Webserver läuft: " + url + " (Ordner: " + (path.relative(root, dir) || ".") + ", Dateien werden bei jedem Aufruf frisch gelesen)\nAls Nächstes: Seite mit check prüfen." };
753
+ }
754
+
755
+ // ---------- Seite prüfen (HTTP + alle Skripte/CSS) ----------
756
+ async function getLocal(url, ms, method, accept) {
757
+ const tries = [url];
758
+ if (/\/\/localhost\b/i.test(url)) tries.push(url.replace(/\/\/localhost\b/i, "//127.0.0.1"), url.replace(/\/\/localhost\b/i, "//[::1]"));
759
+ let last;
760
+ for (const u of tries) {
761
+ const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), ms || 15000);
762
+ try {
763
+ const r = await fetch(u, { method: method || "GET", redirect: "follow", signal: ctl.signal, headers: { "user-agent": WEB_UA, accept: accept || "text/html,*/*" } });
764
+ const text = method === "HEAD" ? "" : Buffer.from(await r.arrayBuffer()).subarray(0, 3e6).toString("utf8");
765
+ return { ok: r.ok, status: r.status, type: r.headers.get("content-type") || "", text, url: r.url || u };
766
+ } catch (e) { last = e; } finally { clearTimeout(to); }
767
+ }
768
+ throw last;
769
+ }
770
+ /** HTTP-Prüfung einer Seite. → { lines, fail, warn, url, html, reachable } */
771
+ async function httpPageCheck(url) {
772
+ const lines = []; let fail = 0, warn = 0;
773
+ let r;
774
+ try { r = await getLocal(url, 20000); }
775
+ catch (e) {
776
+ const why = e.name === "AbortError" ? "Zeitüberschreitung" : (e.cause && e.cause.code) || e.message;
777
+ return { reachable: false, fail: 1, warn: 0, url, html: "", lines: ["✗ Nicht erreichbar (" + why + "). Läuft der Server? Mit start/serve starten und die Adresse aus der Ausgabe nehmen (logs zeigt sie)."] };
778
+ }
779
+ const isHtml = /html/i.test(r.type) || /^\s*<(?:!doctype|html)/i.test(r.text);
780
+ lines.push((r.ok ? "✓" : "✗") + " HTTP " + r.status + " · " + (r.type.split(";")[0] || "?") + " · " + (r.text.length / 1024).toFixed(1).replace(".", ",") + " KB" + (r.url.replace(/\/$/, "") !== url.replace(/\/$/, "") ? " · nach Weiterleitung: " + r.url : ""));
781
+ if (!r.ok) {
782
+ fail++;
783
+ if (/Cannot (?:GET|POST) \//i.test(r.text)) lines.push("✗ „" + (r.text.match(/Cannot \w+ \/[^\s<]*/) || ["Cannot GET /"])[0] + "“: Der Server (meist Express) hat für diese Adresse keine Route und liefert keine index.html aus. Lösung: app.use(express.static(path.join(__dirname, \"public\"))) mit public/index.html, oder app.get(\"/\", …) – für eine reine Webseite ohne Backend einfach das serve-Werkzeug nutzen.");
784
+ else lines.push("Antwort: " + htmlToText(r.text).slice(0, 400));
785
+ return { reachable: true, status: r.status, fail, warn, url: r.url, html: r.text, lines };
786
+ }
787
+ if (!isHtml) return { reachable: true, status: r.status, fail, warn, url: r.url, html: "", lines: lines.concat(["ℹ Keine HTML-Seite – Inhalt: " + r.text.slice(0, 300)]) };
788
+ const html = r.text;
789
+ const title = (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1];
790
+ if (title) lines.push("Titel: „" + decodeEntities(title.trim()).slice(0, 80) + "“");
791
+ if (/file:\/\/|(?:src|href)=["'][a-z]:\\/i.test(html)) { fail++; lines.push("✗ Die Seite verweist auf lokale Dateipfade (file:// oder C:\\…) – im Browser über http lädt das nicht. Relative Pfade nutzen (./style.css)."); }
792
+ if (needsBundler(html) && !/@vite\/client|\/@react-refresh/.test(html)) { fail++; lines.push("✗ index.html lädt TypeScript/JSX direkt (.tsx/.ts/.jsx) ohne Dev-Server → weiße Seite. Mit start \"npm run dev\" (Vite) starten oder bauen und dist ausliefern."); }
793
+ // Alle eingebundenen Skripte und Stylesheets laden.
794
+ const refs = [];
795
+ html.replace(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi, (_m, s) => refs.push(["Skript", s]));
796
+ html.replace(/<link\b[^>]*>/gi, (tag) => { if (/rel=["'](?:stylesheet|modulepreload)["']/i.test(tag)) { const h = tag.match(/href=["']([^"']+)["']/i); if (h) refs.push([/stylesheet/i.test(tag) ? "CSS" : "Modul", h[1]]); } });
797
+ const base = new URL(r.url);
798
+ let okRefs = 0;
799
+ for (const [kind, ref] of refs.slice(0, 30)) {
800
+ let u; try { u = new URL(ref, base); } catch { continue; }
801
+ if (u.host !== base.host) continue;
802
+ try {
803
+ const a = await getLocal(u.href, 15000, "GET", kind === "CSS" ? "text/css,*/*;q=0.1" : "*/*");
804
+ const isJs = kind !== "CSS";
805
+ if (!a.ok) { fail++; lines.push("✗ " + kind + " " + u.pathname + " → HTTP " + a.status + " (Datei fehlt oder Pfad falsch)"); }
806
+ else if (/html/i.test(a.type) && !/html/i.test(u.pathname)) { fail++; lines.push("✗ " + kind + " " + u.pathname + " → liefert HTML statt " + (isJs ? "JavaScript" : "CSS") + " (Datei fehlt, Server schickt index.html)"); }
807
+ else okRefs++;
808
+ } catch (e) { fail++; lines.push("✗ " + kind + " " + u.pathname + " → " + e.message); }
809
+ }
810
+ if (refs.length) lines.push((okRefs === refs.length ? "✓ " : "") + okRefs + "/" + refs.length + " eingebundene Skripte/CSS laden");
811
+ const bodyText = htmlToText((html.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, html])[1]);
812
+ if (bodyText.length < 3) {
813
+ const hasScripts = /<script\b/i.test(html);
814
+ if (!hasScripts) { fail++; lines.push("✗ Die Seite ist leer: kein sichtbarer Text und keine Skripte."); }
815
+ else { warn++; lines.push("ℹ Der Inhalt entsteht erst per JavaScript (leeres <body>/#root) – ob er wirklich erscheint, zeigt nur der Browser-Test."); }
816
+ } else lines.push("✓ Sichtbarer Text im HTML: " + bodyText.length + " Zeichen – „" + bodyText.replace(/\s+/g, " ").slice(0, 120) + "“");
817
+ return { reachable: true, status: r.status, fail, warn, url: r.url, html, lines };
818
+ }
819
+
820
+ /** Projekt automatisch prüfen: fehlende Pakete installieren, Typecheck, Lint, Build, Tests. */
821
+ function runCapture(cmd, cwd, opts) {
822
+ opts = opts || {};
823
+ return new Promise((res) => {
824
+ let out = "";
825
+ const p = spawn(cmd, { cwd, shell: true, windowsHide: true, env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", BROWSER: "none", ...(opts.env || {}) } });
826
+ if (opts.track) opts.track(p);
827
+ const on = (dec) => (d) => { const s = dec(d); out += s; if (out.length > 200000) out = out.slice(-100000); if (opts.onChunk) opts.onChunk(s); };
828
+ p.stdout.on("data", on(outputDecoder())); p.stderr.on("data", on(outputDecoder()));
829
+ const to = setTimeout(() => { out += "\n[Zeitüberschreitung – abgebrochen]"; killProc(p); }, opts.timeout || 600000);
830
+ p.on("error", (e) => { out += "\nFEHLER: " + e.message; });
831
+ p.on("close", (code) => { clearTimeout(to); res({ code: code == null ? 1 : code, out }); });
832
+ });
833
+ }
834
+ function detectChecks(root) {
835
+ const has = (f) => fs.existsSync(path.join(root, f));
836
+ const checks = [];
837
+ let pkg = null; try { pkg = JSON.parse(readText(path.join(root, "package.json")) || "null"); } catch { pkg = null; }
838
+ if (pkg) {
839
+ const pm = has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("bun.lockb") || has("bun.lock") ? "bun" : "npm";
840
+ const run = (s) => (pm === "npm" ? "npm run " + s : pm + " run " + s);
841
+ const sc = pkg.scripts || {};
842
+ const deps = Object.keys(Object.assign({}, pkg.dependencies, pkg.devDependencies));
843
+ if (deps.length && !has("node_modules")) checks.push({ label: "Pakete installieren", cmd: pm + " install", fatal: true });
844
+ const tc = ["typecheck", "type-check", "tsc", "check-types"].find((s) => sc[s]);
845
+ if (tc) checks.push({ label: "Typecheck", cmd: run(tc) });
846
+ else if (has("tsconfig.json") && !/\btsc\b/.test(sc.build || "") && (deps.includes("typescript") || has("node_modules/typescript"))) {
847
+ const ts = readText(path.join(root, "tsconfig.json")) || "";
848
+ checks.push({ label: "Typecheck", cmd: /"references"\s*:/.test(ts) ? "npx tsc -b" : "npx tsc --noEmit" });
849
+ }
850
+ if (sc.lint) checks.push({ label: "Lint", cmd: run("lint"), soft: true });
851
+ if (sc.build) checks.push({ label: "Build", cmd: run("build") });
852
+ if (sc.test && !/no test specified/i.test(sc.test)) checks.push({ label: "Tests", cmd: pm === "npm" ? "npm test" : pm + " test", env: { CI: "1" } });
853
+ if (!sc.build && !sc.test) {
854
+ const main = [pkg.main, "index.js", "server.js", "app.js"].find((f) => f && has(f) && /\.c?js$/.test(f));
855
+ if (main) checks.push({ label: "Syntax " + main, cmd: "node --check " + JSON.stringify(main) });
856
+ }
857
+ }
858
+ const py = has("requirements.txt") || has("pyproject.toml") || has("setup.py") || (!pkg && fs.readdirSync(root).some((f) => /\.py$/.test(f)));
859
+ if (py) {
860
+ const pyc = process.platform === "win32" ? "python" : "python3";
861
+ checks.push({ label: "Python-Syntax", cmd: pyc + " -m compileall -q -x \"(node_modules|venv|\\.venv|__pycache__)\" ." });
862
+ if (has("pytest.ini") || has("conftest.py") || has("tests")) checks.push({ label: "Tests", cmd: pyc + " -m pytest -q" });
863
+ }
864
+ if (has("Cargo.toml")) checks.push({ label: "Build", cmd: "cargo build" }, { label: "Tests", cmd: "cargo test" });
865
+ if (has("go.mod")) checks.push({ label: "Build", cmd: "go build ./..." }, { label: "Tests", cmd: "go test ./..." });
866
+ if (has("pom.xml")) checks.push({ label: "Build", cmd: "mvn -q -DskipTests package" });
867
+ return { checks, pkg };
868
+ }
869
+ /** Statische Seiten: verlinkte lokale Dateien müssen existieren. */
870
+ function staticRefCheck(root) {
871
+ const pages = [];
872
+ const walk = (dir, depth) => {
873
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
874
+ for (const e of entries) {
875
+ const full = path.join(dir, e.name);
876
+ if (e.isDirectory()) { if (depth < 3 && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(full, depth + 1); }
877
+ else if (/\.html?$/i.test(e.name) && pages.length < 40) pages.push(full);
878
+ }
879
+ };
880
+ walk(root, 0);
881
+ const missing = [];
882
+ for (const f of pages) {
883
+ const html = readText(f) || "";
884
+ html.replace(/<(?:script|img|source|link|iframe)\b[^>]*\b(?:src|href)=["']([^"'#?]+)/gi, (_m, ref) => {
885
+ if (/^(?:[a-z]+:|\/\/|data:|\$|\{)/i.test(ref)) return;
886
+ const tgt = ref.startsWith("/") ? path.join(root, ref) : path.join(path.dirname(f), ref);
887
+ if (!fs.existsSync(tgt) && !/\/src\/main\.\w+$/.test(ref)) missing.push(path.relative(root, f).split(path.sep).join("/") + " → " + ref);
888
+ });
889
+ }
890
+ return { pages: pages.length, missing };
891
+ }
892
+ async function verifyProject(root, opts) {
893
+ opts = opts || {};
894
+ const { checks, pkg } = detectChecks(root);
895
+ const lines = []; let fail = 0;
896
+ for (const c of checks) {
897
+ if (opts.onChunk) opts.onChunk("\n▶ " + c.label + ": " + c.cmd + "\n");
898
+ const r = await runCapture(c.cmd, root, { onChunk: opts.onChunk, track: opts.track, env: c.env, timeout: 900000 });
899
+ if (r.code === 0) lines.push("✓ " + c.label + " (" + c.cmd + ")");
900
+ else {
901
+ if (!c.soft) fail++;
902
+ lines.push((c.soft ? "⚠ " : "✗ ") + c.label + " (" + c.cmd + ") – Exit " + r.code + (c.soft ? " (Lint-Hinweise; beheben, wenn sie deine Änderungen betreffen)" : "") + ":\n" + tailText(r.out, c.soft ? 1500 : 3500).split("\n").map((l) => " " + l).join("\n"));
903
+ if (c.fatal) break;
904
+ }
905
+ }
906
+ const st = staticRefCheck(root);
907
+ if (st.missing.length) { fail++; lines.push("✗ Fehlende Dateien in HTML-Seiten:\n" + st.missing.slice(0, 20).map((m) => " " + m).join("\n")); }
908
+ else if (st.pages && !pkg) lines.push("✓ " + st.pages + " HTML-Seite(n): alle verlinkten lokalen Dateien vorhanden");
909
+ if (!lines.length) return { result: "Keine automatischen Prüfungen erkannt (kein package.json-Skript, keine Tests, keine HTML-Seiten). Prüfe selbst mit run (z. B. das Programm einmal ausführen)." };
910
+ const web = st.pages || (pkg && /"(?:vite|react|react-dom|vue|svelte|next|express|astro)"/.test(JSON.stringify(Object.assign({}, pkg.dependencies, pkg.devDependencies))));
911
+ return { fail, result: "Prüfbericht:\n" + lines.join("\n") + "\n\n" + (fail ? "ERGEBNIS: ✗ " + fail + " Prüfung(en) fehlgeschlagen – Ursache beheben und verify erneut ausführen." : "ERGEBNIS: ✓ alle Prüfungen bestanden.") +
912
+ (web ? "\nDas ist ein Web-Projekt: starte es (serve bzw. start) und prüfe die Seite mit check." : "") };
913
+ }
914
+
915
+ // ---------- Werkzeuge start/logs/stop/serve/verify/check (gemeinsamer Teil) ----------
916
+ const DEV_TOOLS = ["start", "logs", "stop", "serve", "verify", "check"];
917
+ const isDevTool = (t) => DEV_TOOLS.includes(t.name);
918
+ /** Ziel für check: Adresse, Datei/Ordner (→ eingebauter Webserver) oder zuletzt gestarteter Server. */
919
+ async function checkTarget(t, root) {
920
+ let url = String(t.url || t.path || "").trim();
921
+ if (!url) {
922
+ const job = [...BG.values()].reverse().find((j) => j.url && j.code == null);
923
+ const srv = [...SERVERS.values()].pop();
924
+ url = (job && job.url) || (srv && srv.url) || "";
925
+ if (!url) return { error: "FEHLER: Keine Adresse angegeben und kein laufender Server. Erst serve/start, dann check mit \"url\"." };
926
+ }
927
+ if (!/^https?:\/\//i.test(url)) {
928
+ if (/^(?:localhost|127\.0\.0\.1)(?::\d+)?/i.test(url)) url = "http://" + url;
929
+ else { const s = await serveDir(root, url.replace(/^file:\/\/\/?/i, "")); if (!s.url) return { error: s.result }; url = s.url; }
930
+ }
931
+ return { url };
932
+ }
933
+ /** Prüfbericht aus HTTP-Prüfung h und (optional) Browser-Test rv = renderVerdict(…). */
934
+ function checkReport(url, h, rv, note) {
935
+ const lines = h.lines.concat(rv ? ["", "Browser-Test (echt gerendert):"].concat(rv.lines) : note ? [note] : []);
936
+ const fail = h.fail + (rv ? rv.fail : 0);
937
+ return "Seitenprüfung " + url + "\n" + lines.join("\n") + "\n\n" + (fail
938
+ ? "ERGEBNIS: ✗ Probleme gefunden – Ursache beheben und erneut prüfen."
939
+ : rv ? "ERGEBNIS: ✓ Seite lädt ohne Fehler und zeigt Inhalt." : "ERGEBNIS: ✓ Seite antwortet, alle Dateien laden.");
940
+ }
941
+ /** Muss die Seite noch im Browser gerendert werden? (erreichbar und HTML) */
942
+ const wantsRender = (r) => !!(r && r.http && r.http.reachable && r.http.html && r.http.status < 400);
943
+ /** opts: { onChunk, track } – Live-Ausgabe und Abbrechen der Oberfläche. */
944
+ async function devTool(t, root, opts) {
945
+ opts = opts || {};
946
+ try {
947
+ if (t.name === "start") {
948
+ if (!t.cmd) return { result: 'FEHLER: "cmd" fehlt.' };
949
+ return await bgStart(String(t.cmd), root, { onChunk: opts.onChunk, wait: t.wait });
950
+ }
951
+ if (t.name === "logs") return { result: bgLogs(t.id) };
952
+ if (t.name === "stop") return { result: bgStop(t.id) };
953
+ if (t.name === "serve") return await serveDir(root, t.path);
954
+ if (t.name === "verify") return await verifyProject(root, opts);
955
+ if (t.name === "check") {
956
+ const tg = await checkTarget(t, root);
957
+ if (tg.error) return { result: tg.error };
958
+ const h = await httpPageCheck(tg.url);
959
+ return { url: tg.url, http: h, result: checkReport(tg.url, h) };
960
+ }
961
+ } catch (e) { return { result: "FEHLER: " + e.message }; }
962
+ return { result: "Unbekanntes Werkzeug: " + t.name };
963
+ }
964
+
965
+ // ---------- Browser-Test ohne Fenster (CLI): installiertes Chrome/Edge/Chromium headless ----------
966
+ function findChrome() {
967
+ const env = process.env.MYTHOS_CHROME || process.env.CHROME_PATH;
968
+ const pf = [process.env["PROGRAMFILES"], process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
969
+ const list = (env ? [env] : []).concat(process.platform === "win32"
970
+ ? pf.flatMap((b) => [b + "\\Google\\Chrome\\Application\\chrome.exe", b + "\\Microsoft\\Edge\\Application\\msedge.exe", b + "\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"])
971
+ : process.platform === "darwin"
972
+ ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Chromium.app/Contents/MacOS/Chromium"]
973
+ : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/microsoft-edge", "/snap/bin/chromium"]);
974
+ return list.find((f) => { try { return fs.statSync(f).isFile(); } catch { return false; } }) || "";
975
+ }
976
+ function chromeRun(bin, args, ms) {
977
+ return new Promise((res) => {
978
+ let out = "", err = "";
979
+ const p = spawn(bin, args, { windowsHide: true });
980
+ const to = setTimeout(() => killProc(p), ms);
981
+ p.stdout.on("data", (d) => { out += d; }); p.stderr.on("data", (d) => { err += d; if (err.length > 400000) err = err.slice(-200000); });
982
+ p.on("error", (e) => { clearTimeout(to); res({ out, err: err + e.message, code: 1 }); });
983
+ p.on("close", (code) => { clearTimeout(to); res({ out, err, code }); });
984
+ });
985
+ }
986
+ /** Seite in echtem Chrome rendern: sichtbarer Text, Konsolenfehler, Screenshot. null = kein Browser gefunden. */
987
+ async function headlessRender(url) {
988
+ const bin = findChrome();
989
+ if (!bin) return null;
990
+ const tmp = path.join(process.env.TEMP || process.env.TMPDIR || "/tmp", "mythos-check-" + process.pid);
991
+ try { fs.mkdirSync(tmp, { recursive: true }); } catch { /* egal */ }
992
+ const shot = path.join(tmp, "shot.png");
993
+ const base = ["--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", "--user-data-dir=" + path.join(tmp, "profil"),
994
+ "--enable-logging=stderr", "--v=0", "--virtual-time-budget=5000", "--window-size=1280,800"];
995
+ if (process.platform === "linux" && process.getuid && process.getuid() === 0) base.push("--no-sandbox");
996
+ const dom = await chromeRun(bin, base.concat(["--dump-dom", url]), 45000);
997
+ try { fs.rmSync(shot, { force: true }); } catch { /* egal */ }
998
+ await chromeRun(bin, base.concat(["--screenshot=" + shot, url]), 45000);
999
+ const errors = [];
1000
+ for (const m of dom.err.matchAll(/CONSOLE[:(]\d+\)?\]\s*"([^\n]*?)",\s*source:\s*(\S*)\s*\((\d+)\)/g)) {
1001
+ const msg = m[1] + (m[2] ? " (" + m[2].split("/").pop() + ":" + m[3] + ")" : "");
1002
+ if (!/favicon\.ico/.test(msg) && !errors.includes(msg) && /error|uncaught|failed|refused|not defined|cannot|unexpected|typeerror|syntaxerror|404|500/i.test(msg)) errors.push(msg);
1003
+ }
1004
+ const html = dom.out || "";
1005
+ const body = (html.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, html])[1];
1006
+ const text = htmlToText(body);
1007
+ const vis = (body.match(/<(?:img|svg|canvas|video|button|input|iframe)\b/gi) || []).length;
1008
+ let image = null;
1009
+ try { const b = fs.readFileSync(shot); if (b.length > 2000) image = { media_type: "image/png", data: b.toString("base64") }; } catch { /* kein Screenshot */ }
1010
+ return { ok: !!html, browser: path.basename(bin), title: (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || "", text: text.length, sample: text.replace(/\s+/g, " ").slice(0, 200), vis, errors, image, shot: image ? shot : "" };
1011
+ }
1012
+ /** Browser-Ergebnis in Prüfzeilen übersetzen. r = { text, vis, errors, failed?, overlay?, sample } */
1013
+ function renderVerdict(r) {
1014
+ const lines = []; let fail = 0;
1015
+ const blank = r.text < 3 && r.vis < 1;
1016
+ if (r.overlay) { fail++; lines.push("✗ Fehler-Overlay des Dev-Servers sichtbar: " + r.overlay.slice(0, 800)); }
1017
+ if (blank) { fail++; lines.push("✗ WEISSE SEITE: Im Browser erscheint kein Inhalt (kein Text, keine Bilder/Elemente)." + (r.errors.length ? " Ursache steht wahrscheinlich in den Konsolenfehlern unten." : " Prüfe, ob das Skript den Inhalt wirklich ins richtige Element (#root/#app) rendert und ob alle Dateien laden.")); }
1018
+ else lines.push("✓ Im Browser sichtbar: " + r.text + " Zeichen Text" + (r.vis ? ", " + r.vis + " Bilder/Bedienelemente" : "") + (r.sample ? " – „" + r.sample.slice(0, 140) + "“" : ""));
1019
+ const errs = [...new Set(r.errors)];
1020
+ if (errs.length) { fail++; lines.push("✗ Konsolenfehler (" + errs.length + "):\n" + errs.slice(0, 12).map((e) => " " + e.slice(0, 400)).join("\n")); }
1021
+ else lines.push("✓ Keine Konsolenfehler");
1022
+ if (r.failed && r.failed.length) { fail++; lines.push("✗ Nicht geladene Dateien:\n" + r.failed.slice(0, 12).map((e) => " " + e).join("\n")); }
1023
+ return { lines, fail };
1024
+ }
1025
+
515
1026
 
516
1027
  const API = process.env.MYTHOS_API_URL || "https://rdxaqiacoitzeowehdts.supabase.co/functions/v1/v1-messages";
517
1028
  const CFG = path.join(os.homedir(), ".mythos-code.json");
@@ -529,7 +1040,82 @@ let goal = "";
529
1040
  const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
530
1041
  TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
531
1042
 
532
- const SITE = process.env.MYTHOS_SITE_URL || "https://mythos-core-ai.lovable.app";
1043
+ const SITE = process.env.MYTHOS_SITE_URL || "https://mythos-ai-final-vers.lovable.app";
1044
+ const VERSION = "1.10.14";
1045
+ const PKG = "mythos-code";
1046
+
1047
+ // ---------- Auto-Update über npm ----------
1048
+ // Beim Start (höchstens alle 6 h) die neueste Version auf npm prüfen. Ist eine neuere da,
1049
+ // wird sie mit `npm i -g` installiert und Mythos startet direkt neu – ohne Zutun des Users.
1050
+ // Abschalten: MYTHOS_NO_UPDATE=1 oder --no-update. Manuell: `mythos update`.
1051
+ const newer = (a, b) => { const p = (v) => String(v).split(/[.+-]/).slice(0, 3).map((n) => parseInt(n, 10) || 0); const [x, y] = [p(a), p(b)]; for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i]; return false; };
1052
+ async function latestVersion() {
1053
+ const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 2500);
1054
+ try { const r = await fetch("https://registry.npmjs.org/" + PKG + "/latest", { signal: ctl.signal }); return r.ok ? (await r.json()).version : null; }
1055
+ catch { return null; } finally { clearTimeout(t); }
1056
+ }
1057
+ function installLatest(ver) {
1058
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1059
+ try { execFileSync(npm, ["install", "-g", PKG + "@" + ver, "--no-audit", "--no-fund", "--loglevel=error"], { stdio: "inherit", shell: process.platform === "win32" }); return true; }
1060
+ catch { return false; }
1061
+ }
1062
+ async function autoUpdate(force = false) {
1063
+ if (VERSION.startsWith("__") || process.env.MYTHOS_UPDATED) return;
1064
+ if (!force && (process.env.MYTHOS_NO_UPDATE || process.argv.includes("--no-update"))) return;
1065
+ const c = load();
1066
+ if (!force && c.updateCheckedAt && Date.now() - c.updateCheckedAt < 6 * 3600e3) return;
1067
+ const latest = await latestVersion();
1068
+ save(Object.assign(load(), { updateCheckedAt: Date.now() }));
1069
+ if (!latest || !newer(latest, VERSION)) { if (force) console.log(C.g + " ✓ Mythos Code ist aktuell (" + VERSION + ")" + C.x); return; }
1070
+ console.log(C.m + " ↑ Update " + VERSION + " → " + latest + " wird installiert…" + C.x);
1071
+ if (!installLatest(latest)) { console.log(C.y + " Update fehlgeschlagen. Manuell: npm i -g " + PKG + "@latest" + C.x); return; }
1072
+ console.log(C.g + " ✓ Aktualisiert auf " + latest + " – starte neu…\n" + C.x);
1073
+ rl.close();
1074
+ const child = spawn(process.platform === "win32" ? "mythos.cmd" : "mythos", process.argv.slice(2), { stdio: "inherit", shell: process.platform === "win32", env: Object.assign({}, process.env, { MYTHOS_UPDATED: "1" }) });
1075
+ child.on("exit", (code) => process.exit(code ?? 0));
1076
+ child.on("error", () => process.exit(0));
1077
+ await new Promise(() => {}); // hier nicht weiterlaufen – der neue Prozess übernimmt
1078
+ }
1079
+
1080
+ // ---------- Startbildschirm im Claude-Code-Stil: Maskottchen „Myth“ links, Infos rechts ----------
1081
+ // Farben aus dem Mythos-Logo (Magenta → Violett → Blau → Türkis), 24-Bit-Farbe wenn möglich.
1082
+ const TRUE = process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit" || process.platform === "win32" || !!process.env.WT_SESSION;
1083
+ const rgb = (r, g, b) => (TRUE ? `\x1b[38;2;${r};${g};${b}m` : "\x1b[35m");
1084
+ const GRAD = [[217, 70, 239], [168, 85, 247], [99, 102, 241], [59, 130, 246], [34, 211, 238]];
1085
+ const grad = (t) => { const x = Math.max(0, Math.min(1, t)) * (GRAD.length - 1), i = Math.min(GRAD.length - 2, Math.floor(x)), f = x - i; const [a, b] = [GRAD[i], GRAD[i + 1]]; return rgb(...a.map((v, k) => Math.round(v + (b[k] - v) * f))); };
1086
+ // Myth: kleiner Kristall-Geist mit Augen – 11 Zeichen breit, 4 Zeilen
1087
+ const MASCOT = [
1088
+ " ▄█▄ ",
1089
+ " ▄█▀█▀█▄ ",
1090
+ " ▀███████▀ ",
1091
+ " ▀▄ ▄▀ ",
1092
+ ];
1093
+ const paintMascot = (line, row) => [...line].map((ch, i) => (ch === " " ? ch : grad((i + row) / 13) + ch)).join("") + C.x;
1094
+ const modelLabel = () => { const m = (typeof MODELS !== "undefined" ? MODELS : []).find((x) => x.id === (cfg.model || "mythos-code")); return m ? m.label : "MythosCode"; };
1095
+ function banner(info) {
1096
+ const tips = ["/hilfe zeigt alle Befehle", "Dateien mit @name anhängen", "Bilder einfach ins Terminal ziehen", "/goal <ziel> – Mythos bleibt dran, bis es fertig ist", "/modell wechselt das Modell"];
1097
+ const right = [
1098
+ C.b + "Mythos Code" + C.x + C.d + " v" + VERSION + C.x,
1099
+ C.d + modelLabel() + " · " + (cfg.name ? cfg.name : "Mythoscraft") + C.x,
1100
+ C.d + info.dir + C.x,
1101
+ "",
1102
+ ];
1103
+ console.log("");
1104
+ MASCOT.forEach((m, i) => console.log(" " + paintMascot(m, i) + " " + (right[i] || "")));
1105
+ console.log("");
1106
+ console.log(" " + grad(0.15) + "✦" + C.x + " " + C.d + tips[Math.floor(Math.random() * tips.length)] + (info.extra ? " · " + info.extra : "") + C.x + "\n");
1107
+ }
1108
+ // Eingabe wie bei Claude Code: Hinweis links + Modell rechts, Linie, dann „❯“.
1109
+ // (Unter der Eingabe zeichnen geht nicht – readline löscht beim Tippen alles unterhalb.)
1110
+ async function framedAsk(goal) {
1111
+ const w = Math.max(30, (process.stdout.columns || 80) - 1);
1112
+ const leftTxt = autoYes ? "▸▸ Vollzugriff an" : "? fragt vor Befehlen · /hilfe";
1113
+ const left = autoYes ? rgb(250, 204, 21) + leftTxt + C.x : C.d + leftTxt + C.x;
1114
+ const rightTxt = "● " + modelLabel() + " · /modell";
1115
+ const gap = Math.max(1, w - leftTxt.length - rightTxt.length);
1116
+ process.stdout.write("\n" + left + " ".repeat(gap) + C.d + rightTxt + C.x + "\n" + C.d + "─".repeat(w) + C.x + "\n");
1117
+ return ask((goal ? "🎯 " : "") + C.b + "❯ " + C.x);
1118
+ }
533
1119
  const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
534
1120
  const authPost = async (body) => (await fetch(FN + "/cli-auth", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
535
1121
 
@@ -572,12 +1158,15 @@ const fmt = (ms) => { const s = Math.floor(ms / 1000); return (s >= 60 ? Math.fl
572
1158
  let T0 = 0, tick = null, activity = "";
573
1159
  const clearLine = () => process.stdout.write("\r\x1b[K");
574
1160
  // Zeigt live, was Mythos gerade tut (z. B. „✍ schreibt src/main.js · 12 KB“) – auf Terminalbreite gekürzt.
1161
+ const SPIN = ["✦", "✧", "✶", "✷", "✸", "✹", "✸", "✷", "✶", "✧"];
1162
+ let spinI = 0;
575
1163
  const tickLine = () => {
576
- const s = "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + (activity ? " · " + activity : "") + " (Strg+C = stoppen)";
577
- const w = (process.stdout.columns || 100) - 2;
1164
+ const what = activity ? activity.charAt(0).toUpperCase() + activity.slice(1) : "Arbeitet…";
1165
+ const s = what + " (" + fmt(Date.now() - T0) + " · Strg+C zum Stoppen)";
1166
+ const w = (process.stdout.columns || 100) - 4;
578
1167
  return s.length > w ? s.slice(0, w - 1) + "…" : s;
579
1168
  };
580
- const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r\x1b[K" + C.d + tickLine() + C.x), 250); };
1169
+ const startTick = () => { if (tick || !working) return; tick = setInterval(() => { spinI = (spinI + 1) % SPIN.length; process.stdout.write("\r\x1b[K" + grad((spinI % 5) / 4) + SPIN[spinI] + C.x + " " + rgb(203, 213, 225) + tickLine() + C.x); }, 120); };
581
1170
  const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
582
1171
  let adN = 0, adTier = null, adAdminDay = "";
583
1172
  async function maybeAd() {
@@ -591,9 +1180,9 @@ async function maybeAd() {
591
1180
  let ads = []; try { ads = (await (await fetch(fnBase + "/ad-review", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "list" }) })).json()).ads || []; } catch {}
592
1181
  const pool = [{ house: true, weight: 3 }].concat(ads); let r = Math.random() * pool.reduce((t, a) => t + Math.max(1, a.weight), 0); let ad = pool[0];
593
1182
  for (const a of pool) { r -= Math.max(1, a.weight); if (r <= 0) { ad = a; break; } }
594
- if (!ad.house) { console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m" + ad.product + "\x1b[0m – " + ad.ad_text + ": " + ad.link + "\n\x1b[2m Buche deine eigene Werbung: https://mythos-core-ai.lovable.app/werbung\x1b[0m\n"); return; }
1183
+ if (!ad.house) { console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m" + ad.product + "\x1b[0m – " + ad.ad_text + ": " + ad.link + "\n\x1b[2m Buche deine eigene Werbung: https://mythos-ai-final-vers.lovable.app/werbung\x1b[0m\n"); return; }
595
1184
  }
596
- if (due) console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m@at\x1b[0m – dein Feed voller Vibes: https://at-feed-vibes.lovable.app/\n\x1b[2m Buche deine eigene Werbung: https://mythos-core-ai.lovable.app/werbung\x1b[0m\n");
1185
+ if (due) console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m@at\x1b[0m – dein Feed voller Vibes: https://at-feed-vibes.lovable.app/\n\x1b[2m Buche deine eigene Werbung: https://mythos-ai-final-vers.lovable.app/werbung\x1b[0m\n");
597
1186
  } catch {}
598
1187
  }
599
1188
  const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
@@ -683,18 +1272,18 @@ function compact(history, level) {
683
1272
  }
684
1273
 
685
1274
  let tokens = { in: 0, out: 0 };
1275
+ let currentTaskId = null;
686
1276
  async function once(messages, onText, onThink) {
687
1277
  const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
688
1278
  const system = SYSTEM();
689
- tokens.in += estimateTokens(system, messages);
690
1279
  try {
691
1280
  const r = await fetch(API, {
692
1281
  method: "POST", signal: c.signal,
693
1282
  headers: { "content-type": "application/json", "x-api-key": cfg.key, "anthropic-version": "2023-06-01" },
694
- body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system, messages, stream: true }),
1283
+ body: JSON.stringify({ model: cfg.model || "mythos-code", task_id: currentTaskId, max_tokens: 8000, system, messages, stream: true }),
695
1284
  });
696
1285
  if (!r.ok) { const j = await r.json().catch(() => ({})); const e = new Error(j?.error?.message || ("HTTP " + r.status)); e.status = r.status; throw e; }
697
- const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "", think = "";
1286
+ const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "", think = "", measured = null;
698
1287
  while (true) {
699
1288
  const { done, value } = await reader.read(); if (done) break;
700
1289
  buf += dec.decode(value, { stream: true });
@@ -703,13 +1292,14 @@ async function once(messages, onText, onThink) {
703
1292
  if (!line.startsWith("data:")) continue;
704
1293
  let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
705
1294
  if (p.type === "error") { const e = new Error(p.error?.message || "Überlastet"); e.status = 529; throw e; }
706
- // Sichtbare Gedanken: kommen als eigener Block VOR der Antwort.
707
- if (p.delta?.type === "thinking_delta" && p.delta.thinking) { think += p.delta.thinking; if (onThink) onThink(think); }
1295
+ if (p.type === "mythos_task") currentTaskId = p.task_id;
1296
+ if (p.type === "message_delta" && p.usage) measured = p.usage;
708
1297
  if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
709
1298
  }
710
1299
  }
711
1300
  if (!out.trim()) throw new Error("Leere Antwort");
712
- tokens.out += Math.ceil(out.length / 4);
1301
+ if (measured?.measured) { tokens.in += measured.input_tokens || 0; tokens.out += measured.output_tokens || 0; }
1302
+ else tokens.unknown = (tokens.unknown || 0) + 1;
713
1303
  return out;
714
1304
  } finally { clearTimeout(to); }
715
1305
  }
@@ -757,6 +1347,25 @@ function runLive(cmd) {
757
1347
  });
758
1348
  }
759
1349
 
1350
+ // start/logs/stop/serve/verify/check – check zusätzlich mit echtem Browser (Chrome/Edge headless), falls installiert.
1351
+ async function cliDevTool(t) {
1352
+ stopTick();
1353
+ const live = t.name === "start" || t.name === "verify" ? (s) => process.stdout.write(C.d + s + C.x) : null;
1354
+ const r = await devTool(t, ROOT, { onChunk: live, track: (p) => { child = p; } });
1355
+ child = null;
1356
+ let text = r.result, image = null;
1357
+ if (t.name === "check" && wantsRender(r)) {
1358
+ say(C.d + " 🌐 Browser-Test …" + C.x);
1359
+ const h = await headlessRender(r.url).catch(() => null);
1360
+ if (h && h.ok) { text = checkReport(r.url, r.http, renderVerdict(h)); image = h.image; }
1361
+ else text = checkReport(r.url, r.http, null, "ℹ Kein Chrome/Edge gefunden – nur HTTP-Prüfung (Pfad mit MYTHOS_CHROME setzen).");
1362
+ }
1363
+ startTick();
1364
+ const show = text.split("\n").filter((l) => /^[✓✗⚠ℹ]|^ERGEBNIS|^Prüfbericht|^Seitenprüfung|^Läuft|^Befehl|^FEHLER/.test(l)).slice(0, 14);
1365
+ say(show.map((l) => (/✗|FEHLER/.test(l) ? C.r : /✓/.test(l) ? C.g : C.d) + " " + l + C.x).join("\n"));
1366
+ return image ? { text, image } : text;
1367
+ }
1368
+
760
1369
  // Mythos Cloud: Backend + KI-Gateway für die gebaute App verbinden und mythos-cloud.js anlegen.
761
1370
  async function cloudConnect(t) {
762
1371
  try {
@@ -778,7 +1387,14 @@ async function runTool(t) {
778
1387
  say(C.c + C.b + "📋 Aufgabenliste " + done + "/" + items.length + C.x + "\n" + items.map((x) => (x.done ? C.g + " ☑ " + C.d : " ☐ ") + x.text + C.x).join("\n"));
779
1388
  return todoSummary(items);
780
1389
  }
1390
+ // Server & Watcher enden nie → automatisch im Hintergrund starten, statt die Aufgabe zu blockieren.
1391
+ if (t.name === "run" && looksLikeServer(t.cmd)) { say(C.d + "↪ " + t.cmd + " läuft dauerhaft – wird im Hintergrund gestartet" + C.x); t = { name: "start", cmd: t.cmd }; }
781
1392
  const label = t.name === "run" ? "run " + t.cmd
1393
+ : t.name === "start" ? "start (Hintergrund) " + t.cmd
1394
+ : t.name === "serve" ? "serve " + (t.path || ".")
1395
+ : t.name === "check" ? "check " + (t.url || t.path || "")
1396
+ : t.name === "verify" ? "verify (Build, Typecheck, Tests …)"
1397
+ : t.name === "logs" || t.name === "stop" ? t.name + " " + (t.id || "")
782
1398
  : t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
783
1399
  : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
784
1400
  : t.name === "websearch" ? "websearch " + t.query
@@ -787,7 +1403,7 @@ async function runTool(t) {
787
1403
  : t.name + " " + (t.path || "");
788
1404
  say(C.y + "⚙ " + label + C.x);
789
1405
  // Sicherheitsnetz: gefährliche Befehle fragen immer nach (auch mit --yes) und werden bei /goal blockiert.
790
- const why = t.name === "run" ? dangerCheck(t.cmd) : null;
1406
+ const why = t.name === "run" || t.name === "start" ? dangerCheck(t.cmd) : null;
791
1407
  if (why) {
792
1408
  say(C.r + "🛡 Sicherheitsnetz: dieser Befehl " + why + "." + C.x);
793
1409
  if (goal) return "BLOCKIERT vom Sicherheitsnetz: Der Befehl " + why + ". Der Nutzer ist nicht da und kann das nicht bestätigen – finde einen sichereren Weg ohne diesen Befehl.";
@@ -796,7 +1412,7 @@ async function runTool(t) {
796
1412
  startTick();
797
1413
  if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
798
1414
  }
799
- if ((t.name === "run" || t.name === "write" || t.name === "edit" || t.name === "cloud") && !autoYes && !why) {
1415
+ if (["run", "start", "verify", "write", "edit", "cloud"].includes(t.name) && !autoYes && !why) {
800
1416
  stopTick();
801
1417
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
802
1418
  startTick();
@@ -804,6 +1420,7 @@ async function runTool(t) {
804
1420
  }
805
1421
  if (t.name === "run") return runLive(t.cmd);
806
1422
  if (t.name === "cloud") return cloudConnect(t);
1423
+ if (isDevTool(t)) return cliDevTool(t);
807
1424
  if (isWebTool(t)) {
808
1425
  const r = await webTool(t);
809
1426
  say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
@@ -816,8 +1433,10 @@ async function runTool(t) {
816
1433
 
817
1434
  // ---------- Eine Aufgabe (auch /goal) ----------
818
1435
  async function turnInner(history) {
819
- const max = goal ? 4000 : 40;
1436
+ const max = goal ? 4000 : 60;
820
1437
  let nudges = 0;
1438
+ const tracker = verifyTracker();
1439
+ const question = String((history.filter((m) => m.role === "user").pop() || {}).content || "");
821
1440
  for (let i = 0; i < max && !stopped; i++) {
822
1441
  const pr = streamPrinter();
823
1442
  let think = null;
@@ -831,6 +1450,8 @@ async function turnInner(history) {
831
1450
  history.push({ role: "assistant", content: out });
832
1451
  const pb = parseToolBlock(out);
833
1452
  if (!pb.tool && !pb.error) {
1453
+ const nudge = tracker.nudge(out, question);
1454
+ if (nudge) { say(C.d + "🔎 Mythos prüft seine Arbeit …" + C.x); history.push({ role: "user", content: nudge }); continue; }
834
1455
  if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
835
1456
  if (++nudges > 40) { say(C.y + "🎯 Mythos kommt beim Ziel nicht weiter – schau es dir bitte an (/weiter macht weiter)." + C.x); return "stuck"; }
836
1457
  history.push({ role: "user", content: GOAL_NUDGE });
@@ -840,8 +1461,11 @@ async function turnInner(history) {
840
1461
  if (pb.error) { history.push({ role: "user", content: pb.error + " Sende den Block vollständig als gültiges JSON in <tool>…</tool>." }); continue; }
841
1462
  const t = pb.tool, bad = toolProblem(t);
842
1463
  if (bad) { history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + bad }); continue; }
843
- const res = await runTool(t);
844
- history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + res });
1464
+ const res = await runTool(t), resText = typeof res === "string" ? res : res.text;
1465
+ tracker.note(t, resText);
1466
+ // Screenshot nur in der neuesten Prüfung mitschicken (spart Tokens).
1467
+ if (res.image) history.forEach((m) => { if (m.shot) { delete m.images; delete m.shot; } });
1468
+ history.push(res.image ? { role: "user", content: "Werkzeug-Ergebnis:\n" + resText + "\n\n(Screenshot der Seite anbei – sieh ihn dir an: Ist das Layout so, wie der Nutzer es will?)", images: [res.image], shot: true } : { role: "user", content: "Werkzeug-Ergebnis:\n" + resText });
845
1469
  }
846
1470
  if (!stopped) say(C.y + "Schrittlimit (" + max + ") erreicht – mit /weiter macht Mythos weiter." + C.x);
847
1471
  return stopped ? "stopped" : "limit";
@@ -862,6 +1486,7 @@ async function summarize(history) {
862
1486
  }
863
1487
 
864
1488
  async function turn(history, input, images) {
1489
+ currentTaskId = null;
865
1490
  if (input != null) history.push({ role: "user", content: input, images });
866
1491
  T0 = Date.now(); working = true; stopped = false; startTick();
867
1492
  const tok0 = tokens.in + tokens.out;
@@ -875,7 +1500,7 @@ async function turn(history, input, images) {
875
1500
  working = false; stopTick();
876
1501
  const took = fmt(Date.now() - T0);
877
1502
  console.log((stopped ? C.r + "■ Gestoppt nach " + took : C.g + "✓ Mythos hat " + took + " gearbeitet") + C.x +
878
- C.d + " · ≈ " + fmtTokens(tokens.in + tokens.out - tok0) + " Tokens (Sitzung ≈ " + fmtTokens(tokens.in + tokens.out) + ")" + C.x + "\n");
1503
+ C.d + " · " + fmtTokens(tokens.in + tokens.out - tok0) + " MythosAI Tokens (Sitzung " + fmtTokens(tokens.in + tokens.out) + ")" + (tokens.unknown ? " · Anbieterwerte teils unbekannt" : "") + C.x + "\n");
879
1504
  if (!stopped && status !== "error") await maybeAd();
880
1505
  if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
881
1506
  // Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
@@ -941,7 +1566,7 @@ const HELP = [
941
1566
  ["/handy <url>", "Handy-Benachrichtigung (ntfy.sh-Thema oder Discord-Webhook) · /handy test · /handy aus"],
942
1567
  ["/vollzugriff an|aus", "Ohne Nachfragen arbeiten"],
943
1568
  ["/modell [name]", "Modell anzeigen oder wechseln"],
944
- ["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
1569
+ ["/tokens", "Gemessenen MythosAI-Verbrauch dieser Sitzung anzeigen"],
945
1570
  ["/clear", "Verlauf leeren"],
946
1571
  ["/review [schwerpunkt]", "Code-Review der Git-Änderungen (ändert nichts)"],
947
1572
  ["/pr [branch]", "Branch pushen und Pull Request erstellen"],
@@ -1024,7 +1649,7 @@ async function command(q, history) {
1024
1649
  cfg.model = m.id; save(cfg);
1025
1650
  return console.log(C.g + "✓ Modell: " + m.label + C.x + "\n");
1026
1651
  }
1027
- if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
1652
+ if (cmd === "/tokens") return console.log(fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " MythosAI Tokens" + (tokens.unknown ? " · " + tokens.unknown + " Aufrufe ohne Anbieterwerte" : "") + "\n");
1028
1653
  if (cmd === "/vollzugriff") { autoYes = !/^(aus|off)$/i.test(arg); return console.log(autoYes ? "Vollzugriff an.\n" : "Vollzugriff aus – Mythos fragt vor Befehlen nach.\n"); }
1029
1654
  if (cmd === "/review") {
1030
1655
  const st = git(["status", "--porcelain"]);
@@ -1068,17 +1693,19 @@ async function command(q, history) {
1068
1693
  }
1069
1694
 
1070
1695
  async function main() {
1071
- const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--key");
1696
+ const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--key" && a !== "--no-update");
1072
1697
  if (args[0] === "login" || !cfg.key) await login();
1073
1698
  if (args[0] === "login") process.exit(0);
1074
1699
  if (args[0] === "logout") { save({}); console.log("Abgemeldet."); process.exit(0); }
1700
+ if (args[0] === "update") { await autoUpdate(true); process.exit(0); }
1701
+ if (args[0] === "--version" || args[0] === "-v") { console.log(VERSION); process.exit(0); }
1702
+ await autoUpdate();
1075
1703
  const branch = gitBranch();
1076
- console.log(C.c + C.b + "\n ✦ MYTHOS CODE" + C.x + C.d + " – by Mythoscraft · " + path.basename(ROOT) + (branch ? " ⎇ " + branch : "") +
1077
- (readMemory(ROOT) ? " · Gedächtnis ✓" : "") + "\n /hilfe für Befehle · Strg+C stoppt · Bilder: Datei ins Terminal ziehen\n" + C.x);
1704
+ banner({ dir: "📁 " + path.basename(ROOT) + (branch ? " ⎇ " + branch : ""), extra: (cfg.name ? "angemeldet als " + cfg.name : "by Mythoscraft") + (readMemory(ROOT) ? " · Gedächtnis ✓" : "") + " · Strg+C stoppt" });
1078
1705
  const history = [];
1079
1706
  if (args.length) { const { text, images } = extractImages(args.join(" ")); await turn(history, text, images); }
1080
1707
  while (true) {
1081
- const q = (await ask(C.c + (goal ? "🎯 " : "") + "› " + C.x)).trim();
1708
+ const q = (await framedAsk(goal)).trim();
1082
1709
  if (!q) continue;
1083
1710
  if (q === "/exit") break;
1084
1711
  if (q.startsWith("/")) { await command(q, history); continue; }
@@ -1089,5 +1716,7 @@ async function main() {
1089
1716
  await turn(history, text + files.map((f) => "\n\n### " + f.name + "\n```\n" + f.content + "\n```").join(""), images);
1090
1717
  }
1091
1718
  rl.close();
1719
+ process.exit(0); // beendet auch Hintergrund-Server (process.on("exit") in den Werkzeugen)
1092
1720
  }
1721
+ for (const sig of ["SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
1093
1722
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.7.8",
3
+ "version": "1.10.14",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",