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.
Files changed (2) hide show
  1. package/bin/mythos.js +292 -16
  2. 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 + "\nDer Nutzer ist nicht am Rechner. Arbeite komplett selbstständig, ohne Rückfragen, bis das Ziel vollständig erreicht und überprüft ist. Erst dann schreibe in deiner letzten Antwort eine eigene Zeile: ZIEL ERREICHT"
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
- const GOAL_NUDGE = "Das Ziel ist noch nicht als erreicht gemeldet. Arbeite selbstständig weiter, ohne Rückfragen. Wenn es wirklich vollständig erledigt und geprüft ist, schreibe ZIEL ERREICHT.";
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
- const save = (c) => fs.writeFileSync(CFG, JSON.stringify(c, null, 2));
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
- const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r" + C.d + "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + " (Strg+C = stoppen)" + C.x + " "), 250); };
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
- if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes) {
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 ? 200 : 40;
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
- const out = await call(history, (full) => pr.push(full));
505
- pr.end();
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 > 5) { say(C.y + "🎯 Mythos kommt beim Ziel nicht weiter – schau es dir bitte an (/weiter macht weiter)." + C.x); return "stuck"; }
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
- const out = await call(history, (full) => pr.push(full));
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
- await turn(history, text, images);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.5.0",
3
+ "version": "1.7.3",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",