mythos-code 1.3.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 +781 -52
- package/package.json +1 -1
package/bin/mythos.js
CHANGED
|
@@ -1,28 +1,416 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Mythos Code CLI – Agent
|
|
2
|
+
// Mythos Code CLI – KI-Coding-Agent im Terminal (by Mythoscraft)
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import readline from "node:readline";
|
|
7
|
-
import { exec } from "node:child_process";
|
|
7
|
+
import { exec, spawn, execFileSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
// ---------- Gemeinsame Werkzeuge (CLI + Desktop-App) ----------
|
|
10
|
+
// Wird roh in bin/mythos.js (CLI, ESM) und main.js (App, CommonJS) eingefügt.
|
|
11
|
+
// Erwartet, dass `fs` und `path` im umgebenden Code schon importiert sind.
|
|
12
|
+
|
|
13
|
+
const TOOL_PROMPT = [
|
|
14
|
+
"Werkzeuge – antworte pro Schritt mit GENAU EINEM Block:",
|
|
15
|
+
'<tool>{"name":"run","cmd":"..."}</tool> Shell-Befehl ausführen (Ausgabe läuft live mit)',
|
|
16
|
+
'<tool>{"name":"read","path":"..."}</tool> Datei lesen',
|
|
17
|
+
'<tool>{"name":"edit","path":"...","old":"exakter alter Text","new":"neuer Text"}</tool> Datei gezielt ändern (bevorzugt! "old" muss genau einmal vorkommen, sonst "all":true)',
|
|
18
|
+
'<tool>{"name":"write","path":"...","content":"..."}</tool> Neue Datei anlegen oder komplett neu schreiben',
|
|
19
|
+
'<tool>{"name":"search","pattern":"regex","path":".","glob":"*.ts"}</tool> Im Projekt suchen (path und glob optional)',
|
|
20
|
+
'<tool>{"name":"ls","path":"."}</tool> Ordner auflisten',
|
|
21
|
+
'<tool>{"name":"websearch","query":"..."}</tool> Im Internet suchen (z. B. Doku, Fehlermeldungen)',
|
|
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)',
|
|
24
|
+
"Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
|
|
25
|
+
"Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool> (Markdown erlaubt).",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
// Modelle, die v1-messages versteht (siehe mapModel dort).
|
|
29
|
+
const MODELS = [
|
|
30
|
+
{ id: "mythos-code", label: "MythosCode", desc: "Standard fürs Programmieren" },
|
|
31
|
+
{ id: "mythos-v2", label: "Mythos v2", desc: "am stärksten, etwas langsamer" },
|
|
32
|
+
{ id: "mythos-sonnet", label: "Mythos v1", desc: "ausgewogen" },
|
|
33
|
+
{ id: "mythos-lite", label: "Mythos Lite", desc: "am schnellsten" },
|
|
34
|
+
];
|
|
35
|
+
/** Grobe Token-Schätzung (≈ 4 Zeichen pro Token, Bilder pauschal). */
|
|
36
|
+
function estimateTokens(system, messages) {
|
|
37
|
+
let chars = String(system || "").length, images = 0;
|
|
38
|
+
for (const m of messages) {
|
|
39
|
+
if (typeof m.content === "string") chars += m.content.length;
|
|
40
|
+
else for (const b of m.content || []) { if (b.type === "image") images++; else chars += String(b.text || "").length; }
|
|
41
|
+
}
|
|
42
|
+
return Math.ceil(chars / 4) + images * 1500;
|
|
43
|
+
}
|
|
44
|
+
const fmtTokens = (n) => (n >= 1e6 ? (n / 1e6).toFixed(1) + " Mio." : n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
|
|
45
|
+
|
|
46
|
+
const MEMORY_FILE = "MYTHOS.md";
|
|
47
|
+
const memoryPrompt = (mem) => mem
|
|
48
|
+
? "\n\nProjekt-Gedächtnis (" + MEMORY_FILE + " im Projektordner – halte dich daran, ergänze es bei wichtigen neuen Erkenntnissen per edit):\n" + mem
|
|
49
|
+
: "";
|
|
50
|
+
const goalPrompt = (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"
|
|
58
|
+
: "";
|
|
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.";
|
|
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.";
|
|
61
|
+
|
|
62
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
|
|
63
|
+
|
|
64
|
+
function readText(file) { try { return fs.readFileSync(file, "utf8"); } catch { return null; } }
|
|
65
|
+
|
|
66
|
+
// Windows-Konsolenprogramme (cmd, dir, …) schreiben in Pipes im OEM-Zeichensatz (CP850), Node/Git in UTF-8.
|
|
67
|
+
const CP850 = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈıÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´±‗¾¶§÷¸°¨·¹³²■ ";
|
|
68
|
+
const fromCp850 = (b) => { let s = ""; for (const c of b) s += c < 128 ? String.fromCharCode(c) : CP850[c - 128]; return s; };
|
|
69
|
+
/** Decoder für eine Ausgabe-Pipe: jedes Stück als UTF-8, sonst (Windows) als CP850. Angefangene UTF-8-Zeichen werden aufgehoben. */
|
|
70
|
+
function outputDecoder() {
|
|
71
|
+
let pending = Buffer.alloc(0);
|
|
72
|
+
return (chunk) => {
|
|
73
|
+
let b = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
74
|
+
let i = b.length - 1, cont = 0;
|
|
75
|
+
while (i >= 0 && cont < 3 && (b[i] & 0xc0) === 0x80) { i--; cont++; }
|
|
76
|
+
const need = i >= 0 ? (b[i] >= 0xf0 ? 4 : b[i] >= 0xe0 ? 3 : b[i] >= 0xc0 ? 2 : 1) : 1;
|
|
77
|
+
const cut = need > 1 && b.length - i < need ? i : b.length;
|
|
78
|
+
pending = Buffer.from(b.subarray(cut)); b = b.subarray(0, cut);
|
|
79
|
+
try { return new TextDecoder("utf-8", { fatal: true }).decode(b); }
|
|
80
|
+
catch { return process.platform === "win32" ? fromCp850(b) : b.toString("latin1"); }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Zeilen-Diff mit Kontext: { added, removed, lines: [[op, text]] }, op = " " | "+" | "-" | "@" (Lücke). */
|
|
85
|
+
function lineDiff(a, b, ctx = 3, maxLines = 400) {
|
|
86
|
+
// Zeilenumbruch am Dateiende ist keine eigene Zeile.
|
|
87
|
+
const toLines = (t) => (t ? t.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n") : []);
|
|
88
|
+
const A = toLines(a), B = toLines(b);
|
|
89
|
+
let s = 0; while (s < A.length && s < B.length && A[s] === B[s]) s++;
|
|
90
|
+
let ea = A.length, eb = B.length; while (ea > s && eb > s && A[ea - 1] === B[eb - 1]) { ea--; eb--; }
|
|
91
|
+
const x = A.slice(s, ea), y = B.slice(s, eb);
|
|
92
|
+
let ops = [];
|
|
93
|
+
if (x.length * y.length <= 4e6) {
|
|
94
|
+
const n = x.length, m = y.length, L = [];
|
|
95
|
+
for (let i = 0; i <= n; i++) L.push(new Uint32Array(m + 1));
|
|
96
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = x[i] === y[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
|
|
97
|
+
let i = 0, j = 0;
|
|
98
|
+
while (i < n && j < m) {
|
|
99
|
+
if (x[i] === y[j]) { ops.push([" ", x[i]]); i++; j++; }
|
|
100
|
+
else if (L[i + 1][j] >= L[i][j + 1]) ops.push(["-", x[i++]]);
|
|
101
|
+
else ops.push(["+", y[j++]]);
|
|
102
|
+
}
|
|
103
|
+
while (i < n) ops.push(["-", x[i++]]);
|
|
104
|
+
while (j < m) ops.push(["+", y[j++]]);
|
|
105
|
+
} else ops = x.map((l) => ["-", l]).concat(y.map((l) => ["+", l]));
|
|
106
|
+
const all = A.slice(0, s).map((l) => [" ", l]).concat(ops, A.slice(ea).map((l) => [" ", l]));
|
|
107
|
+
const keep = new Uint8Array(all.length);
|
|
108
|
+
let added = 0, removed = 0;
|
|
109
|
+
all.forEach((o, k) => {
|
|
110
|
+
if (o[0] === " ") return;
|
|
111
|
+
if (o[0] === "+") added++; else removed++;
|
|
112
|
+
for (let d = Math.max(0, k - ctx); d <= Math.min(all.length - 1, k + ctx); d++) keep[d] = 1;
|
|
113
|
+
});
|
|
114
|
+
const lines = [];
|
|
115
|
+
let gap = false;
|
|
116
|
+
for (let k = 0; k < all.length; k++) {
|
|
117
|
+
if (!keep[k]) { gap = true; continue; }
|
|
118
|
+
if (lines.length >= maxLines) { lines.push(["@", "… (gekürzt)"]); break; }
|
|
119
|
+
if (gap && lines.length) lines.push(["@", "…"]);
|
|
120
|
+
gap = false; lines.push(all[k]);
|
|
121
|
+
}
|
|
122
|
+
return { added, removed, lines };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Gezieltes Ändern: `old` muss genau einmal vorkommen (oder all=true). */
|
|
126
|
+
function editFile(file, oldText, newText, all) {
|
|
127
|
+
const cur = readText(file);
|
|
128
|
+
if (cur == null) return { error: "Datei nicht gefunden: " + file };
|
|
129
|
+
if (!oldText) return { error: '"old" fehlt – gib den exakten Text an, der ersetzt werden soll.' };
|
|
130
|
+
let o = String(oldText), n = String(newText ?? "");
|
|
131
|
+
// CRLF-Dateien mit LF-Suchtext trotzdem treffen.
|
|
132
|
+
if (!cur.includes(o) && cur.includes("\r\n")) { o = o.replace(/\r?\n/g, "\r\n"); n = n.replace(/\r?\n/g, "\r\n"); }
|
|
133
|
+
const count = cur.split(o).length - 1;
|
|
134
|
+
if (count === 0) return { error: "Text nicht gefunden in " + file + " – lies die Datei neu und nimm den exakten Text (inkl. Einrückung)." };
|
|
135
|
+
if (count > 1 && !all) return { error: "Text kommt " + count + "-mal vor in " + file + ' – nimm mehr umgebenden Kontext oder setze "all":true.' };
|
|
136
|
+
const at = cur.indexOf(o);
|
|
137
|
+
const next = all ? cur.split(o).join(n) : cur.slice(0, at) + n + cur.slice(at + o.length);
|
|
138
|
+
fs.writeFileSync(file, next);
|
|
139
|
+
return { diff: lineDiff(cur, next), count: all ? count : 1 };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function globToRe(glob) {
|
|
143
|
+
const g = String(glob).replace(/\\/g, "/").replace(/[.+^${}()|[\]]/g, "\\$&")
|
|
144
|
+
.replace(/\*\*\/?/g, "\u0001").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0001/g, ".*");
|
|
145
|
+
return new RegExp("(^|/)" + g + "$", "i");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Projekt durchsuchen (Regex, Groß-/Kleinschreibung egal, ohne node_modules & Co.). */
|
|
149
|
+
function searchFiles(root, pattern, glob, max = 200) {
|
|
150
|
+
let re;
|
|
151
|
+
try { re = new RegExp(pattern, "i"); } catch { re = new RegExp(String(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); }
|
|
152
|
+
const gre = glob ? globToRe(glob) : null;
|
|
153
|
+
const hits = [];
|
|
154
|
+
let files = 0;
|
|
155
|
+
const walk = (dir) => {
|
|
156
|
+
let entries;
|
|
157
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
158
|
+
for (const e of entries) {
|
|
159
|
+
if (hits.length >= max || files > 20000) return;
|
|
160
|
+
const full = path.join(dir, e.name);
|
|
161
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(full); continue; }
|
|
162
|
+
const rel = path.relative(root, full).split(path.sep).join("/");
|
|
163
|
+
if (gre && !gre.test(rel)) continue;
|
|
164
|
+
let st; try { st = fs.statSync(full); } catch { continue; }
|
|
165
|
+
if (st.size > 1e6) continue;
|
|
166
|
+
files++;
|
|
167
|
+
const text = readText(full);
|
|
168
|
+
if (text == null || text.includes("\u0000")) continue;
|
|
169
|
+
const lines = text.split(/\r?\n/);
|
|
170
|
+
for (let i = 0; i < lines.length && hits.length < max; i++) {
|
|
171
|
+
if (re.test(lines[i])) hits.push(rel + ":" + (i + 1) + ": " + lines[i].trim().slice(0, 200));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
walk(root);
|
|
176
|
+
if (!hits.length) return "Keine Treffer für /" + pattern + "/" + (glob ? " in " + glob : "") + ".";
|
|
177
|
+
return hits.join("\n") + (hits.length >= max ? "\n… (mehr als " + max + " Treffer – Suche eingrenzen)" : "");
|
|
178
|
+
}
|
|
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
|
+
|
|
212
|
+
/** read/write/edit/search/ls ausführen. `run` macht jede Oberfläche selbst (Live-Ausgabe). */
|
|
213
|
+
function fileTool(t, root) {
|
|
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
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
if (t.name === "read") {
|
|
222
|
+
const c = readText(P(t.path));
|
|
223
|
+
return { result: c == null ? "FEHLER: Datei nicht gefunden: " + t.path : c.slice(0, 20000) + (c.length > 20000 ? "\n…[gekürzt – Datei ist länger]" : "") };
|
|
224
|
+
}
|
|
225
|
+
if (t.name === "write") {
|
|
226
|
+
const file = P(t.path), before = readText(file) ?? "", content = String(t.content ?? "");
|
|
227
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
228
|
+
fs.writeFileSync(file, content);
|
|
229
|
+
const d = lineDiff(before, content);
|
|
230
|
+
return { result: "OK geschrieben: " + t.path + " (+" + d.added + " −" + d.removed + ")", diff: d };
|
|
231
|
+
}
|
|
232
|
+
if (t.name === "edit") {
|
|
233
|
+
const r = editFile(P(t.path), t.old, t.new, t.all);
|
|
234
|
+
if (r.error) return { result: "FEHLER: " + r.error };
|
|
235
|
+
return { result: "OK geändert: " + t.path + " (" + r.count + "×, +" + r.diff.added + " −" + r.diff.removed + ")", diff: r.diff };
|
|
236
|
+
}
|
|
237
|
+
if (t.name === "search") return { result: searchFiles(P(t.path), String(t.pattern || ""), t.glob) };
|
|
238
|
+
if (t.name === "ls") return { result: fs.readdirSync(P(t.path), { withFileTypes: true }).map((e) => (e.isDirectory() ? "[D] " : " ") + e.name).join("\n") || "(leer)" };
|
|
239
|
+
return { result: "Unbekanntes Werkzeug: " + t.name };
|
|
240
|
+
} catch (e) { return { result: "FEHLER: " + e.message }; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------- Web: Seite lesen & Suche ----------
|
|
244
|
+
const WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 MythosCode";
|
|
245
|
+
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", auml: "ä", ouml: "ö", uuml: "ü", Auml: "Ä", Ouml: "Ö", Uuml: "Ü", szlig: "ß" };
|
|
246
|
+
const decodeEntities = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e) =>
|
|
247
|
+
e[0] === "#" ? String.fromCodePoint(e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : +e.slice(1)) : ENTITIES[e] ?? m);
|
|
248
|
+
function htmlToText(html) {
|
|
249
|
+
return decodeEntities(html
|
|
250
|
+
.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi, " ")
|
|
251
|
+
.replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|pre|blockquote)>|<br\s*\/?>/gi, "\n")
|
|
252
|
+
.replace(/<li[^>]*>/gi, "• ")
|
|
253
|
+
.replace(/<[^>]+>/g, " "))
|
|
254
|
+
.replace(/[ \t\f\v]+/g, " ").replace(/\n\s*\n\s*/g, "\n\n").trim();
|
|
255
|
+
}
|
|
256
|
+
async function webGet(url) {
|
|
257
|
+
const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 20000);
|
|
258
|
+
try {
|
|
259
|
+
const r = await fetch(url, { headers: { "user-agent": WEB_UA, "accept-language": "de,en;q=0.8" }, redirect: "follow", signal: ctl.signal });
|
|
260
|
+
const type = r.headers.get("content-type") || "";
|
|
261
|
+
const buf = Buffer.from(await r.arrayBuffer()).subarray(0, 3e6);
|
|
262
|
+
return { ok: r.ok, status: r.status, type, text: buf.toString("utf8"), url: r.url };
|
|
263
|
+
} finally { clearTimeout(to); }
|
|
264
|
+
}
|
|
265
|
+
async function webTool(t) {
|
|
266
|
+
try {
|
|
267
|
+
if (t.name === "fetch") {
|
|
268
|
+
const url = String(t.url || "");
|
|
269
|
+
if (!/^https?:\/\//i.test(url)) return { result: "FEHLER: Bitte eine vollständige http(s)-URL angeben." };
|
|
270
|
+
const r = await webGet(url);
|
|
271
|
+
if (!r.ok) return { result: "FEHLER: HTTP " + r.status + " für " + url };
|
|
272
|
+
const text = /html/i.test(r.type) ? htmlToText(r.text) : r.text;
|
|
273
|
+
return { result: "Inhalt von " + r.url + ":\n\n" + text.slice(0, 20000) + (text.length > 20000 ? "\n…[gekürzt]" : "") };
|
|
274
|
+
}
|
|
275
|
+
if (t.name === "websearch") {
|
|
276
|
+
const q = String(t.query || "").trim();
|
|
277
|
+
if (!q) return { result: "FEHLER: query fehlt." };
|
|
278
|
+
const r = await webGet("https://html.duckduckgo.com/html/?q=" + encodeURIComponent(q));
|
|
279
|
+
const hits = [];
|
|
280
|
+
// Jeder Treffer ist ein eigener Block mit Titel-Link und (meist) Kurzbeschreibung.
|
|
281
|
+
for (const block of r.text.split(/<div[^>]+class="[^"]*\bresult\b/).slice(1)) {
|
|
282
|
+
if (hits.length >= 8) break;
|
|
283
|
+
const a = block.match(/class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
|
|
284
|
+
if (!a) continue;
|
|
285
|
+
let href = decodeEntities(a[1]);
|
|
286
|
+
const u = href.match(/[?&]uddg=([^&]+)/); if (u) href = decodeURIComponent(u[1]);
|
|
287
|
+
if (/duckduckgo\.com\/y\.js|result--ad/.test(href + block.slice(0, 200))) continue; // Werbung
|
|
288
|
+
const sn = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/(?:a|div)>/);
|
|
289
|
+
hits.push((hits.length + 1) + ". " + htmlToText(a[2]) + "\n " + href + (sn ? "\n " + htmlToText(sn[1]).slice(0, 300) : ""));
|
|
290
|
+
}
|
|
291
|
+
return { result: hits.length ? "Suchergebnisse für „" + q + "“:\n\n" + hits.join("\n\n") + "\n\nMit fetch kannst du eine Seite genauer lesen." : "Keine Suchergebnisse für „" + q + "“." };
|
|
292
|
+
}
|
|
293
|
+
return { result: "Unbekanntes Werkzeug: " + t.name };
|
|
294
|
+
} catch (e) { return { result: "FEHLER: " + (e.name === "AbortError" ? "Zeitüberschreitung" : e.message) }; }
|
|
295
|
+
}
|
|
296
|
+
const isWebTool = (t) => t.name === "fetch" || t.name === "websearch";
|
|
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
|
+
|
|
376
|
+
function readMemory(root) { const t = readText(path.join(root, MEMORY_FILE)); return t ? t.slice(0, 8000) : ""; }
|
|
377
|
+
function addMemory(root, text) {
|
|
378
|
+
const f = path.join(root, MEMORY_FILE);
|
|
379
|
+
const cur = readText(f) || "# Projekt-Gedächtnis\n\nMythos liest diese Datei bei jeder Aufgabe mit.\n";
|
|
380
|
+
fs.writeFileSync(f, cur.replace(/\s*$/, "\n") + "- " + String(text).trim() + "\n");
|
|
381
|
+
return f;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Handy-Benachrichtigung: ntfy.sh-Thema, Discord-Webhook oder beliebige URL (JSON-POST). */
|
|
385
|
+
async function pushNotify(url, title, message) {
|
|
386
|
+
if (!url) return false;
|
|
387
|
+
try {
|
|
388
|
+
const u = new URL(url);
|
|
389
|
+
let target = url, body;
|
|
390
|
+
if (/(^|\.)discord(app)?\.com$/.test(u.hostname)) body = { content: "**" + title + "**\n" + message };
|
|
391
|
+
else if (/ntfy/.test(u.hostname)) { target = u.origin; body = { topic: u.pathname.replace(/^\/+|\/+$/g, ""), title, message }; }
|
|
392
|
+
else body = { title, message };
|
|
393
|
+
const r = await fetch(target, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
394
|
+
return r.ok;
|
|
395
|
+
} catch { return false; }
|
|
396
|
+
}
|
|
397
|
+
|
|
8
398
|
|
|
9
399
|
const API = process.env.MYTHOS_API_URL || "https://rdxaqiacoitzeowehdts.supabase.co/functions/v1/v1-messages";
|
|
10
400
|
const CFG = path.join(os.homedir(), ".mythos-code.json");
|
|
11
|
-
const
|
|
401
|
+
const ROOT = process.cwd();
|
|
402
|
+
const C = { c: "\x1b[36m", g: "\x1b[32m", y: "\x1b[33m", r: "\x1b[31m", m: "\x1b[35m", d: "\x1b[2m", b: "\x1b[1m", x: "\x1b[0m" };
|
|
12
403
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
404
|
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
14
405
|
const load = () => { try { return JSON.parse(fs.readFileSync(CFG, "utf8")); } catch { return {}; } };
|
|
15
|
-
|
|
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 */ } };
|
|
16
408
|
let cfg = load();
|
|
17
409
|
let autoYes = process.argv.includes("--yes");
|
|
410
|
+
let goal = "";
|
|
18
411
|
|
|
19
|
-
const SYSTEM =
|
|
20
|
-
|
|
21
|
-
<tool>{"name":"run","cmd":"..."}</tool> -> Shell-Befehl ausführen
|
|
22
|
-
<tool>{"name":"read","path":"..."}</tool> -> Datei lesen
|
|
23
|
-
<tool>{"name":"write","path":"...","content":"..."}</tool> -> Datei schreiben/erstellen
|
|
24
|
-
<tool>{"name":"ls","path":"."}</tool> -> Ordner auflisten
|
|
25
|
-
Nach jedem Werkzeug bekommst du das Ergebnis. Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool>. Sei präzise.`;
|
|
412
|
+
const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
|
|
413
|
+
TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
|
|
26
414
|
|
|
27
415
|
const SITE = process.env.MYTHOS_SITE_URL || "https://ai-mythos.lovable.app";
|
|
28
416
|
const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
|
|
@@ -63,30 +451,93 @@ async function login() {
|
|
|
63
451
|
|
|
64
452
|
const fmt = (ms) => { const s = Math.floor(ms / 1000); return (s >= 60 ? Math.floor(s / 60) + "m " : "") + (s % 60) + "s"; };
|
|
65
453
|
|
|
66
|
-
// Live-Timer: läuft während der ganzen Aufgabe
|
|
454
|
+
// Live-Timer: läuft während der ganzen Aufgabe, pausiert für Ausgaben und Rückfragen.
|
|
67
455
|
let T0 = 0, tick = null;
|
|
68
456
|
const clearLine = () => process.stdout.write("\r\x1b[K");
|
|
69
|
-
const startTick = () => { if (tick) return; tick = setInterval(() => process.stdout.write("\r" + C.d + "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + C.x + " "), 250); };
|
|
457
|
+
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); };
|
|
70
458
|
const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
|
|
71
459
|
const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
|
|
72
460
|
|
|
461
|
+
// ---------- Stoppen mit Strg+C ----------
|
|
462
|
+
let working = false, stopped = false, ctl = null, child = null;
|
|
463
|
+
function killChild() {
|
|
464
|
+
if (!child) return;
|
|
465
|
+
try { if (process.platform === "win32") exec("taskkill /pid " + child.pid + " /T /F"); else child.kill("SIGTERM"); } catch {}
|
|
466
|
+
}
|
|
467
|
+
rl.on("SIGINT", () => {
|
|
468
|
+
if (!working) { console.log(); rl.close(); process.exit(0); }
|
|
469
|
+
if (stopped) return;
|
|
470
|
+
stopped = true; killChild(); if (ctl) ctl.abort();
|
|
471
|
+
say(C.r + "■ Wird gestoppt…" + C.x);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ---------- Markdown im Terminal ----------
|
|
475
|
+
function mdLine(line, st) {
|
|
476
|
+
if (/^\s*```/.test(line)) { st.code = !st.code; return C.d + (st.code ? "┌─ " + line.trim().slice(3) : "└─") + C.x; }
|
|
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
|
+
}
|
|
484
|
+
const h = line.match(/^(#{1,6})\s+(.*)/);
|
|
485
|
+
if (h) return C.b + C.m + h[2] + C.x;
|
|
486
|
+
return line
|
|
487
|
+
.replace(/^(\s*)[-*]\s+/, "$1• ")
|
|
488
|
+
.replace(/\*\*(.+?)\*\*/g, C.b + "$1" + C.x)
|
|
489
|
+
.replace(/`([^`]+)`/g, C.c + "$1" + C.x)
|
|
490
|
+
.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, "$1 " + C.d + "($2)" + C.x);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Antwort live mitlesen: fertige Zeilen sofort formatiert ausgeben, <tool>-Blöcke ausblenden.
|
|
494
|
+
function streamPrinter() {
|
|
495
|
+
let printed = 0, buf = "", started = false;
|
|
496
|
+
const st = { code: false };
|
|
497
|
+
const line = (l) => {
|
|
498
|
+
stopTick();
|
|
499
|
+
if (!started) { if (!l.trim()) { startTick(); return; } started = true; process.stdout.write(C.b + "Mythos: " + C.x); }
|
|
500
|
+
console.log(mdLine(l, st));
|
|
501
|
+
startTick();
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
push(full) {
|
|
505
|
+
let vis = full;
|
|
506
|
+
const k = vis.indexOf("<tool>");
|
|
507
|
+
if (k >= 0) vis = vis.slice(0, k);
|
|
508
|
+
else for (let n = 5; n > 0; n--) if (vis.endsWith("<tool>".slice(0, n))) { vis = vis.slice(0, -n); break; }
|
|
509
|
+
if (vis.length <= printed) return;
|
|
510
|
+
buf += vis.slice(printed); printed = vis.length;
|
|
511
|
+
let i; while ((i = buf.indexOf("\n")) !== -1) { line(buf.slice(0, i)); buf = buf.slice(i + 1); }
|
|
512
|
+
},
|
|
513
|
+
end() { if (buf.trim()) line(buf); buf = ""; if (started) { stopTick(); console.log(); startTick(); } },
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------- Modell ----------
|
|
73
518
|
// Verlauf kürzen, damit lange Aufgaben nicht an zu großem Kontext scheitern. level 0 = mild, 2 = stark.
|
|
74
519
|
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));
|
|
520
|
+
const withImages = (m, content) => m.images && m.images.length
|
|
521
|
+
? { role: m.role, content: [{ type: "text", text: content }].concat(m.images.map((i) => ({ type: "image", source: { type: "base64", media_type: i.media_type, data: i.data } }))) }
|
|
522
|
+
: { role: m.role, content };
|
|
75
523
|
function compact(history, level) {
|
|
76
524
|
const keep = [6, 8, 4][level], recentMax = [20000, 3000, 1500][level], oldMax = [1500, 800, 400][level];
|
|
77
525
|
const cut = Math.max(0, history.length - keep);
|
|
78
526
|
const older = level === 0 ? history.slice(0, cut) : history.slice(0, Math.min(1, cut));
|
|
79
527
|
return older.map((m) => ({ role: m.role, content: shorten(m.content, oldMax) }))
|
|
80
|
-
.concat(history.slice(cut).map((m) => (
|
|
528
|
+
.concat(history.slice(cut).map((m) => withImages(m, shorten(m.content, recentMax))));
|
|
81
529
|
}
|
|
82
530
|
|
|
83
|
-
|
|
84
|
-
|
|
531
|
+
let tokens = { in: 0, out: 0 };
|
|
532
|
+
async function once(messages, onText) {
|
|
533
|
+
const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
|
|
534
|
+
const system = SYSTEM();
|
|
535
|
+
tokens.in += estimateTokens(system, messages);
|
|
85
536
|
try {
|
|
86
537
|
const r = await fetch(API, {
|
|
87
|
-
method: "POST", signal:
|
|
538
|
+
method: "POST", signal: c.signal,
|
|
88
539
|
headers: { "content-type": "application/json", "x-api-key": cfg.key, "anthropic-version": "2023-06-01" },
|
|
89
|
-
body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system
|
|
540
|
+
body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system, messages, stream: true }),
|
|
90
541
|
});
|
|
91
542
|
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; }
|
|
92
543
|
const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "";
|
|
@@ -98,18 +549,21 @@ async function once(messages) {
|
|
|
98
549
|
if (!line.startsWith("data:")) continue;
|
|
99
550
|
let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
|
|
100
551
|
if (p.type === "error") { const e = new Error(p.error?.message || "Überlastet"); e.status = 529; throw e; }
|
|
101
|
-
if (p.delta?.text) out += p.delta.text;
|
|
552
|
+
if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
|
|
102
553
|
}
|
|
103
554
|
}
|
|
104
555
|
if (!out.trim()) throw new Error("Leere Antwort");
|
|
556
|
+
tokens.out += Math.ceil(out.length / 4);
|
|
105
557
|
return out;
|
|
106
558
|
} finally { clearTimeout(to); }
|
|
107
559
|
}
|
|
108
560
|
|
|
109
|
-
async function call(history) {
|
|
561
|
+
async function call(history, onText) {
|
|
110
562
|
for (let a = 0; a < 6; a++) {
|
|
111
|
-
|
|
563
|
+
if (stopped) throw new Error("Gestoppt");
|
|
564
|
+
try { return await once(compact(history, Math.min(2, Math.floor(a / 2))), onText); }
|
|
112
565
|
catch (e) {
|
|
566
|
+
if (stopped) throw e;
|
|
113
567
|
if (e.status === 401) throw new Error("Anmeldung ungültig – bitte neu anmelden: mythos login");
|
|
114
568
|
if (/Daily limit/i.test(e.message)) throw new Error("Tageslimit erreicht – mit Pro unbegrenzt.");
|
|
115
569
|
await new Promise((r) => setTimeout(r, Math.min(8000, 1500 * (a + 1))));
|
|
@@ -118,44 +572,313 @@ async function call(history) {
|
|
|
118
572
|
throw new Error("Mythos ist gerade nicht erreichbar – bitte gleich nochmal versuchen.");
|
|
119
573
|
}
|
|
120
574
|
|
|
575
|
+
// ---------- Werkzeuge ----------
|
|
576
|
+
function diffText(d) {
|
|
577
|
+
const shown = d.lines.slice(0, 80).map(([op, l]) =>
|
|
578
|
+
op === "+" ? C.g + " + " + l + C.x : op === "-" ? C.r + " - " + l + C.x : op === "@" ? C.d + " " + l + C.x : C.d + " " + l + C.x);
|
|
579
|
+
if (d.lines.length > 80) shown.push(C.d + " … (" + (d.lines.length - 80) + " weitere Zeilen)" + C.x);
|
|
580
|
+
return shown.join("\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Befehl ausführen und die Ausgabe live im Terminal zeigen.
|
|
584
|
+
function runLive(cmd) {
|
|
585
|
+
stopTick();
|
|
586
|
+
return new Promise((res) => {
|
|
587
|
+
let out = "";
|
|
588
|
+
child = spawn(cmd, { shell: true, cwd: ROOT, env: process.env });
|
|
589
|
+
const on = (decode) => (d) => { const s = decode(d); out += s; if (out.length > 200000) out = out.slice(-100000); process.stdout.write(C.d + s + C.x); };
|
|
590
|
+
child.stdout.on("data", on(outputDecoder())); child.stderr.on("data", on(outputDecoder()));
|
|
591
|
+
const to = setTimeout(() => { out += "\n[Nach 10 Minuten abgebrochen]"; killChild(); }, 600000);
|
|
592
|
+
child.on("error", (e) => { out += "\nFEHLER: " + e.message; });
|
|
593
|
+
child.on("close", (code) => {
|
|
594
|
+
clearTimeout(to); child = null;
|
|
595
|
+
if (out && !out.endsWith("\n")) process.stdout.write("\n");
|
|
596
|
+
startTick();
|
|
597
|
+
res((out + (code ? "\nExit: " + code : "")).slice(-8000) || "(keine Ausgabe)");
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
121
602
|
async function runTool(t) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
610
|
+
const label = t.name === "run" ? "run " + t.cmd
|
|
611
|
+
: t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
|
|
612
|
+
: t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
|
|
613
|
+
: t.name === "websearch" ? "websearch " + t.query
|
|
614
|
+
: t.name === "fetch" ? "fetch " + t.url
|
|
615
|
+
: t.name + " " + (t.path || "");
|
|
616
|
+
say(C.y + "⚙ " + label + C.x);
|
|
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) {
|
|
125
628
|
stopTick();
|
|
126
629
|
const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
|
|
127
630
|
startTick();
|
|
128
631
|
if (a === "a") autoYes = true; else if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt.";
|
|
129
632
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
633
|
+
if (t.name === "run") return runLive(t.cmd);
|
|
634
|
+
if (isWebTool(t)) {
|
|
635
|
+
const r = await webTool(t);
|
|
636
|
+
say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
|
|
637
|
+
return r.result;
|
|
638
|
+
}
|
|
639
|
+
const r = fileTool(t, ROOT);
|
|
640
|
+
if (r.diff && r.diff.lines.length) say(diffText(r.diff));
|
|
641
|
+
return r.result;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ---------- Eine Aufgabe (auch /goal) ----------
|
|
645
|
+
async function turnInner(history) {
|
|
646
|
+
const max = goal ? 4000 : 40;
|
|
647
|
+
let nudges = 0;
|
|
648
|
+
for (let i = 0; i < max && !stopped; i++) {
|
|
649
|
+
const pr = streamPrinter();
|
|
650
|
+
const out = await call(history, (full) => pr.push(full));
|
|
651
|
+
pr.end();
|
|
652
|
+
if (stopped) break;
|
|
150
653
|
history.push({ role: "assistant", content: out });
|
|
151
654
|
const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
655
|
+
if (!m) {
|
|
656
|
+
if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
|
|
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"; }
|
|
658
|
+
history.push({ role: "user", content: GOAL_NUDGE });
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
nudges = 0;
|
|
155
662
|
let t; try { t = JSON.parse(m[1]); } catch { history.push({ role: "user", content: "Tool-JSON ungültig." }); continue; }
|
|
156
663
|
const res = await runTool(t);
|
|
157
664
|
history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + res });
|
|
158
665
|
}
|
|
666
|
+
if (!stopped) say(C.y + "Schrittlimit (" + max + ") erreicht – mit /weiter macht Mythos weiter." + C.x);
|
|
667
|
+
return stopped ? "stopped" : "limit";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function summarize(history) {
|
|
671
|
+
history.push({ role: "user", content: SUMMARY_PROMPT });
|
|
672
|
+
say(C.m + C.b + "📋 Zusammenfassung" + C.x);
|
|
673
|
+
const pr = streamPrinter();
|
|
674
|
+
const out = await call(history, (full) => pr.push(full));
|
|
675
|
+
pr.end();
|
|
676
|
+
history.push({ role: "assistant", content: out });
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function turn(history, input, images) {
|
|
681
|
+
if (input != null) history.push({ role: "user", content: input, images });
|
|
682
|
+
T0 = Date.now(); working = true; stopped = false; startTick();
|
|
683
|
+
const tok0 = tokens.in + tokens.out;
|
|
684
|
+
const wasGoal = goal;
|
|
685
|
+
let status = "error", summary = "";
|
|
686
|
+
try {
|
|
687
|
+
status = await turnInner(history);
|
|
688
|
+
if (wasGoal && status !== "stopped" && !stopped) summary = await summarize(history);
|
|
689
|
+
} catch (e) { if (!stopped) say(C.r + "⚠ " + e.message + C.x); }
|
|
690
|
+
if (stopped) { status = "stopped"; if (history[history.length - 1]?.role === "user") history.push({ role: "assistant", content: "(Vom Nutzer gestoppt.)" }); }
|
|
691
|
+
working = false; stopTick();
|
|
692
|
+
const took = fmt(Date.now() - T0);
|
|
693
|
+
console.log((stopped ? C.r + "■ Gestoppt nach " + took : C.g + "✓ Mythos hat " + took + " gearbeitet") + C.x +
|
|
694
|
+
C.d + " · ≈ " + fmtTokens(tokens.in + tokens.out - tok0) + " Tokens (Sitzung ≈ " + fmtTokens(tokens.in + tokens.out) + ")" + C.x + "\n");
|
|
695
|
+
if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
|
|
696
|
+
// Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
|
|
697
|
+
if (cfg.notify && (wasGoal || Date.now() - T0 > 60000)) {
|
|
698
|
+
const title = status === "reached" ? "🎯 Ziel erreicht" : status === "done" ? "✓ Mythos ist fertig" : status === "stopped" ? "■ Gestoppt" : "⚠ Mythos braucht dich";
|
|
699
|
+
pushNotify(cfg.notify, "Mythos Code – " + path.basename(ROOT), title + " (" + took + ")" + (summary ? "\n\n" + summary.slice(0, 1500) : ""));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ---------- Git ----------
|
|
704
|
+
const git = (args) => {
|
|
705
|
+
try { return { ok: true, out: execFileSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1e7 }) }; }
|
|
706
|
+
catch (e) { return { ok: false, out: ((e.stdout || "") + (e.stderr || "")) || e.message }; }
|
|
707
|
+
};
|
|
708
|
+
const gitBranch = () => { const b = git(["rev-parse", "--abbrev-ref", "HEAD"]); return b.ok ? b.out.trim() : ""; };
|
|
709
|
+
|
|
710
|
+
async function commit(msg) {
|
|
711
|
+
const st = git(["status", "--porcelain"]);
|
|
712
|
+
if (!st.ok) return say(C.r + "Kein Git-Repository: " + st.out.trim() + C.x);
|
|
713
|
+
if (!st.out.trim()) return say(C.d + "Nichts zu committen – alles sauber." + C.x);
|
|
714
|
+
if (!msg) {
|
|
715
|
+
const diff = git(["diff", "HEAD"]).out || git(["diff"]).out;
|
|
716
|
+
process.stdout.write(C.d + "Mythos schreibt die Commit-Nachricht…" + C.x);
|
|
717
|
+
stopped = false;
|
|
718
|
+
try {
|
|
719
|
+
msg = (await call([{ role: "user", content: "Schreibe eine kurze Git-Commit-Nachricht auf Deutsch für diese Änderungen: erste Zeile max. 72 Zeichen, optional Leerzeile + Stichpunkte. Antworte NUR mit der Nachricht, ohne Werkzeuge, ohne Codeblock.\n\n" + st.out + "\n" + diff.slice(0, 20000) }]))
|
|
720
|
+
.replace(/<tool>[\s\S]*?<\/tool>/g, "").replace(/^```\w*\n?|```$/g, "").trim();
|
|
721
|
+
} catch (e) { clearLine(); return say(C.r + "⚠ " + e.message + C.x); }
|
|
722
|
+
clearLine();
|
|
723
|
+
}
|
|
724
|
+
console.log(C.b + "Commit-Nachricht:" + C.x + "\n" + C.c + msg + C.x);
|
|
725
|
+
if (!autoYes && !/^[jy]/i.test((await ask(C.d + "Committen? [j/N] " + C.x)).trim())) return say("Abgebrochen.");
|
|
726
|
+
git(["add", "-A"]);
|
|
727
|
+
const c = git(["commit", "-m", msg]);
|
|
728
|
+
say((c.ok ? C.g : C.r) + c.out.trim() + C.x);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ---------- Bilder: Pfade zu Bilddateien in der Eingabe (z. B. per Drag & Drop ins Terminal) ----------
|
|
732
|
+
const IMG = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
733
|
+
function extractImages(input) {
|
|
734
|
+
const images = [];
|
|
735
|
+
const text = input.replace(/"([^"]+\.(png|jpe?g|gif|webp))"|'([^']+\.(png|jpe?g|gif|webp))'|(\S+\.(png|jpe?g|gif|webp))/gi, (all, a, _1, b, _2, c) => {
|
|
736
|
+
const p = path.resolve(ROOT, a || b || c);
|
|
737
|
+
try {
|
|
738
|
+
const st = fs.statSync(p);
|
|
739
|
+
if (!st.isFile() || st.size > 5e6 || images.length >= 5) return all;
|
|
740
|
+
images.push({ media_type: IMG[p.split(".").pop().toLowerCase()], data: fs.readFileSync(p).toString("base64") });
|
|
741
|
+
return "[Bild: " + path.basename(p) + "]";
|
|
742
|
+
} catch { return all; }
|
|
743
|
+
});
|
|
744
|
+
return { text, images };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const HELP = [
|
|
748
|
+
["/goal <ziel>", "Mythos arbeitet selbstständig, bis das Ziel erreicht ist (+ Zusammenfassung)"],
|
|
749
|
+
["/goal stop", "Ziel beenden"],
|
|
750
|
+
["/weiter", "Unterbrochene Arbeit fortsetzen"],
|
|
751
|
+
["/merken <text>", "Etwas ins Projekt-Gedächtnis (MYTHOS.md) schreiben"],
|
|
752
|
+
["/gedaechtnis", "Projekt-Gedächtnis anzeigen"],
|
|
753
|
+
["/git", "Git-Status anzeigen"],
|
|
754
|
+
["/commit [nachricht]", "Alles committen (ohne Nachricht schreibt Mythos sie)"],
|
|
755
|
+
["/push", "git push"],
|
|
756
|
+
["/handy <url>", "Handy-Benachrichtigung (ntfy.sh-Thema oder Discord-Webhook) · /handy test · /handy aus"],
|
|
757
|
+
["/vollzugriff an|aus", "Ohne Nachfragen arbeiten"],
|
|
758
|
+
["/modell [name]", "Modell anzeigen oder wechseln"],
|
|
759
|
+
["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
|
|
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"],
|
|
768
|
+
["/exit", "Beenden"],
|
|
769
|
+
];
|
|
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
|
+
|
|
806
|
+
async function command(q, history) {
|
|
807
|
+
const cmd = q.split(/\s+/)[0].toLowerCase(), arg = q.slice(cmd.length).trim();
|
|
808
|
+
if (cmd === "/hilfe" || cmd === "/help") return console.log(HELP.map(([c, d]) => " " + C.c + c.padEnd(22) + C.x + d).join("\n") + "\n");
|
|
809
|
+
if (cmd === "/clear") { history.length = 0; goal = ""; return console.log("Verlauf geleert.\n"); }
|
|
810
|
+
if (cmd === "/goal" || cmd === "/ziel") {
|
|
811
|
+
if (!arg) return console.log(goal ? "🎯 Aktuelles Ziel: " + goal : "So geht's: /goal <was Mythos erreichen soll>");
|
|
812
|
+
if (/^(stop|aus|ende|beenden)$/i.test(arg)) { goal = ""; return console.log("🎯 Ziel beendet.\n"); }
|
|
813
|
+
if (!autoYes) { autoYes = true; console.log(C.d + "Vollzugriff für /goal eingeschaltet – Mythos fragt nicht mehr nach." + C.x); }
|
|
814
|
+
goal = arg;
|
|
815
|
+
return turn(history, "Neues Ziel: " + arg + "\nArbeite jetzt komplett selbstständig daran, bis es erreicht und geprüft ist.");
|
|
816
|
+
}
|
|
817
|
+
if (cmd === "/weiter") {
|
|
818
|
+
if (!history.length) return console.log("Hier gibt es noch nichts zum Weitermachen.");
|
|
819
|
+
return turn(history, history[history.length - 1].role === "assistant" ? "Mach bitte genau dort weiter, wo du aufgehört hast." : null);
|
|
820
|
+
}
|
|
821
|
+
if (cmd === "/merken") { if (!arg) return console.log("So geht's: /merken <text>"); addMemory(ROOT, arg); return console.log(C.g + "✓ In " + MEMORY_FILE + " gemerkt." + C.x + "\n"); }
|
|
822
|
+
if (cmd === "/gedaechtnis" || cmd === "/gedächtnis") return console.log(readMemory(ROOT) || "Noch kein Projekt-Gedächtnis. Mit /merken <text> anlegen.", "\n");
|
|
823
|
+
if (cmd === "/git") { const s = git(["status", "--short", "--branch"]); return console.log((s.ok ? s.out : C.r + s.out + C.x).trim() + "\n"); }
|
|
824
|
+
if (cmd === "/commit") return commit(arg);
|
|
825
|
+
if (cmd === "/push") { const p = git(["push"]); return console.log((p.ok ? C.g : C.r) + p.out.trim() + C.x + "\n"); }
|
|
826
|
+
if (cmd === "/handy") {
|
|
827
|
+
if (/^(aus|off)$/i.test(arg)) { delete cfg.notify; save(cfg); return console.log("Handy-Benachrichtigung aus.\n"); }
|
|
828
|
+
if (/^test$/i.test(arg)) return console.log(cfg.notify && await pushNotify(cfg.notify, "Mythos Code", "Test – Benachrichtigungen funktionieren ✓") ? C.g + "✓ Gesendet." + C.x : C.r + "Senden fehlgeschlagen – URL prüfen." + C.x);
|
|
829
|
+
if (!/^https:\/\//i.test(arg)) return console.log("So geht's: App „ntfy“ aufs Handy, Thema abonnieren, dann:\n /handy https://ntfy.sh/dein-geheimes-thema\nOder einen Discord-Webhook-Link angeben.\n");
|
|
830
|
+
cfg.notify = arg; save(cfg);
|
|
831
|
+
return console.log(C.g + "✓ Gespeichert. Bei /goal und Aufgaben über 1 Minute bekommst du eine Nachricht. Test: /handy test" + C.x + "\n");
|
|
832
|
+
}
|
|
833
|
+
if (cmd === "/modell" || cmd === "/model") {
|
|
834
|
+
const cur = cfg.model || "mythos-code";
|
|
835
|
+
if (!arg) return console.log(MODELS.map((m) => (m.id === cur ? C.g + "● " : " ") + m.id.padEnd(15) + C.x + m.label + C.d + " – " + m.desc + C.x).join("\n") + "\n\nWechseln mit /modell <name>\n");
|
|
836
|
+
const m = MODELS.find((x) => x.id === arg.toLowerCase() || x.label.toLowerCase() === arg.toLowerCase());
|
|
837
|
+
if (!m) return console.log(C.r + "Unbekanntes Modell. Verfügbar: " + MODELS.map((x) => x.id).join(", ") + C.x + "\n");
|
|
838
|
+
cfg.model = m.id; save(cfg);
|
|
839
|
+
return console.log(C.g + "✓ Modell: " + m.label + C.x + "\n");
|
|
840
|
+
}
|
|
841
|
+
if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
|
|
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));
|
|
881
|
+
console.log("Unbekannter Befehl: " + cmd + " – /hilfe zeigt alle Befehle.\n");
|
|
159
882
|
}
|
|
160
883
|
|
|
161
884
|
async function main() {
|
|
@@ -163,15 +886,21 @@ async function main() {
|
|
|
163
886
|
if (args[0] === "login" || !cfg.key) await login();
|
|
164
887
|
if (args[0] === "login") process.exit(0);
|
|
165
888
|
if (args[0] === "logout") { save({}); console.log("Abgemeldet."); process.exit(0); }
|
|
166
|
-
|
|
889
|
+
const branch = gitBranch();
|
|
890
|
+
console.log(C.c + C.b + "\n ✦ MYTHOS CODE" + C.x + C.d + " – by Mythoscraft · " + path.basename(ROOT) + (branch ? " ⎇ " + branch : "") +
|
|
891
|
+
(readMemory(ROOT) ? " · Gedächtnis ✓" : "") + "\n /hilfe für Befehle · Strg+C stoppt · Bilder: Datei ins Terminal ziehen\n" + C.x);
|
|
167
892
|
const history = [];
|
|
168
|
-
if (args.length) {
|
|
893
|
+
if (args.length) { const { text, images } = extractImages(args.join(" ")); await turn(history, text, images); }
|
|
169
894
|
while (true) {
|
|
170
|
-
const q = (await ask(C.c + "› " + C.x)).trim();
|
|
895
|
+
const q = (await ask(C.c + (goal ? "🎯 " : "") + "› " + C.x)).trim();
|
|
171
896
|
if (!q) continue;
|
|
172
897
|
if (q === "/exit") break;
|
|
173
|
-
if (q
|
|
174
|
-
|
|
898
|
+
if (q.startsWith("/")) { await command(q, history); continue; }
|
|
899
|
+
const { text, images } = extractImages(q);
|
|
900
|
+
if (images.length) console.log(C.d + "🖼 " + images.length + " Bild(er) angehängt" + C.x);
|
|
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);
|
|
175
904
|
}
|
|
176
905
|
rl.close();
|
|
177
906
|
}
|