mythos-code 1.7.8 → 1.9.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 (3) hide show
  1. package/README.md +2 -0
  2. package/bin/mythos.js +627 -24
  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,32 @@ 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
+ "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.",
196
+ "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.",
197
+ "",
198
+ "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.",
199
+ "",
200
+ "WEBSEITEN RICHTIG AUFSETZEN (gegen weiße Seiten und „Cannot GET /“):",
201
+ "- 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.",
202
+ "- 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\".",
203
+ "- 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).",
204
+ "- 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.",
205
+ "",
206
+ "ANTWORTEN: Vor einem Werkzeug höchstens ein kurzer Satz. Die Abschlussantwort (ohne Werkzeug) knapp und strukturiert in Markdown:",
207
+ "### ✅ Ergebnis – ein bis zwei Sätze, was jetzt geht",
208
+ "**Geändert** – Liste: `datei` – was",
209
+ "**Geprüft** – Liste mit ✓/✗: was du ausgeführt bzw. angesehen hast",
210
+ "**Ansehen/Starten** – Adresse (läuft schon) oder Befehl, falls relevant",
211
+ "**Offen** – nur, wenn etwas fehlt oder der Nutzer etwas tun muss",
212
+ "Keine langen Fließtexte, keinen Code wiederholen. Bei reinen Fragen ohne Codeänderung einfach direkt und gegliedert antworten.",
129
213
  ].join("\n");
130
214
 
131
215
  // Modelle, die v1-messages versteht (siehe mapModel dort).
@@ -174,7 +258,7 @@ function streamActivity(full) {
174
258
  return "🔧 bereitet " + (name || "Werkzeug") + " vor…";
175
259
  }
176
260
  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.";
261
+ 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
262
 
179
263
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
180
264
 
@@ -512,6 +596,427 @@ async function pushNotify(url, title, message) {
512
596
  } catch { return false; }
513
597
  }
514
598
 
