mythos-code 1.5.0 → 1.7.3
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/bin/mythos.js +292 -16
- package/package.json +1 -1
package/bin/mythos.js
CHANGED
|
@@ -20,6 +20,7 @@ const TOOL_PROMPT = [
|
|
|
20
20
|
'<tool>{"name":"ls","path":"."}</tool> Ordner auflisten',
|
|
21
21
|
'<tool>{"name":"websearch","query":"..."}</tool> Im Internet suchen (z. B. Doku, Fehlermeldungen)',
|
|
22
22
|
'<tool>{"name":"fetch","url":"https://..."}</tool> Webseite als Text lesen',
|
|
23
|
+
'<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)',
|
|
23
24
|
"Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
|
|
24
25
|
"Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool> (Markdown erlaubt).",
|
|
25
26
|
].join("\n");
|
|
@@ -47,9 +48,29 @@ const memoryPrompt = (mem) => mem
|
|
|
47
48
|
? "\n\nProjekt-Gedächtnis (" + MEMORY_FILE + " im Projektordner – halte dich daran, ergänze es bei wichtigen neuen Erkenntnissen per edit):\n" + mem
|
|
48
49
|
: "";
|
|
49
50
|
const goalPrompt = (goal) => goal
|
|
50
|
-
? "\n\nZIEL (/goal): " + goal +
|
|
51
|
+
? "\n\nZIEL (/goal): " + goal +
|
|
52
|
+
"\nDer Auftraggeber ist NICHT am Rechner und liest erst viel später mit, was hier im Chat passiert ist – frage ihn nichts, er kann nicht antworten. Behandle jede Entscheidung, jede Rückfrage und jedes Hindernis als etwas, das du selbst lösen musst:" +
|
|
53
|
+
"\n- Gib bei Fehlern NIEMALS auf. Ein fehlgeschlagener Befehl, eine fehlende Abhängigkeit, ein kaputter Build sind Aufgaben, keine Endstationen: lies die Fehlermeldung genau, versuche eine andere Vorgehensweise, installiere Fehlendes selbst (z. B. per npm/pip/winget), nutze bei Bedarf websearch/fetch um die Lösung nachzuschlagen, und probiere es erneut. Erst wenn du wirklich mehrere grundverschiedene Ansätze erfolglos versucht hast, notierst du das Problem knapp und machst mit dem Rest der Aufgabe weiter." +
|
|
54
|
+
"\n- Triff sinnvolle Annahmen statt zu fragen, und schreib kurz dazu, welche Annahme du getroffen hast." +
|
|
55
|
+
"\n- Beginne SOFORT mit einer kurzen Bestätigung und deinem Plan (2–6 Stichpunkte) als normalen Text, BEVOR du das erste Werkzeug nutzt – so sieht der Auftraggeber direkt, dass du das Ziel verstanden hast." +
|
|
56
|
+
"\n- Teile deinen Fortschritt normal im Chat mit (was du tust und warum), aber warte nicht auf eine Antwort. Schreibe vor jedem größeren Schritt einen kurzen Satz dazu." +
|
|
57
|
+
"\n- Baue große Projekte in vielen kleinen Dateien (je höchstens ca. 300 Zeilen) statt in einer riesigen Datei auf einmal." +
|
|
58
|
+
"\n- Das ist eine große, langlaufende Aufgabe: Plane für 1 bis 5 Stunden durchgehende Arbeit in vielen kleinen Schritten, nicht für ein paar Minuten. Höre nicht zu früh auf – arbeite lieber zu gründlich als zu knapp." +
|
|
59
|
+
"\nArbeite komplett selbstständig weiter, bis das Ziel vollständig erreicht und überprüft ist. Erst dann schreibe in deiner letzten Antwort eine eigene Zeile: ZIEL ERREICHT"
|
|
51
60
|
: "";
|
|
52
|
-
|
|
61
|
+
/** Kurzer Live-Status aus der gerade gestreamten Antwort, z. B. „✍ schreibt src/main.js · 12 KB“. */
|
|
62
|
+
function streamActivity(full) {
|
|
63
|
+
if (!full) return "denkt nach…";
|
|
64
|
+
const k = full.lastIndexOf("<tool>");
|
|
65
|
+
if (k < 0 || full.indexOf("</tool>", k) >= 0) return "schreibt Antwort…";
|
|
66
|
+
const t = full.slice(k + 6), kb = (t.length / 1024).toFixed(1).replace(".", ",") + " KB";
|
|
67
|
+
const f = (key) => { const m = t.match(new RegExp('"' + key + '"\\s*:\\s*"([^"\\\\]{0,120})')); return m ? m[1] : ""; };
|
|
68
|
+
const name = f("name"), path = f("path");
|
|
69
|
+
if (name === "write" || name === "edit") return "✍ " + (name === "write" ? "schreibt " : "ändert ") + (path || "eine Datei") + " · " + kb;
|
|
70
|
+
if (name === "run") return "⚙ bereitet Befehl vor: " + f("cmd").slice(0, 60);
|
|
71
|
+
return "🔧 bereitet " + (name || "Werkzeug") + " vor…";
|
|
72
|
+
}
|
|
73
|
+
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.";
|
|
53
74
|
const SUMMARY_PROMPT = "Fasse jetzt kurz auf Deutsch zusammen, OHNE Werkzeuge:\n**Geändert:** welche Dateien und was\n**Geklappt:** was funktioniert (und wie geprüft)\n**Offen:** was noch fehlt oder beachtet werden muss\nMaximal 12 Zeilen.";
|
|
54
75
|
|
|
55
76
|
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
|
|
@@ -170,9 +191,46 @@ function searchFiles(root, pattern, glob, max = 200) {
|
|
|
170
191
|
return hits.join("\n") + (hits.length >= max ? "\n… (mehr als " + max + " Treffer – Suche eingrenzen)" : "");
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
// ---------- Sicherheitsnetz für Befehle ----------
|
|
195
|
+
// Diese Befehle fragen IMMER nach (auch mit Vollzugriff) und werden im /goal-Modus blockiert,
|
|
196
|
+
// weil dann niemand da ist, der sie bestätigen könnte.
|
|
197
|
+
const DANGER = [
|
|
198
|
+
[/\brm\s+-[a-z]*(?:rf|fr)[a-z]*\s+(?:--no-preserve-root\s+)?(?:\/|~|\*|\$HOME|%USERPROFILE%)(?:\s|$)/i, "löscht rekursiv ein System- oder Home-Verzeichnis"],
|
|
199
|
+
[/\bformat(?:\.com)?\s+[a-z]:/i, "formatiert ein Laufwerk"],
|
|
200
|
+
[/\b(?:rd|rmdir)\s+\/s\b[^&|;]*\b[a-z]:\\?\s*(?:$|[&|;])/i, "löscht ein ganzes Laufwerk"],
|
|
201
|
+
[/\b(?:del|erase)\b[^&|;]*\/s\b[^&|;]*\b[a-z]:\\\*?\s*(?:$|[&|;])/i, "löscht ein ganzes Laufwerk"],
|
|
202
|
+
[/Remove-Item\b[^|;]*-Recurse[^|;]*\s["']?[a-z]:\\?["']?(?:\s|$|;)/i, "löscht rekursiv ein ganzes Laufwerk"],
|
|
203
|
+
[/\bgit\s+push\b[^;&|]*\s(?:--force(?:-with-lease)?|-f)\b/i, "überschreibt die Git-Historie auf dem Server (Force-Push)"],
|
|
204
|
+
[/\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-z]*f/i, "verwirft lokale Änderungen unwiderruflich"],
|
|
205
|
+
[/\b(?:shutdown|restart-computer|stop-computer)\b/i, "fährt den PC herunter oder startet ihn neu"],
|
|
206
|
+
[/\breg(?:\.exe)?\s+delete\b|Remove-Item(?:Property)?\b[^|;]*\bHK(?:LM|CU):/i, "löscht Einträge in der Windows-Registry"],
|
|
207
|
+
[/\b(?:curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(?:sh|bash|zsh|iex|invoke-expression|powershell|pwsh)\b/i, "lädt ein Skript aus dem Internet und führt es sofort aus"],
|
|
208
|
+
[/\bmkfs(?:\.\w+)?\b|\bdd\s+[^|;]*\bof=\/dev\//i, "überschreibt ein Laufwerk"],
|
|
209
|
+
[/:\(\)\s*\{\s*:\|:&\s*\};:/, "startet eine Fork-Bombe"],
|
|
210
|
+
[/\b(?:npm|pnpm|yarn)\s+publish\b|\bgh\s+release\s+create\b|\bgh\s+repo\s+delete\b/i, "veröffentlicht oder löscht etwas öffentlich"],
|
|
211
|
+
[/\b(?:Set-MpPreference|netsh\s+advfirewall\s+set)\b/i, "ändert Windows-Sicherheitseinstellungen (Virenschutz/Firewall)"],
|
|
212
|
+
];
|
|
213
|
+
/** Grund, warum ein Befehl gefährlich ist, oder null. */
|
|
214
|
+
function dangerCheck(cmd) {
|
|
215
|
+
const c = String(cmd || "");
|
|
216
|
+
for (const [re, why] of DANGER) if (re.test(c)) return why;
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Liegt `p` (relativ zu root) innerhalb des Projektordners? */
|
|
221
|
+
function insideRoot(root, p) {
|
|
222
|
+
const r = path.relative(path.resolve(root), path.resolve(root, p || "."));
|
|
223
|
+
return !(r.startsWith("..") || path.isAbsolute(r));
|
|
224
|
+
}
|
|
225
|
+
|
|
173
226
|
/** read/write/edit/search/ls ausführen. `run` macht jede Oberfläche selbst (Live-Ausgabe). */
|
|
174
227
|
function fileTool(t, root) {
|
|
175
228
|
const P = (p) => path.resolve(root, p || ".");
|
|
229
|
+
// Schreiben nur innerhalb des Projekts – und nie in Gits interne Dateien.
|
|
230
|
+
if (t.name === "write" || t.name === "edit") {
|
|
231
|
+
if (!insideRoot(root, t.path)) return { result: "FEHLER: Schreiben außerhalb des Projektordners ist gesperrt (" + t.path + "). Arbeite nur mit Dateien im Projekt." };
|
|
232
|
+
if (/^\.git(?:[\\/]|$)/.test(path.relative(path.resolve(root), P(t.path)))) return { result: "FEHLER: Gits interne Dateien (.git/) werden nicht direkt bearbeitet – nutze git-Befehle." };
|
|
233
|
+
}
|
|
176
234
|
try {
|
|
177
235
|
if (t.name === "read") {
|
|
178
236
|
const c = readText(P(t.path));
|
|
@@ -251,6 +309,84 @@ async function webTool(t) {
|
|
|
251
309
|
}
|
|
252
310
|
const isWebTool = (t) => t.name === "fetch" || t.name === "websearch";
|
|
253
311
|
|
|
312
|
+
// ---------- Aufgabenliste (todo) ----------
|
|
313
|
+
/** Werkzeug-Eingabe prüfen → saubere Liste [{ text, done }]. */
|
|
314
|
+
function normalizeTodos(t) {
|
|
315
|
+
const items = Array.isArray(t && t.items) ? t.items : [];
|
|
316
|
+
return items.slice(0, 40).map((x) => (typeof x === "string" ? { text: x, done: false } : { text: String((x && x.text) || "").slice(0, 200), done: !!(x && x.done) })).filter((x) => x.text);
|
|
317
|
+
}
|
|
318
|
+
const todoSummary = (items) => "Aufgabenliste aktualisiert (" + items.filter((x) => x.done).length + "/" + items.length + " erledigt).";
|
|
319
|
+
|
|
320
|
+
// ---------- Eigene Befehle: .mythos/commands/<name>.md ----------
|
|
321
|
+
// Erste Zeile = Beschreibung (optional mit "# "), Rest = Anweisung an Mythos. $ARGUMENTS wird ersetzt.
|
|
322
|
+
function loadCommands(dirs) {
|
|
323
|
+
const out = new Map();
|
|
324
|
+
for (const dir of dirs) {
|
|
325
|
+
let names = [];
|
|
326
|
+
try { names = fs.readdirSync(dir).filter((f) => /\.md$/i.test(f)); } catch { continue; }
|
|
327
|
+
for (const f of names) {
|
|
328
|
+
const text = readText(path.join(dir, f));
|
|
329
|
+
if (!text || !text.trim()) continue;
|
|
330
|
+
const name = f.replace(/\.md$/i, "").toLowerCase().replace(/[^a-z0-9äöüß_-]/g, "-");
|
|
331
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
332
|
+
const desc = lines[0].replace(/^#+\s*/, "").trim().slice(0, 100);
|
|
333
|
+
out.set(name, { name: "/" + name, desc: desc || "Eigener Befehl", body: text.trim(), file: path.join(dir, f) });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return [...out.values()];
|
|
337
|
+
}
|
|
338
|
+
const fillCommand = (cmd, args) => (cmd.body.includes("$ARGUMENTS") ? cmd.body.split("$ARGUMENTS").join(args || "") : cmd.body + (args ? "\n\n" + args : ""));
|
|
339
|
+
const COMMAND_TEMPLATE = "# Tests schreiben für eine Datei\nSchreibe gründliche Tests für $ARGUMENTS.\nNutze das Test-Framework, das im Projekt schon verwendet wird, und führe die Tests am Ende aus.\n";
|
|
340
|
+
|
|
341
|
+
// ---------- Code-Review ----------
|
|
342
|
+
const REVIEW_PROMPT = "Führe ein gründliches Code-Review der folgenden Änderungen durch. ÄNDERE KEINE DATEIEN – nur lesen und bewerten.\n" +
|
|
343
|
+
"Gliedere die Antwort so:\n**🐞 Fehler** (echte Bugs, mit Datei:Zeile)\n**⚠️ Risiken** (Sicherheit, Randfälle, Performance)\n**💡 Verbesserungen** (Lesbarkeit, Vereinfachung)\n**✅ Gut gelöst**\n" +
|
|
344
|
+
"Wenn eine Kategorie leer ist, schreib „nichts gefunden“. Sei konkret, keine allgemeinen Floskeln.";
|
|
345
|
+
|
|
346
|
+
// ---------- Projektstatistik ----------
|
|
347
|
+
const LANG = { js: "JavaScript", mjs: "JavaScript", cjs: "JavaScript", jsx: "JavaScript (JSX)", ts: "TypeScript", tsx: "TypeScript (TSX)", py: "Python", java: "Java", kt: "Kotlin", cs: "C#", cpp: "C++", cc: "C++", c: "C", h: "C/C++ Header", go: "Go", rs: "Rust", rb: "Ruby", php: "PHP", swift: "Swift", html: "HTML", css: "CSS", scss: "SCSS", vue: "Vue", svelte: "Svelte", json: "JSON", md: "Markdown", yml: "YAML", yaml: "YAML", sql: "SQL", sh: "Shell", ps1: "PowerShell", bat: "Batch", lua: "Lua", dart: "Dart", xml: "XML", toml: "TOML" };
|
|
348
|
+
function projectStats(root) {
|
|
349
|
+
const byLang = new Map(), big = [];
|
|
350
|
+
let files = 0, lines = 0, bytes = 0, skipped = 0;
|
|
351
|
+
const walk = (dir) => {
|
|
352
|
+
let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
353
|
+
for (const e of entries) {
|
|
354
|
+
if (files > 30000) return;
|
|
355
|
+
const full = path.join(dir, e.name);
|
|
356
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(full); continue; }
|
|
357
|
+
const ext = path.extname(e.name).slice(1).toLowerCase(), lang = LANG[ext];
|
|
358
|
+
if (!lang) { skipped++; continue; }
|
|
359
|
+
let st; try { st = fs.statSync(full); } catch { continue; }
|
|
360
|
+
if (st.size > 2e6) { skipped++; continue; }
|
|
361
|
+
const text = readText(full); if (text == null) continue;
|
|
362
|
+
const n = text ? text.split("\n").length : 0;
|
|
363
|
+
files++; lines += n; bytes += st.size;
|
|
364
|
+
const l = byLang.get(lang) || { files: 0, lines: 0 }; l.files++; l.lines += n; byLang.set(lang, l);
|
|
365
|
+
big.push({ file: path.relative(root, full).split(path.sep).join("/"), lines: n });
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
walk(root);
|
|
369
|
+
big.sort((a, b) => b.lines - a.lines);
|
|
370
|
+
const langs = [...byLang.entries()].sort((a, b) => b[1].lines - a[1].lines);
|
|
371
|
+
const fmt = (n) => n.toLocaleString("de-DE");
|
|
372
|
+
let md = "### 📊 Projektstatistik: " + path.basename(root) + "\n\n";
|
|
373
|
+
md += "**" + fmt(files) + " Code-Dateien · " + fmt(lines) + " Zeilen · " + (bytes / 1024 / 1024).toFixed(1) + " MB**" + (skipped ? " (" + fmt(skipped) + " andere Dateien übersprungen)" : "") + "\n\n";
|
|
374
|
+
if (langs.length) {
|
|
375
|
+
md += "| Sprache | Dateien | Zeilen | Anteil |\n|---|---:|---:|---:|\n";
|
|
376
|
+
md += langs.slice(0, 12).map(([name, v]) => "| " + name + " | " + fmt(v.files) + " | " + fmt(v.lines) + " | " + Math.round((v.lines / Math.max(1, lines)) * 100) + " % |").join("\n") + "\n\n";
|
|
377
|
+
}
|
|
378
|
+
if (big.length) md += "**Größte Dateien:**\n" + big.slice(0, 5).map((b) => "- `" + b.file + "` – " + fmt(b.lines) + " Zeilen").join("\n") + "\n";
|
|
379
|
+
return md;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------- Chat als Markdown ----------
|
|
383
|
+
function chatToMarkdown(title, messages) {
|
|
384
|
+
const date = new Date().toLocaleString("de-DE");
|
|
385
|
+
let md = "# " + (title || "Mythos-Chat") + "\n\n_Exportiert aus Mythos Code am " + date + "_\n\n---\n\n";
|
|
386
|
+
for (const m of messages) md += (m.role === "user" ? "## 🧑 Du\n\n" : m.role === "tool" ? "#### ⚙ " : "## ✦ Mythos\n\n") + String(m.text || "").trim() + "\n\n";
|
|
387
|
+
return md;
|
|
388
|
+
}
|
|
389
|
+
|
|
254
390
|
function readMemory(root) { const t = readText(path.join(root, MEMORY_FILE)); return t ? t.slice(0, 8000) : ""; }
|
|
255
391
|
function addMemory(root, text) {
|
|
256
392
|
const f = path.join(root, MEMORY_FILE);
|
|
@@ -281,7 +417,8 @@ const C = { c: "\x1b[36m", g: "\x1b[32m", y: "\x1b[33m", r: "\x1b[31m", m: "\x1b
|
|
|
281
417
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
282
418
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
283
419
|
const load = () => { try { return JSON.parse(fs.readFileSync(CFG, "utf8")); } catch { return {}; } };
|
|
284
|
-
|
|
420
|
+
// Konfiguration (mit API-Schlüssel) nur für den eigenen Benutzer lesbar.
|
|
421
|
+
const save = (c) => { fs.writeFileSync(CFG, JSON.stringify(c, null, 2), { mode: 0o600 }); try { fs.chmodSync(CFG, 0o600); } catch { /* Windows: Benutzerordner ist ohnehin privat */ } };
|
|
285
422
|
let cfg = load();
|
|
286
423
|
let autoYes = process.argv.includes("--yes");
|
|
287
424
|
let goal = "";
|
|
@@ -329,9 +466,15 @@ async function login() {
|
|
|
329
466
|
const fmt = (ms) => { const s = Math.floor(ms / 1000); return (s >= 60 ? Math.floor(s / 60) + "m " : "") + (s % 60) + "s"; };
|
|
330
467
|
|
|
331
468
|
// Live-Timer: läuft während der ganzen Aufgabe, pausiert für Ausgaben und Rückfragen.
|
|
332
|
-
let T0 = 0, tick = null;
|
|
469
|
+
let T0 = 0, tick = null, activity = "";
|
|
333
470
|
const clearLine = () => process.stdout.write("\r\x1b[K");
|
|
334
|
-
|
|
471
|
+
// Zeigt live, was Mythos gerade tut (z. B. „✍ schreibt src/main.js · 12 KB“) – auf Terminalbreite gekürzt.
|
|
472
|
+
const tickLine = () => {
|
|
473
|
+
const s = "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + (activity ? " · " + activity : "") + " (Strg+C = stoppen)";
|
|
474
|
+
const w = (process.stdout.columns || 100) - 2;
|
|
475
|
+
return s.length > w ? s.slice(0, w - 1) + "…" : s;
|
|
476
|
+
};
|
|
477
|
+
const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r\x1b[K" + C.d + tickLine() + C.x), 250); };
|
|
335
478
|
const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
|
|
336
479
|
const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
|
|
337
480
|
|
|
@@ -352,6 +495,12 @@ rl.on("SIGINT", () => {
|
|
|
352
495
|
function mdLine(line, st) {
|
|
353
496
|
if (/^\s*```/.test(line)) { st.code = !st.code; return C.d + (st.code ? "┌─ " + line.trim().slice(3) : "└─") + C.x; }
|
|
354
497
|
if (st.code) return C.c + "│ " + line + C.x;
|
|
498
|
+
// Markdown-Tabellen: Trennzeile als Linie, Zellen als ausgerichtete Spalten.
|
|
499
|
+
if (/^\s*\|.*\|\s*$/.test(line)) {
|
|
500
|
+
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) return C.d + " " + "─".repeat(52) + C.x;
|
|
501
|
+
const cells = line.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim().replace(/\*\*(.+?)\*\*/g, "$1"));
|
|
502
|
+
return " " + cells.map((c, i) => (i === 0 ? c.padEnd(22) : c.padStart(9))).join(" ");
|
|
503
|
+
}
|
|
355
504
|
const h = line.match(/^(#{1,6})\s+(.*)/);
|
|
356
505
|
if (h) return C.b + C.m + h[2] + C.x;
|
|
357
506
|
return line
|
|
@@ -385,6 +534,20 @@ function streamPrinter() {
|
|
|
385
534
|
};
|
|
386
535
|
}
|
|
387
536
|
|
|
537
|
+
// Sichtbare Gedanken (wie bei base44): laufen gedimmt Wort für Wort, bevor die eigentliche Antwort beginnt.
|
|
538
|
+
function thinkPrinter() {
|
|
539
|
+
let printed = 0, started = false, ended = false;
|
|
540
|
+
return {
|
|
541
|
+
push(full) {
|
|
542
|
+
if (ended) return;
|
|
543
|
+
if (!started) { stopTick(); process.stdout.write(C.d + "💭 "); started = true; }
|
|
544
|
+
if (full.length <= printed) return;
|
|
545
|
+
process.stdout.write(full.slice(printed)); printed = full.length;
|
|
546
|
+
},
|
|
547
|
+
end() { if (ended) return; ended = true; if (started) process.stdout.write(C.x + "\n"); startTick(); },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
388
551
|
// ---------- Modell ----------
|
|
389
552
|
// Verlauf kürzen, damit lange Aufgaben nicht an zu großem Kontext scheitern. level 0 = mild, 2 = stark.
|
|
390
553
|
const shorten = (s, max) => s.length <= max ? s : s.slice(0, Math.floor(max * 0.7)) + "\n…[gekürzt]…\n" + s.slice(s.length - Math.floor(max * 0.3));
|
|
@@ -400,7 +563,7 @@ function compact(history, level) {
|
|
|
400
563
|
}
|
|
401
564
|
|
|
402
565
|
let tokens = { in: 0, out: 0 };
|
|
403
|
-
async function once(messages, onText) {
|
|
566
|
+
async function once(messages, onText, onThink) {
|
|
404
567
|
const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
|
|
405
568
|
const system = SYSTEM();
|
|
406
569
|
tokens.in += estimateTokens(system, messages);
|
|
@@ -411,7 +574,7 @@ async function once(messages, onText) {
|
|
|
411
574
|
body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system, messages, stream: true }),
|
|
412
575
|
});
|
|
413
576
|
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; }
|
|
414
|
-
const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "";
|
|
577
|
+
const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "", think = "";
|
|
415
578
|
while (true) {
|
|
416
579
|
const { done, value } = await reader.read(); if (done) break;
|
|
417
580
|
buf += dec.decode(value, { stream: true });
|
|
@@ -420,6 +583,8 @@ async function once(messages, onText) {
|
|
|
420
583
|
if (!line.startsWith("data:")) continue;
|
|
421
584
|
let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
|
|
422
585
|
if (p.type === "error") { const e = new Error(p.error?.message || "Überlastet"); e.status = 529; throw e; }
|
|
586
|
+
// Sichtbare Gedanken: kommen als eigener Block VOR der Antwort.
|
|
587
|
+
if (p.delta?.type === "thinking_delta" && p.delta.thinking) { think += p.delta.thinking; if (onThink) onThink(think); }
|
|
423
588
|
if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
|
|
424
589
|
}
|
|
425
590
|
}
|
|
@@ -429,14 +594,16 @@ async function once(messages, onText) {
|
|
|
429
594
|
} finally { clearTimeout(to); }
|
|
430
595
|
}
|
|
431
596
|
|
|
432
|
-
async function call(history, onText) {
|
|
597
|
+
async function call(history, onText, onThink) {
|
|
433
598
|
for (let a = 0; a < 6; a++) {
|
|
434
599
|
if (stopped) throw new Error("Gestoppt");
|
|
435
|
-
try { return await once(compact(history, Math.min(2, Math.floor(a / 2))), onText); }
|
|
600
|
+
try { return await once(compact(history, Math.min(2, Math.floor(a / 2))), onText, onThink); }
|
|
436
601
|
catch (e) {
|
|
437
602
|
if (stopped) throw e;
|
|
438
603
|
if (e.status === 401) throw new Error("Anmeldung ungültig – bitte neu anmelden: mythos login");
|
|
439
604
|
if (/Daily limit/i.test(e.message)) throw new Error("Tageslimit erreicht – mit Pro unbegrenzt.");
|
|
605
|
+
// Konfigurationsproblem (Guthaben leer, kein Ausweich-Schlüssel) statt normaler Überlastung -> nicht sinnlos wiederholen.
|
|
606
|
+
if (/nicht konfiguriert/i.test(e.message)) throw e;
|
|
440
607
|
await new Promise((r) => setTimeout(r, Math.min(8000, 1500 * (a + 1))));
|
|
441
608
|
}
|
|
442
609
|
}
|
|
@@ -471,6 +638,13 @@ function runLive(cmd) {
|
|
|
471
638
|
}
|
|
472
639
|
|
|
473
640
|
async function runTool(t) {
|
|
641
|
+
if (t.name === "todo") {
|
|
642
|
+
const items = normalizeTodos(t);
|
|
643
|
+
if (!items.length) return 'FEHLER: "items" fehlt – sende die komplette Liste.';
|
|
644
|
+
const done = items.filter((x) => x.done).length;
|
|
645
|
+
say(C.c + C.b + "📋 Aufgabenliste " + done + "/" + items.length + C.x + "\n" + items.map((x) => (x.done ? C.g + " ☑ " + C.d : " ☐ ") + x.text + C.x).join("\n"));
|
|
646
|
+
return todoSummary(items);
|
|
647
|
+
}
|
|
474
648
|
const label = t.name === "run" ? "run " + t.cmd
|
|
475
649
|
: t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
|
|
476
650
|
: t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
|
|
@@ -478,7 +652,17 @@ async function runTool(t) {
|
|
|
478
652
|
: t.name === "fetch" ? "fetch " + t.url
|
|
479
653
|
: t.name + " " + (t.path || "");
|
|
480
654
|
say(C.y + "⚙ " + label + C.x);
|
|
481
|
-
|
|
655
|
+
// Sicherheitsnetz: gefährliche Befehle fragen immer nach (auch mit --yes) und werden bei /goal blockiert.
|
|
656
|
+
const why = t.name === "run" ? dangerCheck(t.cmd) : null;
|
|
657
|
+
if (why) {
|
|
658
|
+
say(C.r + "🛡 Sicherheitsnetz: dieser Befehl " + why + "." + C.x);
|
|
659
|
+
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.";
|
|
660
|
+
stopTick();
|
|
661
|
+
const a = (await ask(C.r + " Wirklich ausführen? [j/N] " + C.x)).trim().toLowerCase();
|
|
662
|
+
startTick();
|
|
663
|
+
if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
|
|
664
|
+
}
|
|
665
|
+
if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes && !why) {
|
|
482
666
|
stopTick();
|
|
483
667
|
const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
|
|
484
668
|
startTick();
|
|
@@ -497,18 +681,23 @@ async function runTool(t) {
|
|
|
497
681
|
|
|
498
682
|
// ---------- Eine Aufgabe (auch /goal) ----------
|
|
499
683
|
async function turnInner(history) {
|
|
500
|
-
const max = goal ?
|
|
684
|
+
const max = goal ? 4000 : 40;
|
|
501
685
|
let nudges = 0;
|
|
502
686
|
for (let i = 0; i < max && !stopped; i++) {
|
|
503
687
|
const pr = streamPrinter();
|
|
504
|
-
|
|
505
|
-
|
|
688
|
+
let think = null;
|
|
689
|
+
activity = "denkt nach…";
|
|
690
|
+
const out = await call(history,
|
|
691
|
+
(full) => { if (think) think.end(); activity = streamActivity(full); pr.push(full); },
|
|
692
|
+
(t) => { if (!think) think = thinkPrinter(); think.push(t); });
|
|
693
|
+
if (think) think.end();
|
|
694
|
+
pr.end(); activity = "";
|
|
506
695
|
if (stopped) break;
|
|
507
696
|
history.push({ role: "assistant", content: out });
|
|
508
697
|
const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
|
|
509
698
|
if (!m) {
|
|
510
699
|
if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
|
|
511
|
-
if (++nudges >
|
|
700
|
+
if (++nudges > 40) { say(C.y + "🎯 Mythos kommt beim Ziel nicht weiter – schau es dir bitte an (/weiter macht weiter)." + C.x); return "stuck"; }
|
|
512
701
|
history.push({ role: "user", content: GOAL_NUDGE });
|
|
513
702
|
continue;
|
|
514
703
|
}
|
|
@@ -525,7 +714,11 @@ async function summarize(history) {
|
|
|
525
714
|
history.push({ role: "user", content: SUMMARY_PROMPT });
|
|
526
715
|
say(C.m + C.b + "📋 Zusammenfassung" + C.x);
|
|
527
716
|
const pr = streamPrinter();
|
|
528
|
-
|
|
717
|
+
let think = null;
|
|
718
|
+
const out = await call(history,
|
|
719
|
+
(full) => { if (think) think.end(); pr.push(full); },
|
|
720
|
+
(t) => { if (!think) think = thinkPrinter(); think.push(t); });
|
|
721
|
+
if (think) think.end();
|
|
529
722
|
pr.end();
|
|
530
723
|
history.push({ role: "assistant", content: out });
|
|
531
724
|
return out;
|
|
@@ -612,9 +805,51 @@ const HELP = [
|
|
|
612
805
|
["/modell [name]", "Modell anzeigen oder wechseln"],
|
|
613
806
|
["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
|
|
614
807
|
["/clear", "Verlauf leeren"],
|
|
808
|
+
["/review [schwerpunkt]", "Code-Review der Git-Änderungen (ändert nichts)"],
|
|
809
|
+
["/pr [branch]", "Branch pushen und Pull Request erstellen"],
|
|
810
|
+
["/stats", "Projektstatistik: Dateien, Zeilen, Sprachen"],
|
|
811
|
+
["/export [datei]", "Chat als Markdown-Datei speichern"],
|
|
812
|
+
["/kompakt", "Langen Chat zusammenfassen und Tokens sparen"],
|
|
813
|
+
["@pfad/datei", "Datei in der Nachricht erwähnen – ihr Inhalt geht mit"],
|
|
814
|
+
["/befehle", "Eigene Befehle (.mythos/commands/*.md) anzeigen"],
|
|
615
815
|
["/exit", "Beenden"],
|
|
616
816
|
];
|
|
617
817
|
|
|
818
|
+
/** @pfad in der Eingabe → Dateien im Projekt, die es gibt (max. 8, je 60.000 Zeichen). */
|
|
819
|
+
function mentionedFiles(text) {
|
|
820
|
+
const paths = [...new Set([...text.matchAll(/(?:^|\s)@([^\s@]+)/g)].map((m) => m[1].replace(/[.,;:!?)]+$/, "")))].slice(0, 8);
|
|
821
|
+
const out = [];
|
|
822
|
+
for (const p of paths) {
|
|
823
|
+
const full = path.resolve(ROOT, p);
|
|
824
|
+
if (path.relative(ROOT, full).startsWith("..")) continue;
|
|
825
|
+
try { if (fs.statSync(full).isFile()) { const c = readText(full); if (c != null && !c.includes("\u0000")) out.push({ name: p, content: c.slice(0, 60000) }); } } catch { /* gibt es nicht */ }
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
const COMMAND_DIRS = [path.join(os.homedir(), ".mythos", "commands"), path.join(ROOT, ".mythos", "commands")];
|
|
830
|
+
const mdLines = (md) => { const st = { code: false }; return md.split("\n").map((l) => mdLine(l, st)).join("\n"); };
|
|
831
|
+
async function pullRequest(name) {
|
|
832
|
+
const head = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
833
|
+
if (!head.ok) return console.log(C.r + "Kein Git-Repository." + C.x + "\n");
|
|
834
|
+
if (git(["status", "--porcelain"]).out.trim()) return console.log(C.y + "Es gibt noch nicht committete Änderungen – erst /commit, dann /pr." + C.x + "\n");
|
|
835
|
+
if (!git(["remote", "get-url", "origin"]).out.trim()) return console.log(C.y + "Kein Remote „origin“ eingerichtet – verbinde das Projekt zuerst mit GitHub (git remote add origin <url>)." + C.x + "\n");
|
|
836
|
+
let branch = head.out.trim();
|
|
837
|
+
if (!autoYes && !/^[jy]/i.test((await ask(C.d + "Branch pushen und Pull Request erstellen? [j/N] " + C.x)).trim())) return console.log("Abgebrochen.\n");
|
|
838
|
+
if (/^(main|master)$/.test(branch)) {
|
|
839
|
+
branch = name ? name.replace(/\s+/g, "-") : "mythos/" + new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "");
|
|
840
|
+
const co = git(["checkout", "-b", branch]); if (!co.ok) return console.log(C.r + co.out + C.x);
|
|
841
|
+
}
|
|
842
|
+
const push = git(["push", "-u", "origin", branch]); if (!push.ok) return console.log(C.r + push.out.trim() + C.x + "\n");
|
|
843
|
+
try { const out = execFileSync("gh", ["pr", "create", "--fill"], { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); return console.log(C.g + "🔀 " + out.trim() + C.x + "\n"); }
|
|
844
|
+
catch (e) {
|
|
845
|
+
const m = git(["remote", "get-url", "origin"]).out.trim().match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
846
|
+
if (!m) return console.log(C.r + ((e.stderr || "") + "" || "Kein GitHub-Remote gefunden.") + C.x + "\n");
|
|
847
|
+
const url = "https://github.com/" + m[1] + "/compare/" + branch + "?expand=1";
|
|
848
|
+
openBrowser(url);
|
|
849
|
+
return console.log(C.g + "🔀 Branch „" + branch + "“ gepusht. PR-Seite im Browser: " + url + C.x + (e.code === "ENOENT" ? C.d + "\n (Mit der GitHub-CLI „gh“ würde der PR direkt erstellt.)" + C.x : "") + "\n");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
618
853
|
async function command(q, history) {
|
|
619
854
|
const cmd = q.split(/\s+/)[0].toLowerCase(), arg = q.slice(cmd.length).trim();
|
|
620
855
|
if (cmd === "/hilfe" || cmd === "/help") return console.log(HELP.map(([c, d]) => " " + C.c + c.padEnd(22) + C.x + d).join("\n") + "\n");
|
|
@@ -624,6 +859,7 @@ async function command(q, history) {
|
|
|
624
859
|
if (/^(stop|aus|ende|beenden)$/i.test(arg)) { goal = ""; return console.log("🎯 Ziel beendet.\n"); }
|
|
625
860
|
if (!autoYes) { autoYes = true; console.log(C.d + "Vollzugriff für /goal eingeschaltet – Mythos fragt nicht mehr nach." + C.x); }
|
|
626
861
|
goal = arg;
|
|
862
|
+
console.log(C.m + C.b + "🎯 Ziel angenommen: " + C.x + arg + C.d + "\n Mythos plant jetzt und arbeitet selbstständig – der Fortschritt erscheint hier. Stoppen: Strg+C" + C.x);
|
|
627
863
|
return turn(history, "Neues Ziel: " + arg + "\nArbeite jetzt komplett selbstständig daran, bis es erreicht und geprüft ist.");
|
|
628
864
|
}
|
|
629
865
|
if (cmd === "/weiter") {
|
|
@@ -652,6 +888,44 @@ async function command(q, history) {
|
|
|
652
888
|
}
|
|
653
889
|
if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
|
|
654
890
|
if (cmd === "/vollzugriff") { autoYes = !/^(aus|off)$/i.test(arg); return console.log(autoYes ? "Vollzugriff an.\n" : "Vollzugriff aus – Mythos fragt vor Befehlen nach.\n"); }
|
|
891
|
+
if (cmd === "/review") {
|
|
892
|
+
const st = git(["status", "--porcelain"]);
|
|
893
|
+
if (!st.ok) return console.log(C.r + "Kein Git-Repository." + C.x + "\n");
|
|
894
|
+
const diff = (git(["diff", "HEAD"]).out || git(["diff"]).out).trim();
|
|
895
|
+
if (!diff && !st.out.trim()) return console.log("🔍 Keine Änderungen – nichts zu prüfen.\n");
|
|
896
|
+
return turn(history, REVIEW_PROMPT + (arg ? "\n\nBesonders beachten: " + arg : "") + "\n\n" + st.out + "\n```diff\n" + diff.slice(0, 60000) + "\n```");
|
|
897
|
+
}
|
|
898
|
+
if (cmd === "/pr") return pullRequest(arg);
|
|
899
|
+
if (cmd === "/kompakt") {
|
|
900
|
+
if (history.length < 4) return console.log("🗜 Der Chat ist noch kurz – Komprimieren lohnt sich erst später.\n");
|
|
901
|
+
const before = Math.ceil(history.reduce((a, m) => a + String(m.content).length, 0) / 4);
|
|
902
|
+
if (before < 1500) return console.log("🗜 Der Verlauf hat erst ≈ " + fmtTokens(before) + " Tokens – Komprimieren lohnt sich ab etwa 1,5k.\n");
|
|
903
|
+
const transcript = history.map((m) => (m.role === "user" ? "NUTZER/WERKZEUG: " : "MYTHOS: ") + String(m.content).slice(0, 3000)).join("\n\n").slice(-60000);
|
|
904
|
+
process.stdout.write(C.d + "🗜 Fasse den Chat zusammen…" + C.x);
|
|
905
|
+
stopped = false;
|
|
906
|
+
let sum = "";
|
|
907
|
+
try {
|
|
908
|
+
sum = (await call([{ role: "user", content: "Fasse den bisherigen Verlauf dieses Coding-Chats so zusammen, dass ein Agent nahtlos weiterarbeiten kann: **Ziel**, **Stand**, **Wichtige Dateien & Entscheidungen**, **Offene Punkte**. Maximal 25 Zeilen, ohne Werkzeuge.\n\nVERLAUF:\n" + transcript }]))
|
|
909
|
+
.replace(/<tool>[\s\S]*?<\/tool>/g, "").trim();
|
|
910
|
+
} catch (e) { clearLine(); return console.log(C.r + "⚠ " + e.message + C.x + "\n"); }
|
|
911
|
+
clearLine();
|
|
912
|
+
history.length = 0;
|
|
913
|
+
history.push({ role: "user", content: "Zusammenfassung des bisherigen Chats (komprimiert):\n\n" + sum }, { role: "assistant", content: "Verstanden – ich arbeite auf Basis dieser Zusammenfassung weiter." });
|
|
914
|
+
return console.log(mdLines(sum) + "\n" + C.g + "🗜 Komprimiert: ≈ " + fmtTokens(before) + " → ≈ " + fmtTokens(Math.ceil(sum.length / 4)) + " Tokens" + C.x + "\n");
|
|
915
|
+
}
|
|
916
|
+
if (cmd === "/stats") { const b = gitBranch(); return console.log(mdLines(projectStats(ROOT) + (b ? "\n**Git:** Branch `" + b + "`" : "")) + "\n"); }
|
|
917
|
+
if (cmd === "/export") {
|
|
918
|
+
const file = path.resolve(ROOT, arg || "mythos-chat-" + new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-") + ".md");
|
|
919
|
+
const msgs = history.map((m) => ({ role: /^Werkzeug-Ergebnis/.test(m.content) ? "tool" : m.role, text: String(m.content).replace(/<tool>[\s\S]*?<\/tool>/g, "").slice(0, 20000) })).filter((m) => m.text.trim());
|
|
920
|
+
fs.writeFileSync(file, chatToMarkdown("Mythos-Chat – " + path.basename(ROOT), msgs));
|
|
921
|
+
return console.log(C.g + "✓ Gespeichert: " + file + C.x + "\n");
|
|
922
|
+
}
|
|
923
|
+
if (cmd === "/befehle") {
|
|
924
|
+
const list = loadCommands(COMMAND_DIRS);
|
|
925
|
+
return console.log(list.length ? list.map((c) => " " + C.c + c.name.padEnd(20) + C.x + c.desc).join("\n") + "\n" : "Noch keine eigenen Befehle. Leg eine Markdown-Datei in .mythos/commands/ an, z. B. tests.md → /tests.\nErste Zeile = Beschreibung, Rest = Anweisung an Mythos, $ARGUMENTS = was du dahinter schreibst.\n");
|
|
926
|
+
}
|
|
927
|
+
const own = loadCommands(COMMAND_DIRS).find((c) => c.name === cmd);
|
|
928
|
+
if (own) return turn(history, fillCommand(own, arg));
|
|
655
929
|
console.log("Unbekannter Befehl: " + cmd + " – /hilfe zeigt alle Befehle.\n");
|
|
656
930
|
}
|
|
657
931
|
|
|
@@ -672,7 +946,9 @@ async function main() {
|
|
|
672
946
|
if (q.startsWith("/")) { await command(q, history); continue; }
|
|
673
947
|
const { text, images } = extractImages(q);
|
|
674
948
|
if (images.length) console.log(C.d + "🖼 " + images.length + " Bild(er) angehängt" + C.x);
|
|
675
|
-
|
|
949
|
+
const files = mentionedFiles(text);
|
|
950
|
+
if (files.length) console.log(C.d + "📎 " + files.map((f) => f.name).join(", ") + C.x);
|
|
951
|
+
await turn(history, text + files.map((f) => "\n\n### " + f.name + "\n```\n" + f.content + "\n```").join(""), images);
|
|
676
952
|
}
|
|
677
953
|
rl.close();
|
|
678
954
|
}
|