mythos-code 1.7.3 → 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 +770 -29
  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,15 +4,175 @@ 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) ----------
10
11
  // Wird roh in bin/mythos.js (CLI, ESM) und main.js (App, CommonJS) eingefügt.
11
12
  // Erwartet, dass `fs` und `path` im umgebenden Code schon importiert sind.
12
13
 
14
+ // ---------- Werkzeug-Blöcke robust lesen (CLI, Desktop-App: Haupt- und Fensterprozess) ----------
15
+ // Wird roh über den Platzhalter TOOL_PARSE eingefügt. Keine Abhängigkeiten.
16
+ // Modelle schreiben Aufrufe nicht immer sauber: fehlendes </tool>, <tool_call> statt <tool>, ```json-Zäune,
17
+ // {"name":…,"arguments":{…}} statt flacher Felder, echte Zeilenumbrüche in Strings oder abgeschnittenes JSON.
18
+ // Nichts davon darf als Rohtext im Chat landen.
19
+
20
+ const TOOL_NAMES = ["run", "start", "logs", "stop", "serve", "check", "verify", "read", "edit", "write", "search", "ls", "websearch", "fetch", "cloud", "todo"];
21
+ const TOOL_OPEN_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>/i;
22
+ // Ein ganzer Block – auch nicht geschlossen (dann bis zum Textende).
23
+ const TOOL_BLOCK_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>[\s\S]*?(?:<\/\1\s*>|$)/gi;
24
+
25
+ /** Alle Werkzeug-Blöcke und verwaiste End-Tags entfernen. */
26
+ function stripToolBlocks(text) {
27
+ return String(text || "").replace(TOOL_BLOCK_RE, "").replace(/<\/(?:tool_call|tool_use|tool)\s*>/gi, "");
28
+ }
29
+
30
+ /** Erstes JSON-Objekt im Text herausschneiden. Rohe Zeilenumbrüche in Strings werden escaped,
31
+ * abgeschnittenes JSON wird geschlossen. → { json, complete } oder null. */
32
+ function jsonObjectAt(s) {
33
+ const start = s.indexOf("{");
34
+ if (start < 0) return null;
35
+ let out = "", inStr = false, esc = false;
36
+ const stack = [];
37
+ for (let i = start; i < s.length; i++) {
38
+ const ch = s[i];
39
+ if (inStr) {
40
+ if (esc) { esc = false; out += ch; continue; }
41
+ if (ch === "\\") { esc = true; out += ch; continue; }
42
+ if (ch === '"') { inStr = false; out += ch; continue; }
43
+ out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : ch === "\t" ? "\\t" : ch;
44
+ continue;
45
+ }
46
+ if (ch === '"') { inStr = true; out += ch; continue; }
47
+ if (ch === "{" || ch === "[") stack.push(ch === "{" ? "}" : "]");
48
+ else if (ch === "}" || ch === "]") { stack.pop(); out += ch; if (!stack.length) return { json: out, complete: true }; continue; }
49
+ out += ch;
50
+ }
51
+ // Abgeschnitten: offenen String und Klammern schließen, hängendes Komma/Doppelpunkt weg.
52
+ if (esc) out = out.slice(0, -1);
53
+ if (inStr) out += '"';
54
+ out = out.replace(/[,:]\s*$/, "");
55
+ while (stack.length) out += stack.pop();
56
+ return { json: out, complete: false };
57
+ }
58
+
59
+ /** Ersten Werkzeug-Aufruf aus einer Antwort lesen.
60
+ * → { tool: {name, …} | null, error: string | "", text: sichtbarer Text ohne Werkzeug-Blöcke } */
61
+ function parseToolBlock(out) {
62
+ out = String(out || "");
63
+ const text = stripToolBlocks(out).trim();
64
+ const m = TOOL_OPEN_RE.exec(out);
65
+ if (!m) return { tool: null, error: "", text };
66
+ let body = out.slice(m.index + m[0].length);
67
+ const end = body.search(/<\/(?:tool_call|tool_use|tool)\s*>/i);
68
+ if (end >= 0) body = body.slice(0, end);
69
+ body = body.replace(/```[a-z]*\s*/gi, "");
70
+ const raw = jsonObjectAt(body);
71
+ if (!raw) return { tool: null, error: "Im Werkzeug-Block steht kein JSON-Objekt.", text };
72
+ let t;
73
+ try { t = JSON.parse(raw.json); } catch (e) { return { tool: null, error: "Werkzeug-JSON ungültig (" + e.message + ").", text }; }
74
+ if (!t || typeof t !== "object" || Array.isArray(t)) return { tool: null, error: "Werkzeug-JSON muss ein Objekt sein.", text };
75
+ // Name auch aus <tool name="…"> oder anderen üblichen Feldern.
76
+ const attr = m[0].match(/\bname\s*=\s*["']([^"']+)["']/i);
77
+ if (typeof t.name !== "string") t.name = String(t.tool_name || t.function || (attr && attr[1]) || "");
78
+ // {"name":"x","arguments":{…}} / "input" / "parameters" / "args" flach machen (bei MCP: nach "args").
79
+ for (const k of ["arguments", "input", "parameters", "args"]) {
80
+ const a = t[k];
81
+ if (!a || typeof a !== "object" || Array.isArray(a)) continue;
82
+ if (t.name === "mcp") { if (k !== "args") { delete t[k]; if (!t.args) t.args = a; } continue; }
83
+ delete t[k]; for (const x of Object.keys(a)) if (!(x in t)) t[x] = a[x];
84
+ }
85
+ // mcp__server__tool (Hook-Schreibweise) → MCP-Aufruf.
86
+ const mm = t.name.match(/^mcp__(.+?)__(.+)$/);
87
+ if (mm) { const { name, ...rest } = t; t = { name: "mcp", server: mm[1], tool: mm[2], args: rest.args || rest }; }
88
+ t.name = t.name.trim();
89
+ if (!t.name) return { tool: null, error: "Im Werkzeug-Block fehlt \"name\".", text };
90
+ // Abgeschnittene Aufrufe nicht ausführen (sonst z. B. halbe Dateien) – außer harmlose wie todo.
91
+ if (!raw.complete && t.name !== "todo") return { tool: null, error: "Der Werkzeug-Aufruf " + t.name + " ist abgeschnitten.", text };
92
+ return { tool: t, error: "", text };
93
+ }
94
+
95
+ /** Prüfen, ob es das Werkzeug wirklich gibt. extra = { mcp: [{ server, tools: [Namen] }], browser: bool }.
96
+ * → "" wenn ok, sonst eine Erklärung fürs Modell (mit der Liste, was es gibt). */
97
+ function toolProblem(t, extra) {
98
+ extra = extra || {};
99
+ const mcp = extra.mcp || [];
100
+ const avail = TOOL_NAMES.concat(mcp.length ? ["mcp"] : [], extra.browser ? ["browser"] : []);
101
+ const list = "Verfügbar sind nur: " + avail.join(", ") + (mcp.length ? " · MCP: " + mcp.map((s) => s.server + " (" + s.tools.join(", ") + ")").join("; ") : " · keine MCP-Connectoren verbunden") + ".";
102
+ const tail = " Erfinde keine Werkzeuge. Wenn etwas damit nicht geht, sag das dem Nutzer kurz und mach mit dem Rest weiter.";
103
+ if (t.name === "mcp") {
104
+ const s = mcp.find((x) => x.server === t.server) || mcp.find((x) => String(x.server).toLowerCase() === String(t.server || "").toLowerCase());
105
+ if (!s) return "FEHLER: MCP-Server „" + (t.server || "?") + "“ ist nicht verbunden. " + list + tail;
106
+ t.server = s.server;
107
+ if (!s.tools.includes(t.tool)) return "FEHLER: MCP-Werkzeug „" + t.tool + "“ gibt es bei " + s.server + " nicht. " + list + tail;
108
+ return "";
109
+ }
110
+ if (t.name === "browser") return extra.browser ? "" : "FEHLER: Das Browser-Plugin ist nicht verbunden. " + list + tail;
111
+ return TOOL_NAMES.includes(t.name) ? "" : "FEHLER: Werkzeug „" + t.name + "“ gibt es nicht. " + list + tail;
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
+
166
+
13
167
  const TOOL_PROMPT = [
14
168
  "Werkzeuge – antworte pro Schritt mit GENAU EINEM Block:",
15
- '<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.',
16
176
  '<tool>{"name":"read","path":"..."}</tool> Datei lesen',
17
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)',
18
178
  '<tool>{"name":"write","path":"...","content":"..."}</tool> Neue Datei anlegen oder komplett neu schreiben',
@@ -20,9 +180,36 @@ const TOOL_PROMPT = [
20
180
  '<tool>{"name":"ls","path":"."}</tool> Ordner auflisten',
21
181
  '<tool>{"name":"websearch","query":"..."}</tool> Im Internet suchen (z. B. Doku, Fehlermeldungen)',
22
182
  '<tool>{"name":"fetch","url":"https://..."}</tool> Webseite als Text lesen',
183
+ '<tool>{"name":"cloud","app":"Name der App"}</tool> Mythos Cloud verbinden: eigenes Backend (Datenbank) + KI-Gateway (Chat, Streaming, Bilder) für die App, die du baust. Nutze das IMMER, sobald die App Daten speichern oder KI nutzen soll – statt Supabase/Firebase/OpenAI-Keys o. Ä. Legt mythos-cloud.js im Projekt an und erklärt die Nutzung.',
23
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)',
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>.",
24
186
  "Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
25
- "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.",
26
213
  ].join("\n");
27
214
 
28
215
  // Modelle, die v1-messages versteht (siehe mapModel dort).
@@ -71,7 +258,7 @@ function streamActivity(full) {
71
258
  return "🔧 bereitet " + (name || "Werkzeug") + " vor…";
72
259
  }
73
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.";
74
- 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.";
75
262
 
76
263
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
77
264
 
@@ -409,6 +596,427 @@ async function pushNotify(url, title, message) {
409
596
  } catch { return false; }
410
597
  }
411
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
+
412
1020
 
413
1021
  const API = process.env.MYTHOS_API_URL || "https://rdxaqiacoitzeowehdts.supabase.co/functions/v1/v1-messages";
414
1022
  const CFG = path.join(os.homedir(), ".mythos-code.json");
@@ -426,7 +1034,65 @@ let goal = "";
426
1034
  const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
427
1035
  TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
428
1036
 
429
- const SITE = process.env.MYTHOS_SITE_URL || "https://ai-mythos.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
+ }
430
1096
  const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
431
1097
  const authPost = async (body) => (await fetch(FN + "/cli-auth", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
432
1098
 
@@ -476,6 +1142,23 @@ const tickLine = () => {
476
1142
  };
477
1143
  const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r\x1b[K" + C.d + tickLine() + C.x), 250); };
478
1144
  const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
1145
+ let adN = 0, adTier = null, adAdminDay = "";
1146
+ async function maybeAd() {
1147
+ try {
1148
+ if (adTier == null) { const j = await (await fetch(API.replace(/\/v1-messages.*$/, "") + "/cli-auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "usage", api_key: cfg.key }) })).json(); adTier = j.admin ? "admin" : (j.tier || "free"); }
1149
+ let due = false; const today = new Date().toDateString();
1150
+ if (adTier === "admin") { due = adAdminDay !== today; adAdminDay = today; }
1151
+ else { const every = adTier === "free" ? 4 : adTier === "light" ? 10 : 0; due = every > 0 && (++adN % every === 0); }
1152
+ if (due) {
1153
+ const fnBase = API.replace(/\/v1-messages.*$/, "");
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 {}
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];
1156
+ for (const a of pool) { r -= Math.max(1, a.weight); if (r <= 0) { ad = a; break; } }
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; }
1158
+ }
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");
1160
+ } catch {}
1161
+ }
479
1162
  const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
480
1163
 
481
1164
  // ---------- Stoppen mit Strg+C ----------
@@ -523,9 +1206,9 @@ function streamPrinter() {
523
1206
  return {
524
1207
  push(full) {
525
1208
  let vis = full;
526
- const k = vis.indexOf("<tool>");
1209
+ const k = vis.search(/<tool(?:_call|_use)?[\s>]/i);
527
1210
  if (k >= 0) vis = vis.slice(0, k);
528
- else for (let n = 5; n > 0; n--) if (vis.endsWith("<tool>".slice(0, n))) { vis = vis.slice(0, -n); break; }
1211
+ else { const p = vis.match(/<(?:t(?:o(?:o(?:l(?:_(?:c(?:a(?:l(?:l)?)?)?|u(?:s(?:e)?)?)?)?)?)?)?)?$/i); if (p) vis = vis.slice(0, p.index); }
529
1212
  if (vis.length <= printed) return;
530
1213
  buf += vis.slice(printed); printed = vis.length;
531
1214
  let i; while ((i = buf.indexOf("\n")) !== -1) { line(buf.slice(0, i)); buf = buf.slice(i + 1); }
@@ -563,18 +1246,18 @@ function compact(history, level) {
563
1246
  }
564
1247
 
565
1248
  let tokens = { in: 0, out: 0 };
1249
+ let currentTaskId = null;
566
1250
  async function once(messages, onText, onThink) {
567
1251
  const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
568
1252
  const system = SYSTEM();
569
- tokens.in += estimateTokens(system, messages);
570
1253
  try {
571
1254
  const r = await fetch(API, {
572
1255
  method: "POST", signal: c.signal,
573
1256
  headers: { "content-type": "application/json", "x-api-key": cfg.key, "anthropic-version": "2023-06-01" },
574
- 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 }),
575
1258
  });
576
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; }
577
- 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;
578
1261
  while (true) {
579
1262
  const { done, value } = await reader.read(); if (done) break;
580
1263
  buf += dec.decode(value, { stream: true });
@@ -583,13 +1266,14 @@ async function once(messages, onText, onThink) {
583
1266
  if (!line.startsWith("data:")) continue;
584
1267
  let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
585
1268
  if (p.type === "error") { const e = new Error(p.error?.message || "Überlastet"); e.status = 529; throw e; }
586
- // Sichtbare Gedanken: kommen als eigener Block VOR der Antwort.
587
- 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;
588
1271
  if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
589
1272
  }
590
1273
  }
591
1274
  if (!out.trim()) throw new Error("Leere Antwort");
592
- 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;
593
1277
  return out;
594
1278
  } finally { clearTimeout(to); }
595
1279
  }
@@ -637,6 +1321,38 @@ function runLive(cmd) {
637
1321
  });
638
1322
  }
639
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
+
1343
+ // Mythos Cloud: Backend + KI-Gateway für die gebaute App verbinden und mythos-cloud.js anlegen.
1344
+ async function cloudConnect(t) {
1345
+ try {
1346
+ const r = await fetch(FN + "/app-cloud", { method: "POST", headers: { "content-type": "application/json" },
1347
+ body: JSON.stringify({ action: "connect", api_key: cfg.key, name: String(t.app || path.basename(ROOT)) }) });
1348
+ const j = await r.json().catch(() => ({}));
1349
+ if (!r.ok || !j.client_js) return "FEHLER: Mythos Cloud: " + (j.error || "HTTP " + r.status);
1350
+ const res = fileTool({ name: "write", path: (t.path = t.path || j.file), content: j.client_js }, ROOT);
1351
+ say(C.g + " ☁ Mythos Cloud verbunden (" + j.app + ")" + C.x);
1352
+ return res.result + "\n\n" + j.guide;
1353
+ } catch (e) { return "FEHLER: Mythos Cloud nicht erreichbar: " + e.message; }
1354
+ }
1355
+
640
1356
  async function runTool(t) {
641
1357
  if (t.name === "todo") {
642
1358
  const items = normalizeTodos(t);
@@ -645,15 +1361,23 @@ async function runTool(t) {
645
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"));
646
1362
  return todoSummary(items);
647
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 }; }
648
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 || "")
649
1372
  : t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
650
1373
  : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
651
1374
  : t.name === "websearch" ? "websearch " + t.query
652
1375
  : t.name === "fetch" ? "fetch " + t.url
1376
+ : t.name === "cloud" ? "cloud verbinden: " + (t.app || "App")
653
1377
  : t.name + " " + (t.path || "");
654
1378
  say(C.y + "⚙ " + label + C.x);
655
1379
  // Sicherheitsnetz: gefährliche Befehle fragen immer nach (auch mit --yes) und werden bei /goal blockiert.
656
- const why = t.name === "run" ? dangerCheck(t.cmd) : null;
1380
+ const why = t.name === "run" || t.name === "start" ? dangerCheck(t.cmd) : null;
657
1381
  if (why) {
658
1382
  say(C.r + "🛡 Sicherheitsnetz: dieser Befehl " + why + "." + C.x);
659
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.";
@@ -662,13 +1386,15 @@ async function runTool(t) {
662
1386
  startTick();
663
1387
  if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
664
1388
  }
665
- if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes && !why) {
1389
+ if (["run", "start", "verify", "write", "edit", "cloud"].includes(t.name) && !autoYes && !why) {
666
1390
  stopTick();
667
1391
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
668
1392
  startTick();
669
1393
  if (a === "a") autoYes = true; else if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt.";
670
1394
  }
671
1395
  if (t.name === "run") return runLive(t.cmd);
1396
+ if (t.name === "cloud") return cloudConnect(t);
1397
+ if (isDevTool(t)) return cliDevTool(t);
672
1398
  if (isWebTool(t)) {
673
1399
  const r = await webTool(t);
674
1400
  say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
@@ -681,8 +1407,10 @@ async function runTool(t) {
681
1407
 
682
1408
  // ---------- Eine Aufgabe (auch /goal) ----------
683
1409
  async function turnInner(history) {
684
- const max = goal ? 4000 : 40;
1410
+ const max = goal ? 4000 : 60;
685
1411
  let nudges = 0;
1412
+ const tracker = verifyTracker();
1413
+ const question = String((history.filter((m) => m.role === "user").pop() || {}).content || "");
686
1414
  for (let i = 0; i < max && !stopped; i++) {
687
1415
  const pr = streamPrinter();
688
1416
  let think = null;
@@ -694,17 +1422,24 @@ async function turnInner(history) {
694
1422
  pr.end(); activity = "";
695
1423
  if (stopped) break;
696
1424
  history.push({ role: "assistant", content: out });
697
- const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
698
- if (!m) {
1425
+ const pb = parseToolBlock(out);
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; }
699
1429
  if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
700
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"; }
701
1431
  history.push({ role: "user", content: GOAL_NUDGE });
702
1432
  continue;
703
1433
  }
704
1434
  nudges = 0;
705
- let t; try { t = JSON.parse(m[1]); } catch { history.push({ role: "user", content: "Tool-JSON ungültig." }); continue; }
706
- const res = await runTool(t);
707
- history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + res });
1435
+ if (pb.error) { history.push({ role: "user", content: pb.error + " Sende den Block vollständig als gültiges JSON in <tool>…</tool>." }); continue; }
1436
+ const t = pb.tool, bad = toolProblem(t);
1437
+ if (bad) { history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + bad }); continue; }
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 });
708
1443
  }
709
1444
  if (!stopped) say(C.y + "Schrittlimit (" + max + ") erreicht – mit /weiter macht Mythos weiter." + C.x);
710
1445
  return stopped ? "stopped" : "limit";
@@ -725,6 +1460,7 @@ async function summarize(history) {
725
1460
  }
726
1461
 
727
1462
  async function turn(history, input, images) {
1463
+ currentTaskId = null;
728
1464
  if (input != null) history.push({ role: "user", content: input, images });
729
1465
  T0 = Date.now(); working = true; stopped = false; startTick();
730
1466
  const tok0 = tokens.in + tokens.out;
@@ -738,7 +1474,8 @@ async function turn(history, input, images) {
738
1474
  working = false; stopTick();
739
1475
  const took = fmt(Date.now() - T0);
740
1476
  console.log((stopped ? C.r + "■ Gestoppt nach " + took : C.g + "✓ Mythos hat " + took + " gearbeitet") + C.x +
741
- 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");
1478
+ if (!stopped && status !== "error") await maybeAd();
742
1479
  if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
743
1480
  // Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
744
1481
  if (cfg.notify && (wasGoal || Date.now() - T0 > 60000)) {
@@ -764,7 +1501,7 @@ async function commit(msg) {
764
1501
  stopped = false;
765
1502
  try {
766
1503
  msg = (await call([{ role: "user", content: "Schreibe eine kurze Git-Commit-Nachricht auf Deutsch für diese Änderungen: erste Zeile max. 72 Zeichen, optional Leerzeile + Stichpunkte. Antworte NUR mit der Nachricht, ohne Werkzeuge, ohne Codeblock.\n\n" + st.out + "\n" + diff.slice(0, 20000) }]))
767
- .replace(/<tool>[\s\S]*?<\/tool>/g, "").replace(/^```\w*\n?|```$/g, "").trim();
1504
+ .replace(TOOL_BLOCK_RE, "").replace(/^```\w*\n?|```$/g, "").trim();
768
1505
  } catch (e) { clearLine(); return say(C.r + "⚠ " + e.message + C.x); }
769
1506
  clearLine();
770
1507
  }
@@ -803,7 +1540,7 @@ const HELP = [
803
1540
  ["/handy <url>", "Handy-Benachrichtigung (ntfy.sh-Thema oder Discord-Webhook) · /handy test · /handy aus"],
804
1541
  ["/vollzugriff an|aus", "Ohne Nachfragen arbeiten"],
805
1542
  ["/modell [name]", "Modell anzeigen oder wechseln"],
806
- ["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
1543
+ ["/tokens", "Gemessenen MythosAI-Verbrauch dieser Sitzung anzeigen"],
807
1544
  ["/clear", "Verlauf leeren"],
808
1545
  ["/review [schwerpunkt]", "Code-Review der Git-Änderungen (ändert nichts)"],
809
1546
  ["/pr [branch]", "Branch pushen und Pull Request erstellen"],
@@ -886,7 +1623,7 @@ async function command(q, history) {
886
1623
  cfg.model = m.id; save(cfg);
887
1624
  return console.log(C.g + "✓ Modell: " + m.label + C.x + "\n");
888
1625
  }
889
- 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");
890
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"); }
891
1628
  if (cmd === "/review") {
892
1629
  const st = git(["status", "--porcelain"]);
@@ -906,7 +1643,7 @@ async function command(q, history) {
906
1643
  let sum = "";
907
1644
  try {
908
1645
  sum = (await call([{ role: "user", content: "Fasse den bisherigen Verlauf dieses Coding-Chats so zusammen, dass ein Agent nahtlos weiterarbeiten kann: **Ziel**, **Stand**, **Wichtige Dateien & Entscheidungen**, **Offene Punkte**. Maximal 25 Zeilen, ohne Werkzeuge.\n\nVERLAUF:\n" + transcript }]))
909
- .replace(/<tool>[\s\S]*?<\/tool>/g, "").trim();
1646
+ .replace(TOOL_BLOCK_RE, "").trim();
910
1647
  } catch (e) { clearLine(); return console.log(C.r + "⚠ " + e.message + C.x + "\n"); }
911
1648
  clearLine();
912
1649
  history.length = 0;
@@ -916,7 +1653,7 @@ async function command(q, history) {
916
1653
  if (cmd === "/stats") { const b = gitBranch(); return console.log(mdLines(projectStats(ROOT) + (b ? "\n**Git:** Branch `" + b + "`" : "")) + "\n"); }
917
1654
  if (cmd === "/export") {
918
1655
  const file = path.resolve(ROOT, arg || "mythos-chat-" + new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-") + ".md");
919
- const msgs = history.map((m) => ({ role: /^Werkzeug-Ergebnis/.test(m.content) ? "tool" : m.role, text: String(m.content).replace(/<tool>[\s\S]*?<\/tool>/g, "").slice(0, 20000) })).filter((m) => m.text.trim());
1656
+ const msgs = history.map((m) => ({ role: /^Werkzeug-Ergebnis/.test(m.content) ? "tool" : m.role, text: String(m.content).replace(TOOL_BLOCK_RE, "").slice(0, 20000) })).filter((m) => m.text.trim());
920
1657
  fs.writeFileSync(file, chatToMarkdown("Mythos-Chat – " + path.basename(ROOT), msgs));
921
1658
  return console.log(C.g + "✓ Gespeichert: " + file + C.x + "\n");
922
1659
  }
@@ -930,13 +1667,15 @@ async function command(q, history) {
930
1667
  }
931
1668
 
932
1669
  async function main() {
933
- 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");
934
1671
  if (args[0] === "login" || !cfg.key) await login();
935
1672
  if (args[0] === "login") process.exit(0);
936
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();
937
1677
  const branch = gitBranch();
938
- console.log(C.c + C.b + "\n ✦ MYTHOS CODE" + C.x + C.d + " – by Mythoscraft · " + path.basename(ROOT) + (branch ? " ⎇ " + branch : "") +
939
- (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" });
940
1679
  const history = [];
941
1680
  if (args.length) { const { text, images } = extractImages(args.join(" ")); await turn(history, text, images); }
942
1681
  while (true) {
@@ -951,5 +1690,7 @@ async function main() {
951
1690
  await turn(history, text + files.map((f) => "\n\n### " + f.name + "\n```\n" + f.content + "\n```").join(""), images);
952
1691
  }
953
1692
  rl.close();
1693
+ process.exit(0); // beendet auch Hintergrund-Server (process.on("exit") in den Werkzeugen)
954
1694
  }
1695
+ for (const sig of ["SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
955
1696
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.7.3",
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",