599
+ // ---------- Prüfen, Hintergrund-Prozesse, eingebauter Webserver ----------
600
+ // Braucht zusätzlich `spawn`, `exec` (child_process) und `http` im umgebenden Code.
601
+
602
+ const stripAnsi = (s) => String(s || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
603
+ const tailText = (s, n) => { s = stripAnsi(s).trim(); return s.length > n ? "…" + s.slice(-n) : s; };
604
+ function killProc(p) {
605
+ if (!p || p.exitCode != null) return;
606
+ try {
607
+ if (process.platform === "win32") exec("taskkill /pid " + p.pid + " /T /F");
608
+ else { try { process.kill(-p.pid, "SIGTERM"); } catch { p.kill("SIGTERM"); } }
609
+ } catch { /* schon beendet */ }
610
+ }
611
+
612
+
613
+
614
+ /** Lokale Adresse aus Server-Ausgabe lesen („Local: http://localhost:5173/“, „listening on port 3000“ …). */
615
+ function findLocalUrl(out) {
616
+ const s = stripAnsi(out);
617
+ const m = s.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\])(?::\d+)?(?:\/[^\s"'<>)\]]*)?/i);
618
+ if (m) return m[0].replace(/0\.0\.0\.0|\[::1?\]/, "localhost").replace(/[.,;:]+$/, "");
619
+ 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);
620
+ return p ? "http://localhost:" + p[1] + "/" : "";
621
+ }
622
+
623
+ const BG = new Map(); let bgSeq = 0;
624
+ /** Langlaufenden Befehl im Hintergrund starten. Wartet, bis eine Adresse erscheint, der Prozess endet oder nichts mehr passiert. */
625
+ function bgStart(cmd, cwd, opts) {
626
+ opts = opts || {};
627
+ // Derselbe Befehl im selben Ordner läuft schon → erst beenden (sonst „Port belegt“).
628
+ for (const j of BG.values()) if (j.cmd === cmd && j.cwd === cwd && j.code == null) { killProc(j.p); BG.delete(j.id); }
629
+ return new Promise((resolve) => {
630
+ const id = "p" + (++bgSeq);
631
+ let p;
632
+ try {
633
+ p = spawn(cmd, { cwd: cwd || undefined, shell: true, windowsHide: true, detached: process.platform !== "win32",
634
+ env: { ...process.env, BROWSER: "none", FORCE_COLOR: "0", NO_COLOR: "1", ...(opts.env || {}) } });
635
+ } catch (e) { resolve({ result: "FEHLER: " + e.message }); return; }
636
+ const job = { id, cmd, cwd, p, out: "", url: "", code: null, t0: Date.now(), last: Date.now() };
637
+ BG.set(id, job);
638
+ let done = false;
639
+ const max = Math.min(300, Math.max(5, +opts.wait || 90)) * 1000;
640
+ const finish = () => {
641
+ if (done) return; done = true; clearInterval(timer);
642
+ const out = tailText(job.out, 3000);
643
+ if (job.code != null) {
644
+ BG.delete(id);
645
+ const busy = /EADDRINUSE|address already in use|port \d+ is (?:already )?in use/i.test(job.out);
646
+ resolve({ result: (job.code === 0 ? "Befehl ist fertig (Exit 0) – er lief nicht dauerhaft weiter.\n" : "FEHLER: Prozess beendet mit Exit " + job.code + ".\n") +
647
+ (busy ? "Der Port ist belegt – stoppe den alten Prozess (logs/stop) oder nimm einen anderen Port.\n" : "") + (out || "(keine Ausgabe)"), code: job.code });
648
+ return;
649
+ }
650
+ resolve({ id, url: job.url, result: (job.url
651
+ ? "✓ Läuft im Hintergrund (id " + id + "): " + job.url + "\nAls Nächstes: Seite mit check prüfen."
652
+ : "Läuft im Hintergrund (id " + id + "), noch keine Adresse in der Ausgabe erkannt. Mit logs nachsehen.") +
653
+ "\nAusgabe ansehen: {\"name\":\"logs\",\"id\":\"" + id + "\"} · beenden: {\"name\":\"stop\",\"id\":\"" + id + "\"}\n\nAusgabe bisher:\n" + (out || "(keine)") });
654
+ };
655
+ const on = (dec) => (d) => {
656
+ const s = dec(d); job.out += s; job.last = Date.now();
657
+ if (job.out.length > 200000) job.out = job.out.slice(-100000);
658
+ if (!done && opts.onChunk) opts.onChunk(s);
659
+ if (!job.url) { const u = findLocalUrl(job.out); if (u) { job.url = u; setTimeout(finish, 1200); } }
660
+ };
661
+ p.stdout.on("data", on(outputDecoder())); p.stderr.on("data", on(outputDecoder()));
662
+ p.on("error", (e) => { job.out += "\nFEHLER: " + e.message; });
663
+ p.on("close", (code) => { job.code = code == null ? 1 : code; setTimeout(finish, 100); });
664
+ const timer = setInterval(() => {
665
+ const el = Date.now() - job.t0;
666
+ if (el > max || (el > 25000 && Date.now() - job.last > 25000)) finish();
667
+ }, 500);
668
+ });
669
+ }
670
+ function bgLogs(id) {
671
+ if (!id) {
672
+ const list = [...BG.values()].filter((j) => j.code == null);
673
+ 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.";
674
+ }
675
+ const j = BG.get(String(id));
676
+ if (!j) return "FEHLER: Kein Hintergrundprozess mit id " + id + ". " + bgLogs("");
677
+ return (j.code == null ? "läuft" : "beendet (Exit " + j.code + ")") + " · " + j.cmd + (j.url ? " · " + j.url : "") + "\n" + (tailText(j.out, 6000) || "(keine Ausgabe)");
678
+ }
679
+ function bgStop(id) {
680
+ const list = id === "all" || !id ? [...BG.values()] : [BG.get(String(id))].filter(Boolean);
681
+ if (!list.length) return "FEHLER: Kein Hintergrundprozess mit id " + id + ".";
682
+ list.forEach((j) => { killProc(j.p); BG.delete(j.id); });
683
+ return "Beendet: " + list.map((j) => j.id + " (" + j.cmd + ")").join(", ");
684
+ }
685
+ function stopAllBackground() {
686
+ for (const j of BG.values()) killProc(j.p);
687
+ BG.clear();
688
+ for (const s of SERVERS.values()) try { s.server.close(); } catch { /* egal */ }
689
+ SERVERS.clear();
690
+ }
691
+ process.on("exit", stopAllBackground);
692
+
693
+ // ---------- Eingebauter Webserver (für statische Seiten) ----------
694
+ 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",
695
+ 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",
696
+ 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",
697
+ txt: "text/plain; charset=utf-8", xml: "application/xml", pdf: "application/pdf", wasm: "application/wasm", webmanifest: "application/manifest+json" };
698
+ const SERVERS = new Map();
699
+ /** Braucht diese index.html einen Build-Schritt (TS/JSX/Vite)? */
700
+ const needsBundler = (html) => /<script[^>]+src=["'][^"']*\.(?:tsx?|jsx|vue|svelte)["']/i.test(html);
701
+ function listenFree(server, port) {
702
+ return new Promise((res, rej) => {
703
+ const tryPort = (p) => {
704
+ server.once("error", (e) => (e.code === "EADDRINUSE" && p < port + 30 ? tryPort(p + 1) : rej(e)));
705
+ server.listen(p, "127.0.0.1", () => res(server.address().port));
706
+ };
707
+ tryPort(port);
708
+ });
709
+ }
710
+ async function serveDir(root, rel) {
711
+ let target = path.resolve(root, rel || ".");
712
+ if (!fs.existsSync(target)) return { result: "FEHLER: Nicht gefunden: " + (rel || ".") };
713
+ let page = "";
714
+ if (fs.statSync(target).isFile()) { page = path.basename(target); target = path.dirname(target); }
715
+ if (!page && !fs.existsSync(path.join(target, "index.html"))) {
716
+ const sub = ["dist", "build", "public", "out", "www", "site", "docs"].find((d) => fs.existsSync(path.join(target, d, "index.html")));
717
+ const pkg = readText(path.join(target, "package.json")) || "";
718
+ 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)" : "") + "." };
719
+ 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\"." };
720
+ target = path.join(target, sub);
721
+ }
722
+ const indexHtml = readText(path.join(target, page || "index.html")) || "";
723
+ 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\"." };
724
+ const dir = target;
725
+ let srv = SERVERS.get(dir);
726
+ if (!srv) {
727
+ const server = http.createServer((req, res) => {
728
+ let p; try { p = decodeURIComponent(new URL(req.url, "http://x").pathname); } catch { p = "/"; }
729
+ let f = path.join(dir, p);
730
+ if (path.relative(dir, f).startsWith("..")) { res.writeHead(403); res.end("Verboten"); return; }
731
+ try { if (fs.statSync(f).isDirectory()) f = path.join(f, "index.html"); } catch { /* gibt es nicht */ }
732
+ // Einzelseiten-Apps: unbekannte Pfade ohne Dateiendung → index.html.
733
+ if (!fs.existsSync(f) && !path.extname(p) && fs.existsSync(path.join(dir, "index.html"))) f = path.join(dir, "index.html");
734
+ fs.readFile(f, (err, data) => {
735
+ if (err) { res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); res.end("404 – Datei nicht gefunden: " + p); return; }
736
+ res.writeHead(200, { "content-type": MIME[path.extname(f).slice(1).toLowerCase()] || "application/octet-stream", "cache-control": "no-store" });
737
+ res.end(data);
738
+ });
739
+ });
740
+ let port;
741
+ try { port = await listenFree(server, 5500); } catch (e) { return { result: "FEHLER: Webserver konnte nicht starten: " + e.message }; }
742
+ srv = { server, url: "http://127.0.0.1:" + port + "/" };
743
+ SERVERS.set(dir, srv);
744
+ }
745
+ const url = srv.url + (page && page !== "index.html" ? encodeURI(page) : "");
746
+ 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." };
747
+ }
748
+
749
+ // ---------- Seite prüfen (HTTP + alle Skripte/CSS) ----------
750
+ async function getLocal(url, ms, method, accept) {
751
+ const tries = [url];
752
+ if (/\/\/localhost\b/i.test(url)) tries.push(url.replace(/\/\/localhost\b/i, "//127.0.0.1"), url.replace(/\/\/localhost\b/i, "//[::1]"));
753
+ let last;
754
+ for (const u of tries) {
755
+ const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), ms || 15000);
756
+ try {
757
+ const r = await fetch(u, { method: method || "GET", redirect: "follow", signal: ctl.signal, headers: { "user-agent": WEB_UA, accept: accept || "text/html,*/*" } });
758
+ const text = method === "HEAD" ? "" : Buffer.from(await r.arrayBuffer()).subarray(0, 3e6).toString("utf8");
759
+ return { ok: r.ok, status: r.status, type: r.headers.get("content-type") || "", text, url: r.url || u };
760
+ } catch (e) { last = e; } finally { clearTimeout(to); }
761
+ }
762
+ throw last;
763
+ }
764
+ /** HTTP-Prüfung einer Seite. → { lines, fail, warn, url, html, reachable } */
765
+ async function httpPageCheck(url) {
766
+ const lines = []; let fail = 0, warn = 0;
767
+ let r;
768
+ try { r = await getLocal(url, 20000); }
769
+ catch (e) {
770
+ const why = e.name === "AbortError" ? "Zeitüberschreitung" : (e.cause && e.cause.code) || e.message;
771
+ 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)."] };
772
+ }
773
+ const isHtml = /html/i.test(r.type) || /^\s*<(?:!doctype|html)/i.test(r.text);
774
+ 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 : ""));
775
+ if (!r.ok) {
776
+ fail++;
777
+ 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.");
778
+ else lines.push("Antwort: " + htmlToText(r.text).slice(0, 400));
779
+ return { reachable: true, status: r.status, fail, warn, url: r.url, html: r.text, lines };
780
+ }
781
+ 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)]) };
782
+ const html = r.text;
783
+ const title = (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1];
784
+ if (title) lines.push("Titel: „" + decodeEntities(title.trim()).slice(0, 80) + "“");
785
+ 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)."); }
786
+ 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."); }
787
+ // Alle eingebundenen Skripte und Stylesheets laden.
788
+ const refs = [];
789
+ html.replace(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi, (_m, s) => refs.push(["Skript", s]));
790
+ 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]]); } });
791
+ const base = new URL(r.url);
792
+ let okRefs = 0;
793
+ for (const [kind, ref] of refs.slice(0, 30)) {
794
+ let u; try { u = new URL(ref, base); } catch { continue; }
795
+ if (u.host !== base.host) continue;
796
+ try {
797
+ const a = await getLocal(u.href, 15000, "GET", kind === "CSS" ? "text/css,*/*;q=0.1" : "*/*");
798
+ const isJs = kind !== "CSS";
799
+ if (!a.ok) { fail++; lines.push("✗ " + kind + " " + u.pathname + " → HTTP " + a.status + " (Datei fehlt oder Pfad falsch)"); }
800
+ 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)"); }
801
+ else okRefs++;
802
+ } catch (e) { fail++; lines.push("✗ " + kind + " " + u.pathname + " → " + e.message); }
803
+ }
804
+ if (refs.length) lines.push((okRefs === refs.length ? "✓ " : "") + okRefs + "/" + refs.length + " eingebundene Skripte/CSS laden");
805
+ const bodyText = htmlToText((html.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, html])[1]);
806
+ if (bodyText.length < 3) {
807
+ const hasScripts = /<script\b/i.test(html);
808
+ if (!hasScripts) { fail++; lines.push("✗ Die Seite ist leer: kein sichtbarer Text und keine Skripte."); }
809
+ else { warn++; lines.push("ℹ Der Inhalt entsteht erst per JavaScript (leeres <body>/#root) – ob er wirklich erscheint, zeigt nur der Browser-Test."); }
810
+ } else lines.push("✓ Sichtbarer Text im HTML: " + bodyText.length + " Zeichen – „" + bodyText.replace(/\s+/g, " ").slice(0, 120) + "“");
811
+ return { reachable: true, status: r.status, fail, warn, url: r.url, html, lines };
812
+ }
813
+
814
+ /** Projekt automatisch prüfen: fehlende Pakete installieren, Typecheck, Lint, Build, Tests. */
815
+ function runCapture(cmd, cwd, opts) {
816
+ opts = opts || {};
817
+ return new Promise((res) => {
818
+ let out = "";
819
+ const p = spawn(cmd, { cwd, shell: true, windowsHide: true, env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", BROWSER: "none", ...(opts.env || {}) } });
820
+ if (opts.track) opts.track(p);
821
+ const on = (dec) => (d) => { const s = dec(d); out += s; if (out.length > 200000) out = out.slice(-100000); if (opts.onChunk) opts.onChunk(s); };
822
+ p.stdout.on("data", on(outputDecoder())); p.stderr.on("data", on(outputDecoder()));
823
+ const to = setTimeout(() => { out += "\n[Zeitüberschreitung – abgebrochen]"; killProc(p); }, opts.timeout || 600000);
824
+ p.on("error", (e) => { out += "\nFEHLER: " + e.message; });
825
+ p.on("close", (code) => { clearTimeout(to); res({ code: code == null ? 1 : code, out }); });
826
+ });
827
+ }
828
+ function detectChecks(root) {
829
+ const has = (f) => fs.existsSync(path.join(root, f));
830
+ const checks = [];
831
+ let pkg = null; try { pkg = JSON.parse(readText(path.join(root, "package.json")) || "null"); } catch { pkg = null; }
832
+ if (pkg) {
833
+ const pm = has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("bun.lockb") || has("bun.lock") ? "bun" : "npm";
834
+ const run = (s) => (pm === "npm" ? "npm run " + s : pm + " run " + s);
835
+ const sc = pkg.scripts || {};
836
+ const deps = Object.keys(Object.assign({}, pkg.dependencies, pkg.devDependencies));
837
+ if (deps.length && !has("node_modules")) checks.push({ label: "Pakete installieren", cmd: pm + " install", fatal: true });
838
+ const tc = ["typecheck", "type-check", "tsc", "check-types"].find((s) => sc[s]);
839
+ if (tc) checks.push({ label: "Typecheck", cmd: run(tc) });
840
+ else if (has("tsconfig.json") && !/\btsc\b/.test(sc.build || "") && (deps.includes("typescript") || has("node_modules/typescript"))) {
841
+ const ts = readText(path.join(root, "tsconfig.json")) || "";
842
+ checks.push({ label: "Typecheck", cmd: /"references"\s*:/.test(ts) ? "npx tsc -b" : "npx tsc --noEmit" });
843
+ }
844
+ if (sc.lint) checks.push({ label: "Lint", cmd: run("lint"), soft: true });
845
+ if (sc.build) checks.push({ label: "Build", cmd: run("build") });
846
+ if (sc.test && !/no test specified/i.test(sc.test)) checks.push({ label: "Tests", cmd: pm === "npm" ? "npm test" : pm + " test", env: { CI: "1" } });
847
+ if (!sc.build && !sc.test) {
848
+ const main = [pkg.main, "index.js", "server.js", "app.js"].find((f) => f && has(f) && /\.c?js$/.test(f));
849
+ if (main) checks.push({ label: "Syntax " + main, cmd: "node --check " + JSON.stringify(main) });
850
+ }
851
+ }
852
+ const py = has("requirements.txt") || has("pyproject.toml") || has("setup.py") || (!pkg && fs.readdirSync(root).some((f) => /\.py$/.test(f)));
853
+ if (py) {
854
+ const pyc = process.platform === "win32" ? "python" : "python3";
855
+ checks.push({ label: "Python-Syntax", cmd: pyc + " -m compileall -q -x \"(node_modules|venv|\\.venv|__pycache__)\" ." });
856
+ if (has("pytest.ini") || has("conftest.py") || has("tests")) checks.push({ label: "Tests", cmd: pyc + " -m pytest -q" });
857
+ }
858
+ if (has("Cargo.toml")) checks.push({ label: "Build", cmd: "cargo build" }, { label: "Tests", cmd: "cargo test" });
859
+ if (has("go.mod")) checks.push({ label: "Build", cmd: "go build ./..." }, { label: "Tests", cmd: "go test ./..." });
860
+ if (has("pom.xml")) checks.push({ label: "Build", cmd: "mvn -q -DskipTests package" });
861
+ return { checks, pkg };
862
+ }
863
+ /** Statische Seiten: verlinkte lokale Dateien müssen existieren. */
864
+ function staticRefCheck(root) {
865
+ const pages = [];
866
+ const walk = (dir, depth) => {
867
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
868
+ for (const e of entries) {
869
+ const full = path.join(dir, e.name);
870
+ if (e.isDirectory()) { if (depth < 3 && !SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(full, depth + 1); }
871
+ else if (/\.html?$/i.test(e.name) && pages.length < 40) pages.push(full);
872
+ }
873
+ };
874
+ walk(root, 0);
875
+ const missing = [];
876
+ for (const f of pages) {
877
+ const html = readText(f) || "";
878
+ html.replace(/<(?:script|img|source|link|iframe)\b[^>]*\b(?:src|href)=["']([^"'#?]+)/gi, (_m, ref) => {
879
+ if (/^(?:[a-z]+:|\/\/|data:|\$|\{)/i.test(ref)) return;
880
+ const tgt = ref.startsWith("/") ? path.join(root, ref) : path.join(path.dirname(f), ref);
881
+ if (!fs.existsSync(tgt) && !/\/src\/main\.\w+$/.test(ref)) missing.push(path.relative(root, f).split(path.sep).join("/") + " → " + ref);
882
+ });
883
+ }
884
+ return { pages: pages.length, missing };
885
+ }
886
+ async function verifyProject(root, opts) {
887
+ opts = opts || {};
888
+ const { checks, pkg } = detectChecks(root);
889
+ const lines = []; let fail = 0;
890
+ for (const c of checks) {
891
+ if (opts.onChunk) opts.onChunk("\n▶ " + c.label + ": " + c.cmd + "\n");
892
+ const r = await runCapture(c.cmd, root, { onChunk: opts.onChunk, track: opts.track, env: c.env, timeout: 900000 });
893
+ if (r.code === 0) lines.push("✓ " + c.label + " (" + c.cmd + ")");
894
+ else {
895
+ if (!c.soft) fail++;
896
+ 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"));
897
+ if (c.fatal) break;
898
+ }
899
+ }
900
+ const st = staticRefCheck(root);
901
+ if (st.missing.length) { fail++; lines.push("✗ Fehlende Dateien in HTML-Seiten:\n" + st.missing.slice(0, 20).map((m) => " " + m).join("\n")); }
902
+ else if (st.pages && !pkg) lines.push("✓ " + st.pages + " HTML-Seite(n): alle verlinkten lokalen Dateien vorhanden");
903
+ 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)." };
904
+ const web = st.pages || (pkg && /"(?:vite|react|react-dom|vue|svelte|next|express|astro)"/.test(JSON.stringify(Object.assign({}, pkg.dependencies, pkg.devDependencies))));
905
+ 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.") +
906
+ (web ? "\nDas ist ein Web-Projekt: starte es (serve bzw. start) und prüfe die Seite mit check." : "") };
907
+ }
908
+
909
+ // ---------- Werkzeuge start/logs/stop/serve/verify/check (gemeinsamer Teil) ----------
910
+ const DEV_TOOLS = ["start", "logs", "stop", "serve", "verify", "check"];
911
+ const isDevTool = (t) => DEV_TOOLS.includes(t.name);
912
+ /** Ziel für check: Adresse, Datei/Ordner (→ eingebauter Webserver) oder zuletzt gestarteter Server. */
913
+ async function checkTarget(t, root) {
914
+ let url = String(t.url || t.path || "").trim();
915
+ if (!url) {
916
+ const job = [...BG.values()].reverse().find((j) => j.url && j.code == null);
917
+ const srv = [...SERVERS.values()].pop();
918
+ url = (job && job.url) || (srv && srv.url) || "";
919
+ if (!url) return { error: "FEHLER: Keine Adresse angegeben und kein laufender Server. Erst serve/start, dann check mit \"url\"." };
920
+ }
921
+ if (!/^https?:\/\//i.test(url)) {
922
+ if (/^(?:localhost|127\.0\.0\.1)(?::\d+)?/i.test(url)) url = "http://" + url;
923
+ else { const s = await serveDir(root, url.replace(/^file:\/\/\/?/i, "")); if (!s.url) return { error: s.result }; url = s.url; }
924
+ }
925
+ return { url };
926
+ }
927
+ /** Prüfbericht aus HTTP-Prüfung h und (optional) Browser-Test rv = renderVerdict(…). */
928
+ function checkReport(url, h, rv, note) {
929
+ const lines = h.lines.concat(rv ? ["", "Browser-Test (echt gerendert):"].concat(rv.lines) : note ? [note] : []);
930
+ const fail = h.fail + (rv ? rv.fail : 0);
931
+ return "Seitenprüfung " + url + "\n" + lines.join("\n") + "\n\n" + (fail
932
+ ? "ERGEBNIS: ✗ Probleme gefunden – Ursache beheben und erneut prüfen."
933
+ : rv ? "ERGEBNIS: ✓ Seite lädt ohne Fehler und zeigt Inhalt." : "ERGEBNIS: ✓ Seite antwortet, alle Dateien laden.");
934
+ }
935
+ /** Muss die Seite noch im Browser gerendert werden? (erreichbar und HTML) */
936
+ const wantsRender = (r) => !!(r && r.http && r.http.reachable && r.http.html && r.http.status < 400);
937
+ /** opts: { onChunk, track } – Live-Ausgabe und Abbrechen der Oberfläche. */
938
+ async function devTool(t, root, opts) {
939
+ opts = opts || {};
940
+ try {
941
+ if (t.name === "start") {
942
+ if (!t.cmd) return { result: 'FEHLER: "cmd" fehlt.' };
943
+ return await bgStart(String(t.cmd), root, { onChunk: opts.onChunk, wait: t.wait });
944
+ }
945
+ if (t.name === "logs") return { result: bgLogs(t.id) };
946
+ if (t.name === "stop") return { result: bgStop(t.id) };
947
+ if (t.name === "serve") return await serveDir(root, t.path);
948
+ if (t.name === "verify") return await verifyProject(root, opts);
949
+ if (t.name === "check") {
950
+ const tg = await checkTarget(t, root);
951
+ if (tg.error) return { result: tg.error };
952
+ const h = await httpPageCheck(tg.url);
953
+ return { url: tg.url, http: h, result: checkReport(tg.url, h) };
954
+ }
955
+ } catch (e) { return { result: "FEHLER: " + e.message }; }
956
+ return { result: "Unbekanntes Werkzeug: " + t.name };
957
+ }
958
+
959
+ // ---------- Browser-Test ohne Fenster (CLI): installiertes Chrome/Edge/Chromium headless ----------
960
+ function findChrome() {
961
+ const env = process.env.MYTHOS_CHROME || process.env.CHROME_PATH;
962
+ const pf = [process.env["PROGRAMFILES"], process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
963
+ const list = (env ? [env] : []).concat(process.platform === "win32"
964
+ ? pf.flatMap((b) => [b + "\\Google\\Chrome\\Application\\chrome.exe", b + "\\Microsoft\\Edge\\Application\\msedge.exe", b + "\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"])
965
+ : process.platform === "darwin"
966
+ ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Chromium.app/Contents/MacOS/Chromium"]
967
+ : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/microsoft-edge", "/snap/bin/chromium"]);
968
+ return list.find((f) => { try { return fs.statSync(f).isFile(); } catch { return false; } }) || "";
969
+ }
970
+ function chromeRun(bin, args, ms) {
971
+ return new Promise((res) => {
972
+ let out = "", err = "";
973
+ const p = spawn(bin, args, { windowsHide: true });
974
+ const to = setTimeout(() => killProc(p), ms);
975
+ p.stdout.on("data", (d) => { out += d; }); p.stderr.on("data", (d) => { err += d; if (err.length > 400000) err = err.slice(-200000); });
976
+ p.on("error", (e) => { clearTimeout(to); res({ out, err: err + e.message, code: 1 }); });
977
+ p.on("close", (code) => { clearTimeout(to); res({ out, err, code }); });
978
+ });
979
+ }
980
+ /** Seite in echtem Chrome rendern: sichtbarer Text, Konsolenfehler, Screenshot. null = kein Browser gefunden. */
981
+ async function headlessRender(url) {
982
+ const bin = findChrome();
983
+ if (!bin) return null;
984
+ const tmp = path.join(process.env.TEMP || process.env.TMPDIR || "/tmp", "mythos-check-" + process.pid);
985
+ try { fs.mkdirSync(tmp, { recursive: true }); } catch { /* egal */ }
986
+ const shot = path.join(tmp, "shot.png");
987
+ const base = ["--headless=new", "--disable-gpu", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", "--user-data-dir=" + path.join(tmp, "profil"),
988
+ "--enable-logging=stderr", "--v=0", "--virtual-time-budget=5000", "--window-size=1280,800"];
989
+ if (process.platform === "linux" && process.getuid && process.getuid() === 0) base.push("--no-sandbox");
990
+ const dom = await chromeRun(bin, base.concat(["--dump-dom", url]), 45000);
991
+ try { fs.rmSync(shot, { force: true }); } catch { /* egal */ }
992
+ await chromeRun(bin, base.concat(["--screenshot=" + shot, url]), 45000);
993
+ const errors = [];
994
+ for (const m of dom.err.matchAll(/CONSOLE[:(]\d+\)?\]\s*"([^\n]*?)",\s*source:\s*(\S*)\s*\((\d+)\)/g)) {
995
+ const msg = m[1] + (m[2] ? " (" + m[2].split("/").pop() + ":" + m[3] + ")" : "");
996
+ 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);
997
+ }
998
+ const html = dom.out || "";
999
+ const body = (html.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, html])[1];
1000
+ const text = htmlToText(body);
1001
+ const vis = (body.match(/<(?:img|svg|canvas|video|button|input|iframe)\b/gi) || []).length;
1002
+ let image = null;
1003
+ try { const b = fs.readFileSync(shot); if (b.length > 2000) image = { media_type: "image/png", data: b.toString("base64") }; } catch { /* kein Screenshot */ }
1004
+ 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 : "" };
1005
+ }
1006
+ /** Browser-Ergebnis in Prüfzeilen übersetzen. r = { text, vis, errors, failed?, overlay?, sample } */
1007
+ function renderVerdict(r) {
1008
+ const lines = []; let fail = 0;
1009
+ const blank = r.text < 3 && r.vis < 1;
1010
+ if (r.overlay) { fail++; lines.push("✗ Fehler-Overlay des Dev-Servers sichtbar: " + r.overlay.slice(0, 800)); }
1011
+ 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.")); }
1012
+ else lines.push("✓ Im Browser sichtbar: " + r.text + " Zeichen Text" + (r.vis ? ", " + r.vis + " Bilder/Bedienelemente" : "") + (r.sample ? " – „" + r.sample.slice(0, 140) + "“" : ""));
1013
+ const errs = [...new Set(r.errors)];
1014
+ if (errs.length) { fail++; lines.push("✗ Konsolenfehler (" + errs.length + "):\n" + errs.slice(0, 12).map((e) => " " + e.slice(0, 400)).join("\n")); }
1015
+ else lines.push("✓ Keine Konsolenfehler");
1016
+ if (r.failed && r.failed.length) { fail++; lines.push("✗ Nicht geladene Dateien:\n" + r.failed.slice(0, 12).map((e) => " " + e).join("\n")); }
1017
+ return { lines, fail };
1018
+ }
1019
+
515
1020
 
