mythos-code 1.2.0 → 1.5.0

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 +588 -57
  2. package/package.json +1 -1
package/bin/mythos.js CHANGED
@@ -1,62 +1,414 @@
1
1
  #!/usr/bin/env node
2
- // Mythos Code CLI – Agent mit Zugriff auf Dateien & Shell (mit Bestätigung)
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
+ "Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
24
+ "Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool> (Markdown erlaubt).",
25
+ ].join("\n");
26
+
27
+ // Modelle, die v1-messages versteht (siehe mapModel dort).
28
+ const MODELS = [
29
+ { id: "mythos-code", label: "MythosCode", desc: "Standard fürs Programmieren" },
30
+ { id: "mythos-v2", label: "Mythos v2", desc: "am stärksten, etwas langsamer" },
31
+ { id: "mythos-sonnet", label: "Mythos v1", desc: "ausgewogen" },
32
+ { id: "mythos-lite", label: "Mythos Lite", desc: "am schnellsten" },
33
+ ];
34
+ /** Grobe Token-Schätzung (≈ 4 Zeichen pro Token, Bilder pauschal). */
35
+ function estimateTokens(system, messages) {
36
+ let chars = String(system || "").length, images = 0;
37
+ for (const m of messages) {
38
+ if (typeof m.content === "string") chars += m.content.length;
39
+ else for (const b of m.content || []) { if (b.type === "image") images++; else chars += String(b.text || "").length; }
40
+ }
41
+ return Math.ceil(chars / 4) + images * 1500;
42
+ }
43
+ const fmtTokens = (n) => (n >= 1e6 ? (n / 1e6).toFixed(1) + " Mio." : n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
44
+
45
+ const MEMORY_FILE = "MYTHOS.md";
46
+ const memoryPrompt = (mem) => mem
47
+ ? "\n\nProjekt-Gedächtnis (" + MEMORY_FILE + " im Projektordner – halte dich daran, ergänze es bei wichtigen neuen Erkenntnissen per edit):\n" + mem
48
+ : "";
49
+ 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
+ : "";
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.";
53
+ 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
+
55
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".cache", "vendor", "__pycache__", ".venv", "venv", "target"]);
56
+
57
+ function readText(file) { try { return fs.readFileSync(file, "utf8"); } catch { return null; } }
58
+
59
+ // Windows-Konsolenprogramme (cmd, dir, …) schreiben in Pipes im OEM-Zeichensatz (CP850), Node/Git in UTF-8.
60
+ const CP850 = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪº¿®¬½¼¡«»░▒▓│┤ÁÂÀ©╣║╗╝¢¥┐└┴┬├─┼ãÃ╚╔╩╦╠═╬¤ðÐÊËÈıÍÎÏ┘┌█▄¦Ì▀ÓßÔÒõÕµþÞÚÛÙýݯ´­±‗¾¶§÷¸°¨·¹³²■ ";
61
+ const fromCp850 = (b) => { let s = ""; for (const c of b) s += c < 128 ? String.fromCharCode(c) : CP850[c - 128]; return s; };
62
+ /** Decoder für eine Ausgabe-Pipe: jedes Stück als UTF-8, sonst (Windows) als CP850. Angefangene UTF-8-Zeichen werden aufgehoben. */
63
+ function outputDecoder() {
64
+ let pending = Buffer.alloc(0);
65
+ return (chunk) => {
66
+ let b = pending.length ? Buffer.concat([pending, chunk]) : chunk;
67
+ let i = b.length - 1, cont = 0;
68
+ while (i >= 0 && cont < 3 && (b[i] & 0xc0) === 0x80) { i--; cont++; }
69
+ const need = i >= 0 ? (b[i] >= 0xf0 ? 4 : b[i] >= 0xe0 ? 3 : b[i] >= 0xc0 ? 2 : 1) : 1;
70
+ const cut = need > 1 && b.length - i < need ? i : b.length;
71
+ pending = Buffer.from(b.subarray(cut)); b = b.subarray(0, cut);
72
+ try { return new TextDecoder("utf-8", { fatal: true }).decode(b); }
73
+ catch { return process.platform === "win32" ? fromCp850(b) : b.toString("latin1"); }
74
+ };
75
+ }
76
+
77
+ /** Zeilen-Diff mit Kontext: { added, removed, lines: [[op, text]] }, op = " " | "+" | "-" | "@" (Lücke). */
78
+ function lineDiff(a, b, ctx = 3, maxLines = 400) {
79
+ // Zeilenumbruch am Dateiende ist keine eigene Zeile.
80
+ const toLines = (t) => (t ? t.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n") : []);
81
+ const A = toLines(a), B = toLines(b);
82
+ let s = 0; while (s < A.length && s < B.length && A[s] === B[s]) s++;
83
+ let ea = A.length, eb = B.length; while (ea > s && eb > s && A[ea - 1] === B[eb - 1]) { ea--; eb--; }
84
+ const x = A.slice(s, ea), y = B.slice(s, eb);
85
+ let ops = [];
86
+ if (x.length * y.length <= 4e6) {
87
+ const n = x.length, m = y.length, L = [];
88
+ for (let i = 0; i <= n; i++) L.push(new Uint32Array(m + 1));
89
+ 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]);
90
+ let i = 0, j = 0;
91
+ while (i < n && j < m) {
92
+ if (x[i] === y[j]) { ops.push([" ", x[i]]); i++; j++; }
93
+ else if (L[i + 1][j] >= L[i][j + 1]) ops.push(["-", x[i++]]);
94
+ else ops.push(["+", y[j++]]);
95
+ }
96
+ while (i < n) ops.push(["-", x[i++]]);
97
+ while (j < m) ops.push(["+", y[j++]]);
98
+ } else ops = x.map((l) => ["-", l]).concat(y.map((l) => ["+", l]));
99
+ const all = A.slice(0, s).map((l) => [" ", l]).concat(ops, A.slice(ea).map((l) => [" ", l]));
100
+ const keep = new Uint8Array(all.length);
101
+ let added = 0, removed = 0;
102
+ all.forEach((o, k) => {
103
+ if (o[0] === " ") return;
104
+ if (o[0] === "+") added++; else removed++;
105
+ for (let d = Math.max(0, k - ctx); d <= Math.min(all.length - 1, k + ctx); d++) keep[d] = 1;
106
+ });
107
+ const lines = [];
108
+ let gap = false;
109
+ for (let k = 0; k < all.length; k++) {
110
+ if (!keep[k]) { gap = true; continue; }
111
+ if (lines.length >= maxLines) { lines.push(["@", "… (gekürzt)"]); break; }
112
+ if (gap && lines.length) lines.push(["@", "…"]);
113
+ gap = false; lines.push(all[k]);
114
+ }
115
+ return { added, removed, lines };
116
+ }
117
+
118
+ /** Gezieltes Ändern: `old` muss genau einmal vorkommen (oder all=true). */
119
+ function editFile(file, oldText, newText, all) {
120
+ const cur = readText(file);
121
+ if (cur == null) return { error: "Datei nicht gefunden: " + file };
122
+ if (!oldText) return { error: '"old" fehlt – gib den exakten Text an, der ersetzt werden soll.' };
123
+ let o = String(oldText), n = String(newText ?? "");
124
+ // CRLF-Dateien mit LF-Suchtext trotzdem treffen.
125
+ if (!cur.includes(o) && cur.includes("\r\n")) { o = o.replace(/\r?\n/g, "\r\n"); n = n.replace(/\r?\n/g, "\r\n"); }
126
+ const count = cur.split(o).length - 1;
127
+ if (count === 0) return { error: "Text nicht gefunden in " + file + " – lies die Datei neu und nimm den exakten Text (inkl. Einrückung)." };
128
+ if (count > 1 && !all) return { error: "Text kommt " + count + "-mal vor in " + file + ' – nimm mehr umgebenden Kontext oder setze "all":true.' };
129
+ const at = cur.indexOf(o);
130
+ const next = all ? cur.split(o).join(n) : cur.slice(0, at) + n + cur.slice(at + o.length);
131
+ fs.writeFileSync(file, next);
132
+ return { diff: lineDiff(cur, next), count: all ? count : 1 };
133
+ }
134
+
135
+ function globToRe(glob) {
136
+ const g = String(glob).replace(/\\/g, "/").replace(/[.+^${}()|[\]]/g, "\\$&")
137
+ .replace(/\*\*\/?/g, "\u0001").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0001/g, ".*");
138
+ return new RegExp("(^|/)" + g + "$", "i");
139
+ }
140
+
141
+ /** Projekt durchsuchen (Regex, Groß-/Kleinschreibung egal, ohne node_modules & Co.). */
142
+ function searchFiles(root, pattern, glob, max = 200) {
143
+ let re;
144
+ try { re = new RegExp(pattern, "i"); } catch { re = new RegExp(String(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); }
145
+ const gre = glob ? globToRe(glob) : null;
146
+ const hits = [];
147
+ let files = 0;
148
+ const walk = (dir) => {
149
+ let entries;
150
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
151
+ for (const e of entries) {
152
+ if (hits.length >= max || files > 20000) return;
153
+ const full = path.join(dir, e.name);
154
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(full); continue; }
155
+ const rel = path.relative(root, full).split(path.sep).join("/");
156
+ if (gre && !gre.test(rel)) continue;
157
+ let st; try { st = fs.statSync(full); } catch { continue; }
158
+ if (st.size > 1e6) continue;
159
+ files++;
160
+ const text = readText(full);
161
+ if (text == null || text.includes("\u0000")) continue;
162
+ const lines = text.split(/\r?\n/);
163
+ for (let i = 0; i < lines.length && hits.length < max; i++) {
164
+ if (re.test(lines[i])) hits.push(rel + ":" + (i + 1) + ": " + lines[i].trim().slice(0, 200));
165
+ }
166
+ }
167
+ };
168
+ walk(root);
169
+ if (!hits.length) return "Keine Treffer für /" + pattern + "/" + (glob ? " in " + glob : "") + ".";
170
+ return hits.join("\n") + (hits.length >= max ? "\n… (mehr als " + max + " Treffer – Suche eingrenzen)" : "");
171
+ }
172
+
173
+ /** read/write/edit/search/ls ausführen. `run` macht jede Oberfläche selbst (Live-Ausgabe). */
174
+ function fileTool(t, root) {
175
+ const P = (p) => path.resolve(root, p || ".");
176
+ try {
177
+ if (t.name === "read") {
178
+ const c = readText(P(t.path));
179
+ return { result: c == null ? "FEHLER: Datei nicht gefunden: " + t.path : c.slice(0, 20000) + (c.length > 20000 ? "\n…[gekürzt – Datei ist länger]" : "") };
180
+ }
181
+ if (t.name === "write") {
182
+ const file = P(t.path), before = readText(file) ?? "", content = String(t.content ?? "");
183
+ fs.mkdirSync(path.dirname(file), { recursive: true });
184
+ fs.writeFileSync(file, content);
185
+ const d = lineDiff(before, content);
186
+ return { result: "OK geschrieben: " + t.path + " (+" + d.added + " −" + d.removed + ")", diff: d };
187
+ }
188
+ if (t.name === "edit") {
189
+ const r = editFile(P(t.path), t.old, t.new, t.all);
190
+ if (r.error) return { result: "FEHLER: " + r.error };
191
+ return { result: "OK geändert: " + t.path + " (" + r.count + "×, +" + r.diff.added + " −" + r.diff.removed + ")", diff: r.diff };
192
+ }
193
+ if (t.name === "search") return { result: searchFiles(P(t.path), String(t.pattern || ""), t.glob) };
194
+ if (t.name === "ls") return { result: fs.readdirSync(P(t.path), { withFileTypes: true }).map((e) => (e.isDirectory() ? "[D] " : " ") + e.name).join("\n") || "(leer)" };
195
+ return { result: "Unbekanntes Werkzeug: " + t.name };
196
+ } catch (e) { return { result: "FEHLER: " + e.message }; }
197
+ }
198
+
199
+ // ---------- Web: Seite lesen & Suche ----------
200
+ const WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 MythosCode";
201
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", auml: "ä", ouml: "ö", uuml: "ü", Auml: "Ä", Ouml: "Ö", Uuml: "Ü", szlig: "ß" };
202
+ const decodeEntities = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e) =>
203
+ e[0] === "#" ? String.fromCodePoint(e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : +e.slice(1)) : ENTITIES[e] ?? m);
204
+ function htmlToText(html) {
205
+ return decodeEntities(html
206
+ .replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi, " ")
207
+ .replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|pre|blockquote)>|<br\s*\/?>/gi, "\n")
208
+ .replace(/<li[^>]*>/gi, "• ")
209
+ .replace(/<[^>]+>/g, " "))
210
+ .replace(/[ \t\f\v]+/g, " ").replace(/\n\s*\n\s*/g, "\n\n").trim();
211
+ }
212
+ async function webGet(url) {
213
+ const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 20000);
214
+ try {
215
+ const r = await fetch(url, { headers: { "user-agent": WEB_UA, "accept-language": "de,en;q=0.8" }, redirect: "follow", signal: ctl.signal });
216
+ const type = r.headers.get("content-type") || "";
217
+ const buf = Buffer.from(await r.arrayBuffer()).subarray(0, 3e6);
218
+ return { ok: r.ok, status: r.status, type, text: buf.toString("utf8"), url: r.url };
219
+ } finally { clearTimeout(to); }
220
+ }
221
+ async function webTool(t) {
222
+ try {
223
+ if (t.name === "fetch") {
224
+ const url = String(t.url || "");
225
+ if (!/^https?:\/\//i.test(url)) return { result: "FEHLER: Bitte eine vollständige http(s)-URL angeben." };
226
+ const r = await webGet(url);
227
+ if (!r.ok) return { result: "FEHLER: HTTP " + r.status + " für " + url };
228
+ const text = /html/i.test(r.type) ? htmlToText(r.text) : r.text;
229
+ return { result: "Inhalt von " + r.url + ":\n\n" + text.slice(0, 20000) + (text.length > 20000 ? "\n…[gekürzt]" : "") };
230
+ }
231
+ if (t.name === "websearch") {
232
+ const q = String(t.query || "").trim();
233
+ if (!q) return { result: "FEHLER: query fehlt." };
234
+ const r = await webGet("https://html.duckduckgo.com/html/?q=" + encodeURIComponent(q));
235
+ const hits = [];
236
+ // Jeder Treffer ist ein eigener Block mit Titel-Link und (meist) Kurzbeschreibung.
237
+ for (const block of r.text.split(/<div[^>]+class="[^"]*\bresult\b/).slice(1)) {
238
+ if (hits.length >= 8) break;
239
+ const a = block.match(/class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/);
240
+ if (!a) continue;
241
+ let href = decodeEntities(a[1]);
242
+ const u = href.match(/[?&]uddg=([^&]+)/); if (u) href = decodeURIComponent(u[1]);
243
+ if (/duckduckgo\.com\/y\.js|result--ad/.test(href + block.slice(0, 200))) continue; // Werbung
244
+ const sn = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/(?:a|div)>/);
245
+ hits.push((hits.length + 1) + ". " + htmlToText(a[2]) + "\n " + href + (sn ? "\n " + htmlToText(sn[1]).slice(0, 300) : ""));
246
+ }
247
+ 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 + "“." };
248
+ }
249
+ return { result: "Unbekanntes Werkzeug: " + t.name };
250
+ } catch (e) { return { result: "FEHLER: " + (e.name === "AbortError" ? "Zeitüberschreitung" : e.message) }; }
251
+ }
252
+ const isWebTool = (t) => t.name === "fetch" || t.name === "websearch";
253
+
254
+ function readMemory(root) { const t = readText(path.join(root, MEMORY_FILE)); return t ? t.slice(0, 8000) : ""; }
255
+ function addMemory(root, text) {
256
+ const f = path.join(root, MEMORY_FILE);
257
+ const cur = readText(f) || "# Projekt-Gedächtnis\n\nMythos liest diese Datei bei jeder Aufgabe mit.\n";
258
+ fs.writeFileSync(f, cur.replace(/\s*$/, "\n") + "- " + String(text).trim() + "\n");
259
+ return f;
260
+ }
261
+
262
+ /** Handy-Benachrichtigung: ntfy.sh-Thema, Discord-Webhook oder beliebige URL (JSON-POST). */
263
+ async function pushNotify(url, title, message) {
264
+ if (!url) return false;
265
+ try {
266
+ const u = new URL(url);
267
+ let target = url, body;
268
+ if (/(^|\.)discord(app)?\.com$/.test(u.hostname)) body = { content: "**" + title + "**\n" + message };
269
+ else if (/ntfy/.test(u.hostname)) { target = u.origin; body = { topic: u.pathname.replace(/^\/+|\/+$/g, ""), title, message }; }
270
+ else body = { title, message };
271
+ const r = await fetch(target, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
272
+ return r.ok;
273
+ } catch { return false; }
274
+ }
275
+
8
276
 
9
277
  const API = process.env.MYTHOS_API_URL || "https://rdxaqiacoitzeowehdts.supabase.co/functions/v1/v1-messages";
10
278
  const CFG = path.join(os.homedir(), ".mythos-code.json");
11
- const C = { c: "\x1b[36m", g: "\x1b[32m", y: "\x1b[33m", r: "\x1b[31m", d: "\x1b[2m", b: "\x1b[1m", x: "\x1b[0m" };
279
+ const ROOT = process.cwd();
280
+ 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
281
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13
282
  const ask = (q) => new Promise((r) => rl.question(q, r));
14
283
  const load = () => { try { return JSON.parse(fs.readFileSync(CFG, "utf8")); } catch { return {}; } };
15
284
  const save = (c) => fs.writeFileSync(CFG, JSON.stringify(c, null, 2));
16
285
  let cfg = load();
17
286
  let autoYes = process.argv.includes("--yes");
287
+ let goal = "";
18
288
 
19
- const SYSTEM = `Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: ${process.platform}, CWD: ${process.cwd()}).
20
- Du kannst Werkzeuge nutzen. Um ein Werkzeug aufzurufen, antworte mit GENAU EINEM Block:
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.`;
289
+ const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
290
+ TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
26
291
 
27
- async function login() {
28
- console.log(C.c + "Hol dir einen kostenlosen API-Key im Mythos Dashboard (sk-ant-mythos-...)." + C.x);
29
- const k = (await ask("API-Key: ")).trim();
292
+ const SITE = process.env.MYTHOS_SITE_URL || "https://ai-mythos.lovable.app";
293
+ const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
294
+ const authPost = async (body) => (await fetch(FN + "/cli-auth", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
295
+
296
+ function openBrowser(url) {
297
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
298
+ exec(cmd, () => {});
299
+ }
300
+
301
+ async function loginWithKey() {
302
+ const k = (await ask("API-Key (sk-ant-mythos-...): ")).trim();
30
303
  if (!k.startsWith("sk-ant-mythos-")) { console.log(C.r + "Ungültiger Key." + C.x); process.exit(1); }
31
- cfg.key = k; save(cfg); console.log(C.g + "✓ Gespeichert in " + CFG + C.x);
304
+ cfg.key = k; save(cfg); console.log(C.g + "✓ Angemeldet" + C.x);
305
+ }
306
+
307
+ // Anmeldung über die Website: Browser öffnet sich, dort auf "Authentifizieren" klicken.
308
+ async function login() {
309
+ if (process.argv.includes("--key")) return loginWithKey();
310
+ let s;
311
+ try { s = await authPost({ action: "start", client: "cli" }); } catch { s = null; }
312
+ if (!s?.code) { console.log(C.r + "Anmeldung gerade nicht möglich – versuch es gleich nochmal." + C.x); process.exit(1); }
313
+ const url = SITE + "/cli-auth?code=" + s.code;
314
+ console.log(C.c + "\n Anmeldung mit MythosAI" + C.x);
315
+ console.log(" Dein Browser öffnet sich. Klicke dort auf " + C.b + "Authentifizieren" + C.x + ".");
316
+ console.log(C.d + " Code: " + s.code + " · Falls sich nichts öffnet: " + url + C.x + "\n");
317
+ openBrowser(url);
318
+ const until = Date.now() + 15 * 60e3;
319
+ process.stdout.write(C.d + " Warte auf Bestätigung…" + C.x);
320
+ while (Date.now() < until) {
321
+ await new Promise((r) => setTimeout(r, 2000));
322
+ let p; try { p = await authPost({ action: "poll", code: s.code, poll_secret: s.poll_secret }); } catch { continue; }
323
+ if (p.status === "ok") { cfg.key = p.api_key; cfg.name = p.name; save(cfg); clearLine(); console.log(C.g + " ✓ Angemeldet als " + p.name + C.x + "\n"); return; }
324
+ if (p.status === "expired") break;
325
+ }
326
+ clearLine(); console.log(C.r + " Anmeldung abgelaufen – starte nochmal mit: mythos login" + C.x); process.exit(1);
32
327
  }
33
328
 
34
329
  const fmt = (ms) => { const s = Math.floor(ms / 1000); return (s >= 60 ? Math.floor(s / 60) + "m " : "") + (s % 60) + "s"; };
35
330
 
36
- // Live-Timer: läuft während der ganzen Aufgabe (auch während Befehle laufen), pausiert nur für Rückfragen.
331
+ // Live-Timer: läuft während der ganzen Aufgabe, pausiert für Ausgaben und Rückfragen.
37
332
  let T0 = 0, tick = null;
38
333
  const clearLine = () => process.stdout.write("\r\x1b[K");
39
- const startTick = () => { if (tick) return; tick = setInterval(() => process.stdout.write("\r" + C.d + "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + C.x + " "), 250); };
334
+ const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r" + C.d + "⏳ Mythos arbeitet… " + fmt(Date.now() - T0) + " (Strg+C = stoppen)" + C.x + " "), 250); };
40
335
  const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
41
336
  const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
42
337
 
338
+ // ---------- Stoppen mit Strg+C ----------
339
+ let working = false, stopped = false, ctl = null, child = null;
340
+ function killChild() {
341
+ if (!child) return;
342
+ try { if (process.platform === "win32") exec("taskkill /pid " + child.pid + " /T /F"); else child.kill("SIGTERM"); } catch {}
343
+ }
344
+ rl.on("SIGINT", () => {
345
+ if (!working) { console.log(); rl.close(); process.exit(0); }
346
+ if (stopped) return;
347
+ stopped = true; killChild(); if (ctl) ctl.abort();
348
+ say(C.r + "■ Wird gestoppt…" + C.x);
349
+ });
350
+
351
+ // ---------- Markdown im Terminal ----------
352
+ function mdLine(line, st) {
353
+ if (/^\s*```/.test(line)) { st.code = !st.code; return C.d + (st.code ? "┌─ " + line.trim().slice(3) : "└─") + C.x; }
354
+ if (st.code) return C.c + "│ " + line + C.x;
355
+ const h = line.match(/^(#{1,6})\s+(.*)/);
356
+ if (h) return C.b + C.m + h[2] + C.x;
357
+ return line
358
+ .replace(/^(\s*)[-*]\s+/, "$1• ")
359
+ .replace(/\*\*(.+?)\*\*/g, C.b + "$1" + C.x)
360
+ .replace(/`([^`]+)`/g, C.c + "$1" + C.x)
361
+ .replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, "$1 " + C.d + "($2)" + C.x);
362
+ }
363
+
364
+ // Antwort live mitlesen: fertige Zeilen sofort formatiert ausgeben, <tool>-Blöcke ausblenden.
365
+ function streamPrinter() {
366
+ let printed = 0, buf = "", started = false;
367
+ const st = { code: false };
368
+ const line = (l) => {
369
+ stopTick();
370
+ if (!started) { if (!l.trim()) { startTick(); return; } started = true; process.stdout.write(C.b + "Mythos: " + C.x); }
371
+ console.log(mdLine(l, st));
372
+ startTick();
373
+ };
374
+ return {
375
+ push(full) {
376
+ let vis = full;
377
+ const k = vis.indexOf("<tool>");
378
+ if (k >= 0) vis = vis.slice(0, k);
379
+ else for (let n = 5; n > 0; n--) if (vis.endsWith("<tool>".slice(0, n))) { vis = vis.slice(0, -n); break; }
380
+ if (vis.length <= printed) return;
381
+ buf += vis.slice(printed); printed = vis.length;
382
+ let i; while ((i = buf.indexOf("\n")) !== -1) { line(buf.slice(0, i)); buf = buf.slice(i + 1); }
383
+ },
384
+ end() { if (buf.trim()) line(buf); buf = ""; if (started) { stopTick(); console.log(); startTick(); } },
385
+ };
386
+ }
387
+
388
+ // ---------- Modell ----------
43
389
  // Verlauf kürzen, damit lange Aufgaben nicht an zu großem Kontext scheitern. level 0 = mild, 2 = stark.
44
390
  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));
391
+ const withImages = (m, content) => m.images && m.images.length
392
+ ? { 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 } }))) }
393
+ : { role: m.role, content };
45
394
  function compact(history, level) {
46
395
  const keep = [6, 8, 4][level], recentMax = [20000, 3000, 1500][level], oldMax = [1500, 800, 400][level];
47
396
  const cut = Math.max(0, history.length - keep);
48
397
  const older = level === 0 ? history.slice(0, cut) : history.slice(0, Math.min(1, cut));
49
398
  return older.map((m) => ({ role: m.role, content: shorten(m.content, oldMax) }))
50
- .concat(history.slice(cut).map((m) => ({ role: m.role, content: shorten(m.content, recentMax) })));
399
+ .concat(history.slice(cut).map((m) => withImages(m, shorten(m.content, recentMax))));
51
400
  }
52
401
 
53
- async function once(messages) {
54
- const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 170000);
402
+ let tokens = { in: 0, out: 0 };
403
+ async function once(messages, onText) {
404
+ const c = ctl = new AbortController(); const to = setTimeout(() => c.abort(), 170000);
405
+ const system = SYSTEM();
406
+ tokens.in += estimateTokens(system, messages);
55
407
  try {
56
408
  const r = await fetch(API, {
57
- method: "POST", signal: ctl.signal,
409
+ method: "POST", signal: c.signal,
58
410
  headers: { "content-type": "application/json", "x-api-key": cfg.key, "anthropic-version": "2023-06-01" },
59
- body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system: SYSTEM, messages, stream: true }),
411
+ body: JSON.stringify({ model: cfg.model || "mythos-code", max_tokens: 8000, system, messages, stream: true }),
60
412
  });
61
413
  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; }
