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.
- package/bin/mythos.js +588 -57
- 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
|
|
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
|
|
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 =
|
|
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.`;
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 + "✓
|
|
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
|
|
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) => (
|
|
399
|
+
.concat(history.slice(cut).map((m) => withImages(m, shorten(m.content, recentMax))));
|
|
51
400
|
}
|
|
52
401
|
|
|
53
|
-
|
|
54
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
144
|
-
|
|
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
|
}
|