516
1021
  const API = process.env.MYTHOS_API_URL || "https://rdxaqiacoitzeowehdts.supabase.co/functions/v1/v1-messages";
517
1022
  const CFG = path.join(os.homedir(), ".mythos-code.json");
@@ -529,7 +1034,65 @@ let goal = "";
529
1034
  const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
530
1035
  TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
531
1036
 
532
- const SITE = process.env.MYTHOS_SITE_URL || "https://mythos-core-ai.lovable.app";
1037
+ const SITE = process.env.MYTHOS_SITE_URL || "https://mythos-ai-final-version.lovable.app";
1038
+ const VERSION = "1.9.0";
1039
+ const PKG = "mythos-code";
1040
+
1041
+ // ---------- Auto-Update über npm ----------
1042
+ // Beim Start (höchstens alle 6 h) die neueste Version auf npm prüfen. Ist eine neuere da,
1043
+ // wird sie mit `npm i -g` installiert und Mythos startet direkt neu – ohne Zutun des Users.
1044
+ // Abschalten: MYTHOS_NO_UPDATE=1 oder --no-update. Manuell: `mythos update`.
1045
+ 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; };
1046
+ async function latestVersion() {
1047
+ const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 2500);
1048
+ try { const r = await fetch("https://registry.npmjs.org/" + PKG + "/latest", { signal: ctl.signal }); return r.ok ? (await r.json()).version : null; }
1049
+ catch { return null; } finally { clearTimeout(t); }
1050
+ }
1051
+ function installLatest(ver) {
1052
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
1053
+ try { execFileSync(npm, ["install", "-g", PKG + "@" + ver, "--no-audit", "--no-fund", "--loglevel=error"], { stdio: "inherit", shell: process.platform === "win32" }); return true; }
1054
+ catch { return false; }
1055
+ }
1056
+ async function autoUpdate(force = false) {
1057
+ if (VERSION.startsWith("__") || process.env.MYTHOS_UPDATED) return;
1058
+ if (!force && (process.env.MYTHOS_NO_UPDATE || process.argv.includes("--no-update"))) return;
1059
+ const c = load();
1060
+ if (!force && c.updateCheckedAt && Date.now() - c.updateCheckedAt < 6 * 3600e3) return;
1061
+ const latest = await latestVersion();
1062
+ save(Object.assign(load(), { updateCheckedAt: Date.now() }));
1063
+ if (!latest || !newer(latest, VERSION)) { if (force) console.log(C.g + " ✓ Mythos Code ist aktuell (" + VERSION + ")" + C.x); return; }
1064
+ console.log(C.m + " ↑ Update " + VERSION + " → " + latest + " wird installiert…" + C.x);
1065
+ if (!installLatest(latest)) { console.log(C.y + " Update fehlgeschlagen. Manuell: npm i -g " + PKG + "@latest" + C.x); return; }
1066
+ console.log(C.g + " ✓ Aktualisiert auf " + latest + " – starte neu…\n" + C.x);
1067
+ rl.close();
1068
+ 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" }) });
1069
+ child.on("exit", (code) => process.exit(code ?? 0));
1070
+ child.on("error", () => process.exit(0));
1071
+ await new Promise(() => {}); // hier nicht weiterlaufen – der neue Prozess übernimmt
1072
+ }
1073
+
1074
+ // ---------- Startbildschirm mit Maskottchen „Myth“ ----------
1075
+ // Kleiner Block-Charakter (Minecraft-Stil) – das Logo der Website bleibt unverändert.
1076
+ const MASCOT = [
1077
+ " ▄█████▄ ",
1078
+ " ██▀███▀██ ",
1079
+ " ▀███████▀ ",
1080
+ " ▀▀ ▀▀ ",
1081
+ ];
1082
+ function banner(info) {
1083
+ const tip = ["/hilfe zeigt alle Befehle", "Dateien mit @name anhängen", "Bilder einfach ins Terminal ziehen", "/goal <ziel> – Mythos bleibt dran, bis es fertig ist"][Math.floor(Math.random() * 4)];
1084
+ const w = Math.min((process.stdout.columns || 80) - 4, 64);
1085
+ const line = C.d + "─".repeat(w) + C.x;
1086
+ const right = [
1087
+ C.b + "Mythos Code" + C.x + C.d + " v" + VERSION + C.x,
1088
+ C.d + info.dir + C.x,
1089
+ C.d + info.extra + C.x,
1090
+ C.c + "✦ " + C.x + C.d + tip + C.x,
1091
+ ];
1092
+ console.log("\n" + line);
1093
+ MASCOT.forEach((m, i) => console.log(C.m + m + C.x + " " + (right[i] || "")));
1094
+ console.log(line + "\n");
1095
+ }
533
1096
  const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
