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