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