534
1097
  const authPost = async (body) => (await fetch(FN + "/cli-auth", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
535
1098
 
@@ -591,9 +1154,9 @@ async function maybeAd() {
591
1154
  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
1155
  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
1156
  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; }
1157
+ 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-version.lovable.app/werbung\x1b[0m\n"); return; }
595
1158
  }
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");
1159
+ 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-version.lovable.app/werbung\x1b[0m\n");
597
1160
  } catch {}
598
1161
  }
599
1162
  const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
@@ -683,18 +1246,18 @@ function compact(history, level) {
683
1246
  }
684
1247
 
685
1248
  let tokens = { in: 0, out: 0 };
1249
+ let currentTaskId = null;
686
1250
  async function once(messages, onText, onThink) {
687
1251
  const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
688
1252
  const system = SYSTEM();
689
- tokens.in += estimateTokens(system, messages);
690
1253
  try {
691
1254
  const r = await fetch(API, {
692
1255
  method: "POST", signal: c.signal,
693
1256
  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 }),
1257
+ body: JSON.stringify({ model: cfg.model || "mythos-code", task_id: currentTaskId, max_tokens: 8000, system, messages, stream: true }),
695
1258
  });
696
1259
  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 = "";
1260
+ const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "", think = "", measured = null;
698
1261
  while (true) {
699
1262
  const { done, value } = await reader.read(); if (done) break;
700
1263
  buf += dec.decode(value, { stream: true });
@@ -703,13 +1266,14 @@ async function once(messages, onText, onThink) {
703
1266
  if (!line.startsWith("data:")) continue;
704
1267
  let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
705
1268
  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); }
1269
+ if (p.type === "mythos_task") currentTaskId = p.task_id;
1270
+ if (p.type === "message_delta" && p.usage) measured = p.usage;
708
1271
  if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
709
1272
  }
