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.
- package/bin/mythos.js +235 -7
- 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 +
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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 >
|
|
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
|
-
|
|
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
|
}
|