62
414
  const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = "", out = "";
@@ -68,19 +420,22 @@ async function once(messages) {
68
420
  if (!line.startsWith("data:")) continue;
69
421
  let p; try { p = JSON.parse(line.slice(5)); } catch { continue; }
70
422
  if (p.type === "error") { const e = new Error(p.error?.message || "Überlastet"); e.status = 529; throw e; }
71
- if (p.delta?.text) out += p.delta.text;
423
+ if (p.delta?.text) { out += p.delta.text; if (onText) onText(out); }
72
424
  }
73
425
  }
74
426
  if (!out.trim()) throw new Error("Leere Antwort");
427
+ tokens.out += Math.ceil(out.length / 4);
75
428
  return out;
76
429
  } finally { clearTimeout(to); }
77
430
  }
78
431
 
79
- async function call(history) {
432
+ async function call(history, onText) {
80
433
  for (let a = 0; a < 6; a++) {
81
- try { return await once(compact(history, Math.min(2, Math.floor(a / 2)))); }
434
+ if (stopped) throw new Error("Gestoppt");
435
+ try { return await once(compact(history, Math.min(2, Math.floor(a / 2))), onText); }
82
436
  catch (e) {
83
- if (e.status === 401) throw new Error("API-Key ungültig – bitte neu anmelden: mythos login");
437
+ if (stopped) throw e;
438
+ if (e.status === 401) throw new Error("Anmeldung ungültig – bitte neu anmelden: mythos login");
84
439
  if (/Daily limit/i.test(e.message)) throw new Error("Tageslimit erreicht – mit Pro unbegrenzt.");
85
440
  await new Promise((r) => setTimeout(r, Math.min(8000, 1500 * (a + 1))));
86
441
  }
@@ -88,60 +443,236 @@ async function call(history) {
88
443
  throw new Error("Mythos ist gerade nicht erreichbar – bitte gleich nochmal versuchen.");
89
444
  }
90
445
 
446
+ // ---------- Werkzeuge ----------
447
+ function diffText(d) {
448
+ const shown = d.lines.slice(0, 80).map(([op, l]) =>
449
+ op === "+" ? C.g + " + " + l + C.x : op === "-" ? C.r + " - " + l + C.x : op === "@" ? C.d + " " + l + C.x : C.d + " " + l + C.x);
450
+ if (d.lines.length > 80) shown.push(C.d + " … (" + (d.lines.length - 80) + " weitere Zeilen)" + C.x);
451
+ return shown.join("\n");
452
+ }
453
+
454
+ // Befehl ausführen und die Ausgabe live im Terminal zeigen.
455
+ function runLive(cmd) {
456
+ stopTick();
457
+ return new Promise((res) => {
458
+ let out = "";
459
+ child = spawn(cmd, { shell: true, cwd: ROOT, env: process.env });
460
+ 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); };
461
+ child.stdout.on("data", on(outputDecoder())); child.stderr.on("data", on(outputDecoder()));
462
+ const to = setTimeout(() => { out += "\n[Nach 10 Minuten abgebrochen]"; killChild(); }, 600000);
463
+ child.on("error", (e) => { out += "\nFEHLER: " + e.message; });
464
+ child.on("close", (code) => {
465
+ clearTimeout(to); child = null;
466
+ if (out && !out.endsWith("\n")) process.stdout.write("\n");
467
+ startTick();
468
+ res((out + (code ? "\nExit: " + code : "")).slice(-8000) || "(keine Ausgabe)");
469
+ });
470
+ });
471
+ }
472
+
91
473
  async function runTool(t) {
92
- const show = t.name === "write" ? `write ${t.path} (${(t.content || "").length} Zeichen)` : `${t.name} ${t.cmd || t.path || ""}`;
93
- say(C.y + "⚙ " + show + C.x);
94
- if ((t.name === "run" || t.name === "write") && !autoYes) {
474
+ const label = t.name === "run" ? "run " + t.cmd
475
+ : t.name === "search" ? "search /" + t.pattern + "/" + (t.glob ? " " + t.glob : "")
476
+ : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
477
+ : t.name === "websearch" ? "websearch " + t.query
478
+ : t.name === "fetch" ? "fetch " + t.url
479
+ : t.name + " " + (t.path || "");
480
+ say(C.y + "⚙ " + label + C.x);
481
+ if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes) {
95
482
  stopTick();
96
483
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
97
484
  startTick();
98
485
  if (a === "a") autoYes = true; else if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt.";
99
486
  }
100
- try {
101
- if (t.name === "run") return await new Promise((res) => exec(t.cmd, { encoding: "utf8", timeout: 180000, maxBuffer: 1e7 },
102
- (err, so, se) => res(((so || "") + (se || "") + (err ? "\nExit: " + (err.code ?? err.message) : "")).slice(-8000) || "(keine Ausgabe)")));
103
- if (t.name === "read") return fs.readFileSync(t.path, "utf8").slice(0, 20000);
104
- if (t.name === "write") { fs.mkdirSync(path.dirname(path.resolve(t.path)), { recursive: true }); fs.writeFileSync(t.path, t.content ?? ""); return "OK geschrieben: " + t.path; }
105
- if (t.name === "ls") return fs.readdirSync(t.path || ".", { withFileTypes: true }).map((e) => (e.isDirectory() ? "📁 " : " ") + e.name).join("\n");
106
- return "Unbekanntes Werkzeug";
107
- } catch (e) { return "FEHLER: " + (e.stdout || "") + (e.stderr || "") + e.message; }
108
- }
109
-
110
- async function turn(history, input) {
111
- T0 = Date.now(); startTick();
112
- try { await turnInner(history, input); }
113
- catch (e) { say(C.r + "⚠ " + e.message + C.x); }
114
- finally { stopTick(); console.log(C.g + "✓ Mythos hat " + fmt(Date.now() - T0) + " gearbeitet" + C.x + "\n"); }
115
- }
116
- async function turnInner(history, input) {
117
- history.push({ role: "user", content: input });
118
- for (let i = 0; i < 40; i++) {
119
- const out = await call(history);
487
+ if (t.name === "run") return runLive(t.cmd);
488
+ if (isWebTool(t)) {
489
+ const r = await webTool(t);
490
+ say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
491
+ return r.result;
492
+ }
493
+ const r = fileTool(t, ROOT);
494
+ if (r.diff && r.diff.lines.length) say(diffText(r.diff));
495
+ return r.result;
496
+ }
497
+
498
+ // ---------- Eine Aufgabe (auch /goal) ----------
499
+ async function turnInner(history) {
500
+ const max = goal ? 200 : 40;
501
+ let nudges = 0;
502
+ for (let i = 0; i < max && !stopped; i++) {
503
+ const pr = streamPrinter();
504
+ const out = await call(history, (full) => pr.push(full));
505
+ pr.end();
506
+ if (stopped) break;
120
507
  history.push({ role: "assistant", content: out });
121
508
  const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
122
- const text = out.replace(/<tool>[\s\S]*?<\/tool>/g, "").trim();
123
- if (text) say(C.b + "Mythos: " + C.x + text + "\n");
124
- if (!m) return;
509
+ if (!m) {
510
+ 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"; }
512
+ history.push({ role: "user", content: GOAL_NUDGE });
513
+ continue;
514
+ }
515
+ nudges = 0;
125
516
  let t; try { t = JSON.parse(m[1]); } catch { history.push({ role: "user", content: "Tool-JSON ungültig." }); continue; }
126
517
  const res = await runTool(t);
127
518
  history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + res });
128
519
  }
520
+ if (!stopped) say(C.y + "Schrittlimit (" + max + ") erreicht – mit /weiter macht Mythos weiter." + C.x);
521
+ return stopped ? "stopped" : "limit";
522
+ }
523
+
524
+ async function summarize(history) {
525
+ history.push({ role: "user", content: SUMMARY_PROMPT });
526
+ say(C.m + C.b + "📋 Zusammenfassung" + C.x);
527
+ const pr = streamPrinter();
528
+ const out = await call(history, (full) => pr.push(full));
529
+ pr.end();
530
+ history.push({ role: "assistant", content: out });
531
+ return out;
532
+ }
533
+
534
+ async function turn(history, input, images) {
535
+ if (input != null) history.push({ role: "user", content: input, images });
536
+ T0 = Date.now(); working = true; stopped = false; startTick();
537
+ const tok0 = tokens.in + tokens.out;
538
+ const wasGoal = goal;
539
+ let status = "error", summary = "";
540
+ try {
541
+ status = await turnInner(history);
542
+ if (wasGoal && status !== "stopped" && !stopped) summary = await summarize(history);
543
+ } catch (e) { if (!stopped) say(C.r + "⚠ " + e.message + C.x); }
544
+ if (stopped) { status = "stopped"; if (history[history.length - 1]?.role === "user") history.push({ role: "assistant", content: "(Vom Nutzer gestoppt.)" }); }
545
+ working = false; stopTick();
546
+ const took = fmt(Date.now() - T0);
547
+ console.log((stopped ? C.r + "■ Gestoppt nach " + took : C.g + "✓ Mythos hat " + took + " gearbeitet") + C.x +
548
+ C.d + " · ≈ " + fmtTokens(tokens.in + tokens.out - tok0) + " Tokens (Sitzung ≈ " + fmtTokens(tokens.in + tokens.out) + ")" + C.x + "\n");
549
+ if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
550
+ // Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
551
+ if (cfg.notify && (wasGoal || Date.now() - T0 > 60000)) {
552
+ const title = status === "reached" ? "🎯 Ziel erreicht" : status === "done" ? "✓ Mythos ist fertig" : status === "stopped" ? "■ Gestoppt" : "⚠ Mythos braucht dich";
553
+ pushNotify(cfg.notify, "Mythos Code – " + path.basename(ROOT), title + " (" + took + ")" + (summary ? "\n\n" + summary.slice(0, 1500) : ""));
554
+ }
555
+ }
556
+
557
+ // ---------- Git ----------
558
+ const git = (args) => {
559
+ try { return { ok: true, out: execFileSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1e7 }) }; }
560
+ catch (e) { return { ok: false, out: ((e.stdout || "") + (e.stderr || "")) || e.message }; }
561
+ };
562
+ const gitBranch = () => { const b = git(["rev-parse", "--abbrev-ref", "HEAD"]); return b.ok ? b.out.trim() : ""; };
563
+
564
+ async function commit(msg) {
565
+ const st = git(["status", "--porcelain"]);
566
+ if (!st.ok) return say(C.r + "Kein Git-Repository: " + st.out.trim() + C.x);
567
+ if (!st.out.trim()) return say(C.d + "Nichts zu committen – alles sauber." + C.x);
568
+ if (!msg) {
569
+ const diff = git(["diff", "HEAD"]).out || git(["diff"]).out;
570
+ process.stdout.write(C.d + "Mythos schreibt die Commit-Nachricht…" + C.x);
571
+ stopped = false;
572
+ try {
573
+ 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) }]))
574
+ .replace(/<tool>[\s\S]*?<\/tool>/g, "").replace(/^```\w*\n?|```$/g, "").trim();
575
+ } catch (e) { clearLine(); return say(C.r + "⚠ " + e.message + C.x); }
576
+ clearLine();
577
+ }
578
+ console.log(C.b + "Commit-Nachricht:" + C.x + "\n" + C.c + msg + C.x);
579
+ if (!autoYes && !/^[jy]/i.test((await ask(C.d + "Committen? [j/N] " + C.x)).trim())) return say("Abgebrochen.");
580
+ git(["add", "-A"]);
581
+ const c = git(["commit", "-m", msg]);
582
+ say((c.ok ? C.g : C.r) + c.out.trim() + C.x);
583
+ }
584
+
585
+ // ---------- Bilder: Pfade zu Bilddateien in der Eingabe (z. B. per Drag & Drop ins Terminal) ----------
586
+ const IMG = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
587
+ function extractImages(input) {
588
+ const images = [];
589
+ 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) => {
590
+ const p = path.resolve(ROOT, a || b || c);
591
+ try {
592
+ const st = fs.statSync(p);
593
+ if (!st.isFile() || st.size > 5e6 || images.length >= 5) return all;
594
+ images.push({ media_type: IMG[p.split(".").pop().toLowerCase()], data: fs.readFileSync(p).toString("base64") });
595
+ return "[Bild: " + path.basename(p) + "]";
596
+ } catch { return all; }
597
+ });
598
+ return { text, images };
599
+ }
600
+
601
+ const HELP = [
602
+ ["/goal <ziel>", "Mythos arbeitet selbstständig, bis das Ziel erreicht ist (+ Zusammenfassung)"],
603
+ ["/goal stop", "Ziel beenden"],
604
+ ["/weiter", "Unterbrochene Arbeit fortsetzen"],
605
+ ["/merken <text>", "Etwas ins Projekt-Gedächtnis (MYTHOS.md) schreiben"],
606
+ ["/gedaechtnis", "Projekt-Gedächtnis anzeigen"],
607
+ ["/git", "Git-Status anzeigen"],
608
+ ["/commit [nachricht]", "Alles committen (ohne Nachricht schreibt Mythos sie)"],
609
+ ["/push", "git push"],
610
+ ["/handy <url>", "Handy-Benachrichtigung (ntfy.sh-Thema oder Discord-Webhook) · /handy test · /handy aus"],
611
+ ["/vollzugriff an|aus", "Ohne Nachfragen arbeiten"],
612
+ ["/modell [name]", "Modell anzeigen oder wechseln"],
613
+ ["/tokens", "Geschätzten Token-Verbrauch dieser Sitzung anzeigen"],
614
+ ["/clear", "Verlauf leeren"],
615
+ ["/exit", "Beenden"],
616
+ ];
617
+
618
+ async function command(q, history) {
619
+ const cmd = q.split(/\s+/)[0].toLowerCase(), arg = q.slice(cmd.length).trim();
620
+ if (cmd === "/hilfe" || cmd === "/help") return console.log(HELP.map(([c, d]) => " " + C.c + c.padEnd(22) + C.x + d).join("\n") + "\n");
621
+ if (cmd === "/clear") { history.length = 0; goal = ""; return console.log("Verlauf geleert.\n"); }
622
+ if (cmd === "/goal" || cmd === "/ziel") {
623
+ if (!arg) return console.log(goal ? "🎯 Aktuelles Ziel: " + goal : "So geht's: /goal <was Mythos erreichen soll>");
624
+ if (/^(stop|aus|ende|beenden)$/i.test(arg)) { goal = ""; return console.log("🎯 Ziel beendet.\n"); }
625
+ if (!autoYes) { autoYes = true; console.log(C.d + "Vollzugriff für /goal eingeschaltet – Mythos fragt nicht mehr nach." + C.x); }
626
+ goal = arg;
627
+ return turn(history, "Neues Ziel: " + arg + "\nArbeite jetzt komplett selbstständig daran, bis es erreicht und geprüft ist.");
628
+ }
629
+ if (cmd === "/weiter") {
630
+ if (!history.length) return console.log("Hier gibt es noch nichts zum Weitermachen.");
631
+ return turn(history, history[history.length - 1].role === "assistant" ? "Mach bitte genau dort weiter, wo du aufgehört hast." : null);
632
+ }
633
+ 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"); }
634
+ if (cmd === "/gedaechtnis" || cmd === "/gedächtnis") return console.log(readMemory(ROOT) || "Noch kein Projekt-Gedächtnis. Mit /merken <text> anlegen.", "\n");
635
+ if (cmd === "/git") { const s = git(["status", "--short", "--branch"]); return console.log((s.ok ? s.out : C.r + s.out + C.x).trim() + "\n"); }
636
+ if (cmd === "/commit") return commit(arg);
637
+ if (cmd === "/push") { const p = git(["push"]); return console.log((p.ok ? C.g : C.r) + p.out.trim() + C.x + "\n"); }
638
+ if (cmd === "/handy") {
639
+ if (/^(aus|off)$/i.test(arg)) { delete cfg.notify; save(cfg); return console.log("Handy-Benachrichtigung aus.\n"); }
640
+ 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);
641
+ 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");
642
+ cfg.notify = arg; save(cfg);
643
+ return console.log(C.g + "✓ Gespeichert. Bei /goal und Aufgaben über 1 Minute bekommst du eine Nachricht. Test: /handy test" + C.x + "\n");
644
+ }
645
+ if (cmd === "/modell" || cmd === "/model") {
646
+ const cur = cfg.model || "mythos-code";
647
+ 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");
648
+ const m = MODELS.find((x) => x.id === arg.toLowerCase() || x.label.toLowerCase() === arg.toLowerCase());
649
+ if (!m) return console.log(C.r + "Unbekanntes Modell. Verfügbar: " + MODELS.map((x) => x.id).join(", ") + C.x + "\n");
650
+ cfg.model = m.id; save(cfg);
651
+ return console.log(C.g + "✓ Modell: " + m.label + C.x + "\n");
652
+ }
653
+ if (cmd === "/tokens") return console.log("≈ " + fmtTokens(tokens.in) + " Eingabe + " + fmtTokens(tokens.out) + " Ausgabe = " + fmtTokens(tokens.in + tokens.out) + " Tokens (geschätzt)\n");
654
+ if (cmd === "/vollzugriff") { autoYes = !/^(aus|off)$/i.test(arg); return console.log(autoYes ? "Vollzugriff an.\n" : "Vollzugriff aus – Mythos fragt vor Befehlen nach.\n"); }
655
+ console.log("Unbekannter Befehl: " + cmd + " – /hilfe zeigt alle Befehle.\n");
129
656
  }