710
1273
  }
711
1274
  if (!out.trim()) throw new Error("Leere Antwort");
712
- tokens.out += Math.ceil(out.length / 4);
1275
+ if (measured?.measured) { tokens.in += measured.input_tokens || 0; tokens.out += measured.output_tokens || 0; }
1276
+ else tokens.unknown = (tokens.unknown || 0) + 1;
713
1277
  return out;
714
1278
  } finally { clearTimeout(to); }
715
1279
  }
@@ -757,6 +1321,25 @@ function runLive(cmd) {
757
1321
  });
758
1322
  }
759
1323
 
1324
+ // start/logs/stop/serve/verify/check – check zusätzlich mit echtem Browser (Chrome/Edge headless), falls installiert.
1325
+ async function cliDevTool(t) {
1326
+ stopTick();
1327
+ const live = t.name === "start" || t.name === "verify" ? (s) => process.stdout.write(C.d + s + C.x) : null;
1328
+ const r = await devTool(t, ROOT, { onChunk: live, track: (p) => { child = p; } });
1329
+ child = null;
1330
+ let text = r.result, image = null;
1331
+ if (t.name === "check" && wantsRender(r)) {
1332
+ say(C.d + " 🌐 Browser-Test …" + C.x);
1333
+ const h = await headlessRender(r.url).catch(() => null);
1334
+ if (h && h.ok) { text = checkReport(r.url, r.http, renderVerdict(h)); image = h.image; }
1335
+ else text = checkReport(r.url, r.http, null, "ℹ Kein Chrome/Edge gefunden – nur HTTP-Prüfung (Pfad mit MYTHOS_CHROME setzen).");
1336
+ }
1337
+ startTick();
1338
+ const show = text.split("\n").filter((l) => /^[✓✗⚠ℹ]|^ERGEBNIS|^Prüfbericht|^Seitenprüfung|^Läuft|^Befehl|^FEHLER/.test(l)).slice(0, 14);
1339
+ say(show.map((l) => (/✗|FEHLER/.test(l) ? C.r : /✓/.test(l) ? C.g : C.d) + " " + l + C.x).join("\n"));
1340
+ return image ? { text, image } : text;
1341
+ }
1342
+
760
1343
  // Mythos Cloud: Backend + KI-Gateway für die gebaute App verbinden und mythos-cloud.js anlegen.
