mythos-code 1.5.0 → 1.7.1

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 +235 -7
  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,15 @@ 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- Teile deinen Fortschritt normal im Chat mit (was du tust und warum), aber warte nicht auf eine Antwort." +
56
+ "\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." +
57
+ "\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
58
  : "";
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.";
59
+ 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
60
  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
61
 
55
62
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
@@ -170,9 +177,46 @@ function searchFiles(root, pattern, glob, max = 200) {
170
177
  return hits.join("\n") + (hits.length >= max ? "\n… (mehr als " + max + " Treffer – Suche eingrenzen)" : "");
171
178
  }
172
179
 
180
+ // ---------- Sicherheitsnetz für Befehle ----------
181
+ // Diese Befehle fragen IMMER nach (auch mit Vollzugriff) und werden im /goal-Modus blockiert,
182
+ // weil dann niemand da ist, der sie bestätigen könnte.
183
+ const DANGER = [
184
+ [/\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"],
185
+ [/\bformat(?:\.com)?\s+[a-z]:/i, "formatiert ein Laufwerk"],
186
+ [/\b(?:rd|rmdir)\s+\/s\b[^&|;]*\b[a-z]:\\?\s*(?:$|[&|;])/i, "löscht ein ganzes Laufwerk"],
187
+ [/\b(?:del|erase)\b[^&|;]*\/s\b[^&|;]*\b[a-z]:\\\*?\s*(?:$|[&|;])/i, "löscht ein ganzes Laufwerk"],
188
+ [/Remove-Item\b[^|;]*-Recurse[^|;]*\s["']?[a-z]:\\?["']?(?:\s|$|;)/i, "löscht rekursiv ein ganzes Laufwerk"],
189
+ [/\bgit\s+push\b[^;&|]*\s(?:--force(?:-with-lease)?|-f)\b/i, "überschreibt die Git-Historie auf dem Server (Force-Push)"],
190
+ [/\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-z]*f/i, "verwirft lokale Änderungen unwiderruflich"],
191
+ [/\b(?:shutdown|restart-computer|stop-computer)\b/i, "fährt den PC herunter oder startet ihn neu"],
192
+ [/\breg(?:\.exe)?\s+delete\b|Remove-Item(?:Property)?\b[^|;]*\bHK(?:LM|CU):/i, "löscht Einträge in der Windows-Registry"],
193
+ [/\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"],
194
+ [/\bmkfs(?:\.\w+)?\b|\bdd\s+[^|;]*\bof=\/dev\//i, "überschreibt ein Laufwerk"],
195
+ [/:\(\)\s*\{\s*:\|:&\s*\};:/, "startet eine Fork-Bombe"],
196
+ [/\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"],
197
+ [/\b(?:Set-MpPreference|netsh\s+advfirewall\s+set)\b/i, "ändert Windows-Sicherheitseinstellungen (Virenschutz/Firewall)"],
198
+ ];
199
+ /** Grund, warum ein Befehl gefährlich ist, oder null. */
200
+ function dangerCheck(cmd) {
201
+ const c = String(cmd || "");
202
+ for (const [re, why] of DANGER) if (re.test(c)) return why;
203
+ return null;
204
+ }
205
+
206
+ /** Liegt `p` (relativ zu root) innerhalb des Projektordners? */
207
+ function insideRoot(root, p) {
208
+ const r = path.relative(path.resolve(root), path.resolve(root, p || "."));
209
+ return !(r.startsWith("..") || path.isAbsolute(r));
210
+ }
211
+
173
212
  /** read/write/edit/search/ls ausführen. `run` macht jede Oberfläche selbst (Live-Ausgabe). */
174
213
  function fileTool(t, root) {
175
214
  const P = (p) => path.resolve(root, p || ".");
215
+ // Schreiben nur innerhalb des Projekts – und nie in Gits interne Dateien.
216
+ if (t.name === "write" || t.name === "edit") {
217
+ if (!insideRoot(root, t.path)) return { result: "FEHLER: Schreiben außerhalb des Projektordners ist gesperrt (" + t.path + "). Arbeite nur mit Dateien im Projekt." };
218
+ 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." };
219
+ }
176
220
  try {
177
221
  if (t.name === "read") {
178
222
  const c = readText(P(t.path));
@@ -251,6 +295,84 @@ async function webTool(t) {
251
295
  }
252
296
  const isWebTool = (t) => t.name === "fetch" || t.name === "websearch";
253
297
 
298
+ // ---------- Aufgabenliste (todo) ----------
299
+ /** Werkzeug-Eingabe prüfen → saubere Liste [{ text, done }]. */
300
+ function normalizeTodos(t) {
301
+ const items = Array.isArray(t && t.items) ? t.items : [];
302
+ 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);
303
+ }
304
+ const todoSummary = (items) => "Aufgabenliste aktualisiert (" + items.filter((x) => x.done).length + "/" + items.length + " erledigt).";
305
+
306
+ // ---------- Eigene Befehle: .mythos/commands/<name>.md ----------
307
+ // Erste Zeile = Beschreibung (optional mit "# "), Rest = Anweisung an Mythos. $ARGUMENTS wird ersetzt.
308
+ function loadCommands(dirs) {
309
+ const out = new Map();
310
+ for (const dir of dirs) {
311
+ let names = [];
312
+ try { names = fs.readdirSync(dir).filter((f) => /\.md$/i.test(f)); } catch { continue; }
313
+ for (const f of names) {
314
+ const text = readText(path.join(dir, f));
315
+ if (!text || !text.trim()) continue;
316
+ const name = f.replace(/\.md$/i, "").toLowerCase().replace(/[^a-z0-9äöüß_-]/g, "-");
317
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
318
+ const desc = lines[0].replace(/^#+\s*/, "").trim().slice(0, 100);
319
+ out.set(name, { name: "/" + name, desc: desc || "Eigener Befehl", body: text.trim(), file: path.join(dir, f) });
320
+ }
321
+ }
322
+ return [...out.values()];
323
+ }
324
+ const fillCommand = (cmd, args) => (cmd.body.includes("$ARGUMENTS") ? cmd.body.split("$ARGUMENTS").join(args || "") : cmd.body + (args ? "\n\n" + args : ""));
325
+ 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";
326
+
327
+ // ---------- Code-Review ----------
328
+ const REVIEW_PROMPT = "Führe ein gründliches Code-Review der folgenden Änderungen durch. ÄNDERE KEINE DATEIEN – nur lesen und bewerten.\n" +
329
+ "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" +
330
+ "Wenn eine Kategorie leer ist, schreib „nichts gefunden“. Sei konkret, keine allgemeinen Floskeln.";
331
+
332
+ // ---------- Projektstatistik ----------
333
+ 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" };
334
+ function projectStats(root) {
335
+ const byLang = new Map(), big = [];
336
+ let files = 0, lines = 0, bytes = 0, skipped = 0;
337
+ const walk = (dir) => {
338
+ let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
339
+ for (const e of entries) {
340
+ if (files > 30000) return;
341
+ const full = path.join(dir, e.name);
342
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(full); continue; }
343
+ const ext = path.extname(e.name).slice(1).toLowerCase(), lang = LANG[ext];
344
+ if (!lang) { skipped++; continue; }
345
+ let st; try { st = fs.statSync(full); } catch { continue; }
346
+ if (st.size > 2e6) { skipped++; continue; }
347
+ const text = readText(full); if (text == null) continue;
348
+ const n = text ? text.split("\n").length : 0;
349
+ files++; lines += n; bytes += st.size;
350
+ const l = byLang.get(lang) || { files: 0, lines: 0 }; l.files++; l.lines += n; byLang.set(lang, l);
351
+ big.push({ file: path.relative(root, full).split(path.sep).join("/"), lines: n });
352
+ }
353
+ };
354
+ walk(root);
355
+ big.sort((a, b) => b.lines - a.lines);
356
+ const langs = [...byLang.entries()].sort((a, b) => b[1].lines - a[1].lines);
357
+ const fmt = (n) => n.toLocaleString("de-DE");
358
+ let md = "### 📊 Projektstatistik: " + path.basename(root) + "\n\n";
359
+ md += "**" + fmt(files) + " Code-Dateien · " + fmt(lines) + " Zeilen · " + (bytes / 1024 / 1024).toFixed(1) + " MB**" + (skipped ? " (" + fmt(skipped) + " andere Dateien übersprungen)" : "") + "\n\n";
360
+ if (langs.length) {
361
+ md += "| Sprache | Dateien | Zeilen | Anteil |\n|---|---:|---:|---:|\n";
362
+ 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";
363
+ }
364
+ if (big.length) md += "**Größte Dateien:**\n" + big.slice(0, 5).map((b) => "- `" + b.file + "` – " + fmt(b.lines) + " Zeilen").join("\n") + "\n";
365
+ return md;
366
+ }
367
+
368
+ // ---------- Chat als Markdown ----------
369
+ function chatToMarkdown(title, messages) {
370
+ const date = new Date().toLocaleString("de-DE");
371
+ let md = "# " + (title || "Mythos-Chat") + "\n\n_Exportiert aus Mythos Code am " + date + "_\n\n---\n\n";
372
+ for (const m of messages) md += (m.role === "user" ? "## 🧑 Du\n\n" : m.role === "tool" ? "#### ⚙ " : "## ✦ Mythos\n\n") + String(m.text || "").trim() + "\n\n";
373
+ return md;
374
+ }
375
+
254
376
  function readMemory(root) { const t = readText(path.join(root, MEMORY_FILE)); return t ? t.slice(0, 8000) : ""; }
255
377
  function addMemory(root, text) {
256
378
  const f = path.join(root, MEMORY_FILE);
@@ -281,7 +403,8 @@ const C = { c: "\x1b[36m", g: "\x1b[32m", y: "\x1b[33m", r: "\x1b[31m", m: "\x1b
281
403
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
282
404
  const ask = (q) => new Promise((r) => rl.question(q, r));
283
405
  const load = () => { try { return JSON.parse(fs.readFileSync(CFG, "utf8")); } catch { return {}; } };
284
- const save = (c) => fs.writeFileSync(CFG, JSON.stringify(c, null, 2));
406
+ // Konfiguration (mit API-Schlüssel) nur für den eigenen Benutzer lesbar.
407
+ 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
408
  let cfg = load();
286
409
  let autoYes = process.argv.includes("--yes");
287
410
  let goal = "";
@@ -352,6 +475,12 @@ rl.on("SIGINT", () => {
352
475
  function mdLine(line, st) {
353
476
  if (/^\s*```/.test(line)) { st.code = !st.code; return C.d + (st.code ? "┌─ " + line.trim().slice(3) : "└─") + C.x; }
354
477
  if (st.code) return C.c + "│ " + line + C.x;
478
+ // Markdown-Tabellen: Trennzeile als Linie, Zellen als ausgerichtete Spalten.
479
+ if (/^\s*\|.*\|\s*$/.test(line)) {
480
+ if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) return C.d + " " + "─".repeat(52) + C.x;
481
+ const cells = line.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim().replace(/\*\*(.+?)\*\*/g, "$1"));
482
+ return " " + cells.map((c, i) => (i === 0 ? c.padEnd(22) : c.padStart(9))).join(" ");
483
+ }
355
484
  const h = line.match(/^(#{1,6})\s+(.*)/);
356
485
  if (h) return C.b + C.m + h[2] + C.x;
357
486
  return line
@@ -471,6 +600,13 @@ function runLive(cmd) {
471
600
  }
472
601
 
473
602
  async function runTool(t) {
603
+ if (t.name === "todo") {
604
+ const items = normalizeTodos(t);
605
+ if (!items.length) return 'FEHLER: "items" fehlt – sende die komplette Liste.';
606
+ const done = items.filter((x) => x.done).length;
607
+ 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"));
608
+ return todoSummary(items);
609
+ }
474
610
  const label = t.name === "run" ? "run " + t.cmd
475
611
  : t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
476
612
  : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
@@ -478,7 +614,17 @@ async function runTool(t) {
478
614
  : t.name === "fetch" ? "fetch " + t.url
479
615
  : t.name + " " + (t.path || "");
480
616
  say(C.y + "⚙ " + label + C.x);
481
- if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes) {
617
+ // Sicherheitsnetz: gefährliche Befehle fragen immer nach (auch mit --yes) und werden bei /goal blockiert.
618
+ const why = t.name === "run" ? dangerCheck(t.cmd) : null;
619
+ if (why) {
620
+ say(C.r + "🛡 Sicherheitsnetz: dieser Befehl " + why + "." + C.x);
621
+ 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.";
622
+ stopTick();
623
+ const a = (await ask(C.r + " Wirklich ausführen? [j/N] " + C.x)).trim().toLowerCase();
624
+ startTick();
625
+ if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
626
+ }
627
+ if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes && !why) {
482
628
  stopTick();
483
629
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
484
630
  startTick();
@@ -497,7 +643,7 @@ async function runTool(t) {
497
643
 
498
644
  // ---------- Eine Aufgabe (auch /goal) ----------
499
645
  async function turnInner(history) {
500
- const max = goal ? 200 : 40;
646
+ const max = goal ? 4000 : 40;
501
647
  let nudges = 0;
502
648
  for (let i = 0; i < max && !stopped; i++) {
503
649
  const pr = streamPrinter();
@@ -508,7 +654,7 @@ async function turnInner(history) {
508
654
  const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
509
655
  if (!m) {
510
656
  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"; }
657
+ 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
658
  history.push({ role: "user", content: GOAL_NUDGE });
513
659
  continue;
514
660
  }
@@ -612,9 +758,51 @@ const HELP = [
612
758
  ["/modell [name]", "Modell anzeigen oder wechseln"],
613
759
  ["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
614
760
  ["/clear", "Verlauf leeren"],
761
+ ["/review [schwerpunkt]", "Code-Review der Git-Änderungen (ändert nichts)"],
762
+ ["/pr [branch]", "Branch pushen und Pull Request erstellen"],
763
+ ["/stats", "Projektstatistik: Dateien, Zeilen, Sprachen"],
764
+ ["/export [datei]", "Chat als Markdown-Datei speichern"],
765
+ ["/kompakt", "Langen Chat zusammenfassen und Tokens sparen"],
766
+ ["@pfad/datei", "Datei in der Nachricht erwähnen – ihr Inhalt geht mit"],
767
+ ["/befehle", "Eigene Befehle (.mythos/commands/*.md) anzeigen"],
615
768
  ["/exit", "Beenden"],
616
769
  ];
617
770
 
771
+ /** @pfad in der Eingabe → Dateien im Projekt, die es gibt (max. 8, je 60.000 Zeichen). */
772
+ function mentionedFiles(text) {
773
+ const paths = [...new Set([...text.matchAll(/(?:^|\s)@([^\s@]+)/g)].map((m) => m[1].replace(/[.,;:!?)]+$/, "")))].slice(0, 8);
774
+ const out = [];
775
+ for (const p of paths) {
776
+ const full = path.resolve(ROOT, p);
777
+ if (path.relative(ROOT, full).startsWith("..")) continue;
778
+ 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 */ }
779
+ }
780
+ return out;
781
+ }
782
+ const COMMAND_DIRS = [path.join(os.homedir(), ".mythos", "commands"), path.join(ROOT, ".mythos", "commands")];
783
+ const mdLines = (md) => { const st = { code: false }; return md.split("\n").map((l) => mdLine(l, st)).join("\n"); };
784
+ async function pullRequest(name) {
785
+ const head = git(["rev-parse", "--abbrev-ref", "HEAD"]);
786
+ if (!head.ok) return console.log(C.r + "Kein Git-Repository." + C.x + "\n");
787
+ if (git(["status", "--porcelain"]).out.trim()) return console.log(C.y + "Es gibt noch nicht committete Änderungen – erst /commit, dann /pr." + C.x + "\n");
788
+ 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");
789
+ let branch = head.out.trim();
790
+ 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");
791
+ if (/^(main|master)$/.test(branch)) {
792
+ branch = name ? name.replace(/\s+/g, "-") : "mythos/" + new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "");
793
+ const co = git(["checkout", "-b", branch]); if (!co.ok) return console.log(C.r + co.out + C.x);
794
+ }
795
+ const push = git(["push", "-u", "origin", branch]); if (!push.ok) return console.log(C.r + push.out.trim() + C.x + "\n");
796
+ 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"); }
797
+ catch (e) {
798
+ const m = git(["remote", "get-url", "origin"]).out.trim().match(/github\.com[:/](.+?)(?:\.git)?$/);
799
+ if (!m) return console.log(C.r + ((e.stderr || "") + "" || "Kein GitHub-Remote gefunden.") + C.x + "\n");
800
+ const url = "https://github.com/" + m[1] + "/compare/" + branch + "?expand=1";
801
+ openBrowser(url);
802
+ 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");
803
+ }
804
+ }
805
+
618
806
  async function command(q, history) {
619
807
  const cmd = q.split(/\s+/)[0].toLowerCase(), arg = q.slice(cmd.length).trim();
620
808
  if (cmd === "/hilfe" || cmd === "/help") return console.log(HELP.map(([c, d]) => " " + C.c + c.padEnd(22) + C.x + d).join("\n") + "\n");
@@ -652,6 +840,44 @@ async function command(q, history) {
652
840
  }
653
841
  if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
654
842
  if (cmd === "/vollzugriff") { autoYes = !/^(aus|off)$/i.test(arg); return console.log(autoYes ? "Vollzugriff an.\n" : "Vollzugriff aus – Mythos fragt vor Befehlen nach.\n"); }
843
+ if (cmd === "/review") {
844
+ const st = git(["status", "--porcelain"]);
845
+ if (!st.ok) return console.log(C.r + "Kein Git-Repository." + C.x + "\n");
846
+ const diff = (git(["diff", "HEAD"]).out || git(["diff"]).out).trim();
847
+ if (!diff && !st.out.trim()) return console.log("🔍 Keine Änderungen – nichts zu prüfen.\n");
848
+ return turn(history, REVIEW_PROMPT + (arg ? "\n\nBesonders beachten: " + arg : "") + "\n\n" + st.out + "\n```diff\n" + diff.slice(0, 60000) + "\n```");
849
+ }
850
+ if (cmd === "/pr") return pullRequest(arg);
851
+ if (cmd === "/kompakt") {
852
+ if (history.length < 4) return console.log("🗜 Der Chat ist noch kurz – Komprimieren lohnt sich erst später.\n");
853
+ const before = Math.ceil(history.reduce((a, m) => a + String(m.content).length, 0) / 4);
854
+ if (before < 1500) return console.log("🗜 Der Verlauf hat erst ≈ " + fmtTokens(before) + " Tokens – Komprimieren lohnt sich ab etwa 1,5k.\n");
855
+ const transcript = history.map((m) => (m.role === "user" ? "NUTZER/WERKZEUG: " : "MYTHOS: ") + String(m.content).slice(0, 3000)).join("\n\n").slice(-60000);
856
+ process.stdout.write(C.d + "🗜 Fasse den Chat zusammen…" + C.x);
857
+ stopped = false;
858
+ let sum = "";
859
+ try {
860
+ 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 }]))
861
+ .replace(/<tool>[\s\S]*?<\/tool>/g, "").trim();
862
+ } catch (e) { clearLine(); return console.log(C.r + "⚠ " + e.message + C.x + "\n"); }
863
+ clearLine();
864
+ history.length = 0;
865
+ history.push({ role: "user", content: "Zusammenfassung des bisherigen Chats (komprimiert):\n\n" + sum }, { role: "assistant", content: "Verstanden – ich arbeite auf Basis dieser Zusammenfassung weiter." });
866
+ return console.log(mdLines(sum) + "\n" + C.g + "🗜 Komprimiert: ≈ " + fmtTokens(before) + " → ≈ " + fmtTokens(Math.ceil(sum.length / 4)) + " Tokens" + C.x + "\n");
867
+ }
868
+ if (cmd === "/stats") { const b = gitBranch(); return console.log(mdLines(projectStats(ROOT) + (b ? "\n**Git:** Branch `" + b + "`" : "")) + "\n"); }
869
+ if (cmd === "/export") {
870
+ const file = path.resolve(ROOT, arg || "mythos-chat-" + new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-") + ".md");
871
+ 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());
872
+ fs.writeFileSync(file, chatToMarkdown("Mythos-Chat – " + path.basename(ROOT), msgs));
873
+ return console.log(C.g + "✓ Gespeichert: " + file + C.x + "\n");
874
+ }
875
+ if (cmd === "/befehle") {
876
+ const list = loadCommands(COMMAND_DIRS);
877
+ 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");
878
+ }
879
+ const own = loadCommands(COMMAND_DIRS).find((c) => c.name === cmd);
880
+ if (own) return turn(history, fillCommand(own, arg));
655
881
  console.log("Unbekannter Befehl: " + cmd + " – /hilfe zeigt alle Befehle.\n");
656
882
  }
657
883
 
@@ -672,7 +898,9 @@ async function main() {
672
898
  if (q.startsWith("/")) { await command(q, history); continue; }
673
899
  const { text, images } = extractImages(q);
674
900
  if (images.length) console.log(C.d + "🖼 " + images.length + " Bild(er) angehängt" + C.x);
675
- await turn(history, text, images);
901
+ const files = mentionedFiles(text);
902
+ if (files.length) console.log(C.d + "📎 " + files.map((f) => f.name).join(", ") + C.x);
903
+ await turn(history, text + files.map((f) => "\n\n### " + f.name + "\n```\n" + f.content + "\n```").join(""), images);
676
904
  }
677
905
  rl.close();
678
906
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.5.0",
3
+ "version": "1.7.1",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",