130
657
 
131
658
  async function main() {
132
- const args = process.argv.slice(2).filter((a) => a !== "--yes");
659
+ const args = process.argv.slice(2).filter((a) => a !== "--yes" && a !== "--key");
133
660
  if (args[0] === "login" || !cfg.key) await login();
134
661
  if (args[0] === "login") process.exit(0);
135
662
  if (args[0] === "logout") { save({}); console.log("Abgemeldet."); process.exit(0); }
136
- console.log(C.c + C.b + "\n ✦ MYTHOS CODE" + C.x + C.d + " – by Mythoscraft · /exit zum Beenden · /clear leeren\n" + C.x);
663
+ const branch = gitBranch();
664
+ console.log(C.c + C.b + "\n ✦ MYTHOS CODE" + C.x + C.d + " – by Mythoscraft · " + path.basename(ROOT) + (branch ? " ⎇ " + branch : "") +
665
+ (readMemory(ROOT) ? " · Gedächtnis ✓" : "") + "\n /hilfe für Befehle · Strg+C stoppt · Bilder: Datei ins Terminal ziehen\n" + C.x);
137
666
  const history = [];
138
- if (args.length) { await turn(history, args.join(" ")); }
667
+ if (args.length) { const { text, images } = extractImages(args.join(" ")); await turn(history, text, images); }
139
668
  while (true) {
140
- const q = (await ask(C.c + "› " + C.x)).trim();
669
+ const q = (await ask(C.c + (goal ? "🎯 " : "") + "› " + C.x)).trim();
141
670
  if (!q) continue;
142
671
  if (q === "/exit") break;
143
- if (q === "/clear") { history.length = 0; console.log("Verlauf geleert."); continue; }
144
- await turn(history, q);
672
+ if (q.startsWith("/")) { await command(q, history); continue; }
673
+ const { text, images } = extractImages(q);
674
+ if (images.length) console.log(C.d + "🖼 " + images.length + " Bild(er) angehängt" + C.x);
675
+ await turn(history, text, images);
145
676
  }
146
677
  rl.close();
147
678
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",