761
1344
  async function cloudConnect(t) {
762
1345
  try {
@@ -778,7 +1361,14 @@ async function runTool(t) {
778
1361
  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
1362
  return todoSummary(items);
780
1363
  }
1364
+ // Server & Watcher enden nie → automatisch im Hintergrund starten, statt die Aufgabe zu blockieren.
1365
+ 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
1366
  const label = t.name === "run" ? "run " + t.cmd
1367
+ : t.name === "start" ? "start (Hintergrund) " + t.cmd
1368
+ : t.name === "serve" ? "serve " + (t.path || ".")
1369
+ : t.name === "check" ? "check " + (t.url || t.path || "")
1370
+ : t.name === "verify" ? "verify (Build, Typecheck, Tests …)"
1371
+ : t.name === "logs" || t.name === "stop" ? t.name + " " + (t.id || "")
782
1372
  : t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
783
1373
  : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
784
1374
  : t.name === "websearch" ? "websearch " + t.query
@@ -787,7 +1377,7 @@ async function runTool(t) {
787
1377
  : t.name + " " + (t.path || "");
788
1378
  say(C.y + "⚙ " + label + C.x);
789
1379
  // 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;
1380
+ const why = t.name === "run" || t.name === "start" ? dangerCheck(t.cmd) : null;
791
1381
  if (why) {
792
1382
  say(C.r + "🛡 Sicherheitsnetz: dieser Befehl " + why + "." + C.x);
793
1383
  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 +1386,7 @@ async function runTool(t) {
796
1386
  startTick();
797
1387
  if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
798
1388
  }
799
- if ((t.name === "run" || t.name === "write" || t.name === "edit" || t.name === "cloud") && !autoYes && !why) {
1389
+ if (["run", "start", "verify", "write", "edit", "cloud"].includes(t.name) && !autoYes && !why) {
800
1390
  stopTick();
801
1391
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
802
1392
  startTick();
@@ -804,6 +1394,7 @@ async function runTool(t) {
804
1394
  }
805
1395
  if (t.name === "run") return runLive(t.cmd);
806
1396
  if (t.name === "cloud") return cloudConnect(t);
1397
+ if (isDevTool(t)) return cliDevTool(t);
807
1398
  if (isWebTool(t)) {
808
1399
  const r = await webTool(t);
809
1400
  say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
@@ -816,8 +1407,10 @@ async function runTool(t) {
816
1407
 
817
1408
  // ---------- Eine Aufgabe (auch /goal) ----------
818
1409
  async function turnInner(history) {
819
- const max = goal ? 4000 : 40;
1410
+ const max = goal ? 4000 : 60;
820
1411
  let nudges = 0;
1412
+ const tracker = verifyTracker();
1413
+ const question = String((history.filter((m) => m.role === "user").pop() || {}).content || "");
821
1414
  for (let i = 0; i < max && !stopped; i++) {
822
1415
  const pr = streamPrinter();
823
1416
  let think = null;
@@ -831,6 +1424,8 @@ async function turnInner(history) {
831
1424
  history.push({ role: "assistant", content: out });
832
1425
  const pb = parseToolBlock(out);
833
1426
  if (!pb.tool && !pb.error) {
1427
+ const nudge = tracker.nudge(out, question);
1428
+ if (nudge) { say(C.d + "🔎 Mythos prüft seine Arbeit …" + C.x); history.push({ role: "user", content: nudge }); continue; }
834
1429
  if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
835
1430
  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
1431
  history.push({ role: "user", content: GOAL_NUDGE });
@@ -840,8 +1435,11 @@ async function turnInner(history) {
840
1435
  if (pb.error) { history.push({ role: "user", content: pb.error + " Sende den Block vollständig als gültiges JSON in <tool>…</tool>." }); continue; }
841
1436
  const t = pb.tool, bad = toolProblem(t);
842
1437
  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 });
1438
+ const res = await runTool(t), resText = typeof res === "string" ? res : res.text;
1439
+ tracker.note(t, resText);
1440
+ // Screenshot nur in der neuesten Prüfung mitschicken (spart Tokens).
1441
+ if (res.image) history.forEach((m) => { if (m.shot) { delete m.images; delete m.shot; } });
1442
+ 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
1443
  }
846
1444
  if (!stopped) say(C.y + "Schrittlimit (" + max + ") erreicht – mit /weiter macht Mythos weiter." + C.x);
847
1445
  return stopped ? "stopped" : "limit";
@@ -862,6 +1460,7 @@ async function summarize(history) {
862
1460
  }
863
1461
 
864
1462
  async function turn(history, input, images) {
1463
+ currentTaskId = null;
865
1464
  if (input != null) history.push({ role: "user", content: input, images });
866
1465
  T0 = Date.now(); working = true; stopped = false; startTick();
867
1466
  const tok0 = tokens.in + tokens.out;
@@ -875,7 +1474,7 @@ async function turn(history, input, images) {
875
1474
  working = false; stopTick();
876
1475
  const took = fmt(Date.now() - T0);
877
1476
  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");
1477
+ 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
1478
  if (!stopped && status !== "error") await maybeAd();
880
1479
  if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
881
1480
  // Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
@@ -941,7 +1540,7 @@ const HELP = [
941
1540
  ["/handy <url>", "Handy-Benachrichtigung (ntfy.sh-Thema oder Discord-Webhook) · /handy test · /handy aus"],
942
1541
  ["/vollzugriff an|aus", "Ohne Nachfragen arbeiten"],
943
1542
  ["/modell [name]", "Modell anzeigen oder wechseln"],
944
- ["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
1543
+ ["/tokens", "Gemessenen MythosAI-Verbrauch dieser Sitzung anzeigen"],
945
1544
  ["/clear", "Verlauf leeren"],
946
1545
  ["/review [schwerpunkt]", "Code-Review der Git-Änderungen (ändert nichts)"],
947
1546
  ["/pr [branch]", "Branch pushen und Pull Request erstellen"],
@@ -1024,7 +1623,7 @@ async function command(q, history) {
1024
1623
  cfg.model = m.id; save(cfg);
1025
1624
  return console.log(C.g + "✓ Modell: " + m.label + C.x + "\n");
1026
1625
  }
1027
- if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
1626
+ 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
1627
  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
1628
  if (cmd === "/review") {
1030
1629
  const st = git(["status", "--porcelain"]);
@@ -1068,13 +1667,15 @@ async function command(q, history) {
1068
1667
  }
1069
1668
 
1070
1669
  async function main() {
1071
- const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--key");
1670
+ const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--key" && a !== "--no-update");
1072
1671
  if (args[0] === "login" || !cfg.key) await login();
1073
1672
  if (args[0] === "login") process.exit(0);
1074
1673
  if (args[0] === "logout") { save({}); console.log("Abgemeldet."); process.exit(0); }
1674
+ if (args[0] === "update") { await autoUpdate(true); process.exit(0); }
1675
+ if (args[0] === "--version" || args[0] === "-v") { console.log(VERSION); process.exit(0); }
1676
+ await autoUpdate();
1075
1677
  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);
1678
+ banner({ dir: "📁 " + path.basename(ROOT) + (branch ? " ⎇ " + branch : ""), extra: (cfg.name ? "angemeldet als " + cfg.name : "by Mythoscraft") + (readMemory(ROOT) ? " · Gedächtnis ✓" : "") + " · Strg+C stoppt" });
1078
1679
  const history = [];
1079
1680
  if (args.length) { const { text, images } = extractImages(args.join(" ")); await turn(history, text, images); }
1080
1681
  while (true) {
@@ -1089,5 +1690,7 @@ async function main() {
1089
1690
  await turn(history, text + files.map((f) => "\n\n### " + f.name + "\n```\n" + f.content + "\n```").join(""), images);
1090
1691
  }
1091
1692
  rl.close();
1693
+ process.exit(0); // beendet auch Hintergrund-Server (process.on("exit") in den Werkzeugen)
1092
1694
  }
1695
+ for (const sig of ["SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
1093
1696
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.7.8",
3
+ "version": "1.9.0",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",