mythos-code 1.7.3 → 1.7.8

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 +148 -10
  2. package/package.json +1 -1
package/bin/mythos.js CHANGED
@@ -10,6 +10,107 @@ import { exec, spawn, execFileSync } from "node:child_process";
10
10
  // Wird roh in bin/mythos.js (CLI, ESM) und main.js (App, CommonJS) eingefügt.
11
11
  // Erwartet, dass `fs` und `path` im umgebenden Code schon importiert sind.
12
12
 
13
+ // ---------- Werkzeug-Blöcke robust lesen (CLI, Desktop-App: Haupt- und Fensterprozess) ----------
14
+ // Wird roh über den Platzhalter TOOL_PARSE eingefügt. Keine Abhängigkeiten.
15
+ // Modelle schreiben Aufrufe nicht immer sauber: fehlendes </tool>, <tool_call> statt <tool>, ```json-Zäune,
16
+ // {"name":…,"arguments":{…}} statt flacher Felder, echte Zeilenumbrüche in Strings oder abgeschnittenes JSON.
17
+ // Nichts davon darf als Rohtext im Chat landen.
18
+
19
+ const TOOL_NAMES = ["run", "read", "edit", "write", "search", "ls", "websearch", "fetch", "cloud", "todo"];
20
+ const TOOL_OPEN_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>/i;
21
+ // Ein ganzer Block – auch nicht geschlossen (dann bis zum Textende).
22
+ const TOOL_BLOCK_RE = /<(tool_call|tool_use|tool)(?:\s[^>]*)?>[\s\S]*?(?:<\/\1\s*>|$)/gi;
23
+
24
+ /** Alle Werkzeug-Blöcke und verwaiste End-Tags entfernen. */
25
+ function stripToolBlocks(text) {
26
+ return String(text || "").replace(TOOL_BLOCK_RE, "").replace(/<\/(?:tool_call|tool_use|tool)\s*>/gi, "");
27
+ }
28
+
29
+ /** Erstes JSON-Objekt im Text herausschneiden. Rohe Zeilenumbrüche in Strings werden escaped,
30
+ * abgeschnittenes JSON wird geschlossen. → { json, complete } oder null. */
31
+ function jsonObjectAt(s) {
32
+ const start = s.indexOf("{");
33
+ if (start < 0) return null;
34
+ let out = "", inStr = false, esc = false;
35
+ const stack = [];
36
+ for (let i = start; i < s.length; i++) {
37
+ const ch = s[i];
38
+ if (inStr) {
39
+ if (esc) { esc = false; out += ch; continue; }
40
+ if (ch === "\\") { esc = true; out += ch; continue; }
41
+ if (ch === '"') { inStr = false; out += ch; continue; }
42
+ out += ch === "\n" ? "\\n" : ch === "\r" ? "\\r" : ch === "\t" ? "\\t" : ch;
43
+ continue;
44
+ }
45
+ if (ch === '"') { inStr = true; out += ch; continue; }
46
+ if (ch === "{" || ch === "[") stack.push(ch === "{" ? "}" : "]");
47
+ else if (ch === "}" || ch === "]") { stack.pop(); out += ch; if (!stack.length) return { json: out, complete: true }; continue; }
48
+ out += ch;
49
+ }
50
+ // Abgeschnitten: offenen String und Klammern schließen, hängendes Komma/Doppelpunkt weg.
51
+ if (esc) out = out.slice(0, -1);
52
+ if (inStr) out += '"';
53
+ out = out.replace(/[,:]\s*$/, "");
54
+ while (stack.length) out += stack.pop();
55
+ return { json: out, complete: false };
56
+ }
57
+
58
+ /** Ersten Werkzeug-Aufruf aus einer Antwort lesen.
59
+ * → { tool: {name, …} | null, error: string | "", text: sichtbarer Text ohne Werkzeug-Blöcke } */
60
+ function parseToolBlock(out) {
61
+ out = String(out || "");
62
+ const text = stripToolBlocks(out).trim();
63
+ const m = TOOL_OPEN_RE.exec(out);
64
+ if (!m) return { tool: null, error: "", text };
65
+ let body = out.slice(m.index + m[0].length);
66
+ const end = body.search(/<\/(?:tool_call|tool_use|tool)\s*>/i);
67
+ if (end >= 0) body = body.slice(0, end);
68
+ body = body.replace(/```[a-z]*\s*/gi, "");
69
+ const raw = jsonObjectAt(body);
70
+ if (!raw) return { tool: null, error: "Im Werkzeug-Block steht kein JSON-Objekt.", text };
71
+ let t;
72
+ try { t = JSON.parse(raw.json); } catch (e) { return { tool: null, error: "Werkzeug-JSON ungültig (" + e.message + ").", text }; }
73
+ if (!t || typeof t !== "object" || Array.isArray(t)) return { tool: null, error: "Werkzeug-JSON muss ein Objekt sein.", text };
74
+ // Name auch aus <tool name="…"> oder anderen üblichen Feldern.
75
+ const attr = m[0].match(/\bname\s*=\s*["']([^"']+)["']/i);
76
+ if (typeof t.name !== "string") t.name = String(t.tool_name || t.function || (attr && attr[1]) || "");
77
+ // {"name":"x","arguments":{…}} / "input" / "parameters" / "args" flach machen (bei MCP: nach "args").
78
+ for (const k of ["arguments", "input", "parameters", "args"]) {
79
+ const a = t[k];
80
+ if (!a || typeof a !== "object" || Array.isArray(a)) continue;
81
+ if (t.name === "mcp") { if (k !== "args") { delete t[k]; if (!t.args) t.args = a; } continue; }
82
+ delete t[k]; for (const x of Object.keys(a)) if (!(x in t)) t[x] = a[x];
83
+ }
84
+ // mcp__server__tool (Hook-Schreibweise) → MCP-Aufruf.
85
+ const mm = t.name.match(/^mcp__(.+?)__(.+)$/);
86
+ if (mm) { const { name, ...rest } = t; t = { name: "mcp", server: mm[1], tool: mm[2], args: rest.args || rest }; }
87
+ t.name = t.name.trim();
88
+ if (!t.name) return { tool: null, error: "Im Werkzeug-Block fehlt \"name\".", text };
89
+ // Abgeschnittene Aufrufe nicht ausführen (sonst z. B. halbe Dateien) – außer harmlose wie todo.
90
+ if (!raw.complete && t.name !== "todo") return { tool: null, error: "Der Werkzeug-Aufruf " + t.name + " ist abgeschnitten.", text };
91
+ return { tool: t, error: "", text };
92
+ }
93
+
94
+ /** Prüfen, ob es das Werkzeug wirklich gibt. extra = { mcp: [{ server, tools: [Namen] }], browser: bool }.
95
+ * → "" wenn ok, sonst eine Erklärung fürs Modell (mit der Liste, was es gibt). */
96
+ function toolProblem(t, extra) {
97
+ extra = extra || {};
98
+ const mcp = extra.mcp || [];
99
+ const avail = TOOL_NAMES.concat(mcp.length ? ["mcp"] : [], extra.browser ? ["browser"] : []);
100
+ const list = "Verfügbar sind nur: " + avail.join(", ") + (mcp.length ? " · MCP: " + mcp.map((s) => s.server + " (" + s.tools.join(", ") + ")").join("; ") : " · keine MCP-Connectoren verbunden") + ".";
101
+ const tail = " Erfinde keine Werkzeuge. Wenn etwas damit nicht geht, sag das dem Nutzer kurz und mach mit dem Rest weiter.";
102
+ if (t.name === "mcp") {
103
+ const s = mcp.find((x) => x.server === t.server) || mcp.find((x) => String(x.server).toLowerCase() === String(t.server || "").toLowerCase());
104
+ if (!s) return "FEHLER: MCP-Server „" + (t.server || "?") + "“ ist nicht verbunden. " + list + tail;
105
+ t.server = s.server;
106
+ if (!s.tools.includes(t.tool)) return "FEHLER: MCP-Werkzeug „" + t.tool + "“ gibt es bei " + s.server + " nicht. " + list + tail;
107
+ return "";
108
+ }
109
+ if (t.name === "browser") return extra.browser ? "" : "FEHLER: Das Browser-Plugin ist nicht verbunden. " + list + tail;
110
+ return TOOL_NAMES.includes(t.name) ? "" : "FEHLER: Werkzeug „" + t.name + "“ gibt es nicht. " + list + tail;
111
+ }
112
+
113
+
13
114
  const TOOL_PROMPT = [
14
115
  "Werkzeuge – antworte pro Schritt mit GENAU EINEM Block:",
15
116
  '<tool>{"name":"run","cmd":"..."}</tool> Shell-Befehl ausführen (Ausgabe läuft live mit)',
@@ -20,7 +121,9 @@ const TOOL_PROMPT = [
20
121
  '<tool>{"name":"ls","path":"."}</tool> Ordner auflisten',
21
122
  '<tool>{"name":"websearch","query":"..."}</tool> Im Internet suchen (z. B. Doku, Fehlermeldungen)',
22
123
  '<tool>{"name":"fetch","url":"https://..."}</tool> Webseite als Text lesen',
124
+ '<tool>{"name":"cloud","app":"Name der App"}</tool> Mythos Cloud verbinden: eigenes Backend (Datenbank) + KI-Gateway (Chat, Streaming, Bilder) für die App, die du baust. Nutze das IMMER, sobald die App Daten speichern oder KI nutzen soll – statt Supabase/Firebase/OpenAI-Keys o. Ä. Legt mythos-cloud.js im Projekt an und erklärt die Nutzung.',
23
125
  '<tool>{"name":"todo","items":[{"text":"Schritt","done":false}]}</tool> Aufgabenliste anlegen/aktualisieren (bei größeren Aufgaben zuerst planen, dann Punkte abhaken; immer die komplette Liste senden)',
126
+ "Es gibt NUR diese Werkzeuge (plus die unten genannten MCP-/Browser-Werkzeuge, falls welche aufgeführt sind). Erfinde keine Werkzeuge, Connectoren oder MCP-Server. Fehlt etwas (z. B. ein nicht verbundener Connector), sag das dem Nutzer kurz und mach mit dem Rest weiter. Schließe jeden Block mit </tool>.",
24
127
  "Nach jedem Werkzeug bekommst du das Ergebnis. Suche erst, statt Dateien blind zu lesen. Ändere bestehende Dateien mit edit statt write.",
25
128
  "Arbeite Schritt für Schritt, bis die Aufgabe erledigt ist, dann antworte normal ohne <tool> (Markdown erlaubt).",
26
129
  ].join("\n");
@@ -426,7 +529,7 @@ let goal = "";
426
529
  const SYSTEM = () => "Du bist Mythos Code, ein autonomer Coding-Agent im Terminal des Nutzers (OS: " + process.platform + ", Projektordner: " + ROOT + ").\n" +
427
530
  TOOL_PROMPT + memoryPrompt(readMemory(ROOT)) + goalPrompt(goal);
428
531
 
429
- const SITE = process.env.MYTHOS_SITE_URL || "https://ai-mythos.lovable.app";
532
+ const SITE = process.env.MYTHOS_SITE_URL || "https://mythos-core-ai.lovable.app";
430
533
  const FN = process.env.MYTHOS_FN_URL || API.replace(/\/v1-messages$/, "");
431
534
  const authPost = async (body) => (await fetch(FN + "/cli-auth", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json();
432
535
 
@@ -476,6 +579,23 @@ const tickLine = () => {
476
579
  };
477
580
  const startTick = () => { if (tick || !working) return; tick = setInterval(() => process.stdout.write("\r\x1b[K" + C.d + tickLine() + C.x), 250); };
478
581
  const stopTick = () => { if (tick) { clearInterval(tick); tick = null; } clearLine(); };
582
+ let adN = 0, adTier = null, adAdminDay = "";
583
+ async function maybeAd() {
584
+ try {
585
+ if (adTier == null) { const j = await (await fetch(API.replace(/\/v1-messages.*$/, "") + "/cli-auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "usage", api_key: cfg.key }) })).json(); adTier = j.admin ? "admin" : (j.tier || "free"); }
586
+ let due = false; const today = new Date().toDateString();
587
+ if (adTier === "admin") { due = adAdminDay !== today; adAdminDay = today; }
588
+ else { const every = adTier === "free" ? 4 : adTier === "light" ? 10 : 0; due = every > 0 && (++adN % every === 0); }
589
+ if (due) {
590
+ const fnBase = API.replace(/\/v1-messages.*$/, "");
591
+ let ads = []; try { ads = (await (await fetch(fnBase + "/ad-review", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "list" }) })).json()).ads || []; } catch {}
592
+ const pool = [{ house: true, weight: 3 }].concat(ads); let r = Math.random() * pool.reduce((t, a) => t + Math.max(1, a.weight), 0); let ad = pool[0];
593
+ for (const a of pool) { r -= Math.max(1, a.weight); if (r <= 0) { ad = a; break; } }
594
+ if (!ad.house) { console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m" + ad.product + "\x1b[0m – " + ad.ad_text + ": " + ad.link + "\n\x1b[2m Buche deine eigene Werbung: https://mythos-core-ai.lovable.app/werbung\x1b[0m\n"); return; }
595
+ }
596
+ if (due) console.log("\x1b[2m📣 Werbung\x1b[0m \x1b[1m@at\x1b[0m – dein Feed voller Vibes: https://at-feed-vibes.lovable.app/\n\x1b[2m Buche deine eigene Werbung: https://mythos-core-ai.lovable.app/werbung\x1b[0m\n");
597
+ } catch {}
598
+ }
479
599
  const say = (s) => { const was = !!tick; stopTick(); console.log(s); if (was) startTick(); };
480
600
 
481
601
  // ---------- Stoppen mit Strg+C ----------
@@ -523,9 +643,9 @@ function streamPrinter() {
523
643
  return {
524
644
  push(full) {
525
645
  let vis = full;
526
- const k = vis.indexOf("<tool>");
646
+ const k = vis.search(/<tool(?:_call|_use)?[\s>]/i);
527
647
  if (k >= 0) vis = vis.slice(0, k);
528
- else for (let n = 5; n > 0; n--) if (vis.endsWith("<tool>".slice(0, n))) { vis = vis.slice(0, -n); break; }
648
+ else { const p = vis.match(/<(?:t(?:o(?:o(?:l(?:_(?:c(?:a(?:l(?:l)?)?)?|u(?:s(?:e)?)?)?)?)?)?)?)?$/i); if (p) vis = vis.slice(0, p.index); }
529
649
  if (vis.length <= printed) return;
530
650
  buf += vis.slice(printed); printed = vis.length;
531
651
  let i; while ((i = buf.indexOf("\n")) !== -1) { line(buf.slice(0, i)); buf = buf.slice(i + 1); }
@@ -637,6 +757,19 @@ function runLive(cmd) {
637
757
  });
638
758
  }
639
759
 
760
+ // Mythos Cloud: Backend + KI-Gateway für die gebaute App verbinden und mythos-cloud.js anlegen.
761
+ async function cloudConnect(t) {
762
+ try {
763
+ const r = await fetch(FN + "/app-cloud", { method: "POST", headers: { "content-type": "application/json" },
764
+ body: JSON.stringify({ action: "connect", api_key: cfg.key, name: String(t.app || path.basename(ROOT)) }) });
765
+ const j = await r.json().catch(() => ({}));
766
+ if (!r.ok || !j.client_js) return "FEHLER: Mythos Cloud: " + (j.error || "HTTP " + r.status);
767
+ const res = fileTool({ name: "write", path: (t.path = t.path || j.file), content: j.client_js }, ROOT);
768
+ say(C.g + " ☁ Mythos Cloud verbunden (" + j.app + ")" + C.x);
769
+ return res.result + "\n\n" + j.guide;
770
+ } catch (e) { return "FEHLER: Mythos Cloud nicht erreichbar: " + e.message; }
771
+ }
772
+
640
773
  async function runTool(t) {
641
774
  if (t.name === "todo") {
642
775
  const items = normalizeTodos(t);
@@ -650,6 +783,7 @@ async function runTool(t) {
650
783
  : t.name === "write" ? "write " + t.path + " (" + String(t.content || "").length + " Zeichen)"
651
784
  : t.name === "websearch" ? "websearch " + t.query
652
785
  : t.name === "fetch" ? "fetch " + t.url
786
+ : t.name === "cloud" ? "cloud verbinden: " + (t.app || "App")
653
787
  : t.name + " " + (t.path || "");
654
788
  say(C.y + "⚙ " + label + C.x);
655
789
  // Sicherheitsnetz: gefährliche Befehle fragen immer nach (auch mit --yes) und werden bei /goal blockiert.
@@ -662,13 +796,14 @@ async function runTool(t) {
662
796
  startTick();
663
797
  if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt (Sicherheitsnetz). Finde einen sichereren Weg.";
664
798
  }
665
- if ((t.name === "run" || t.name === "write" || t.name === "edit") && !autoYes && !why) {
799
+ if ((t.name === "run" || t.name === "write" || t.name === "edit" || t.name === "cloud") && !autoYes && !why) {
666
800
  stopTick();
667
801
  const a = (await ask(C.d + " Erlauben? [j/N/a=immer] " + C.x)).trim().toLowerCase();
668
802
  startTick();
669
803
  if (a === "a") autoYes = true; else if (a !== "j" && a !== "y") return "Vom Nutzer abgelehnt.";
670
804
  }
671
805
  if (t.name === "run") return runLive(t.cmd);
806
+ if (t.name === "cloud") return cloudConnect(t);
672
807
  if (isWebTool(t)) {
673
808
  const r = await webTool(t);
674
809
  say(C.d + " " + r.result.split("\n").filter(Boolean).slice(0, 3).join(" · ").slice(0, 160) + C.x);
@@ -694,15 +829,17 @@ async function turnInner(history) {
694
829
  pr.end(); activity = "";
695
830
  if (stopped) break;
696
831
  history.push({ role: "assistant", content: out });
697
- const m = out.match(/<tool>([\s\S]*?)<\/tool>/);
698
- if (!m) {
832
+ const pb = parseToolBlock(out);
833
+ if (!pb.tool && !pb.error) {
699
834
  if (!goal || /ZIEL ERREICHT/.test(out)) return goal ? "reached" : "done";
700
835
  if (++nudges > 40) { say(C.y + "🎯 Mythos kommt beim Ziel nicht weiter – schau es dir bitte an (/weiter macht weiter)." + C.x); return "stuck"; }
701
836
  history.push({ role: "user", content: GOAL_NUDGE });
702
837
  continue;
703
838
  }
704
839
  nudges = 0;
705
- let t; try { t = JSON.parse(m[1]); } catch { history.push({ role: "user", content: "Tool-JSON ungültig." }); continue; }
840
+ if (pb.error) { history.push({ role: "user", content: pb.error + " Sende den Block vollständig als gültiges JSON in <tool>…</tool>." }); continue; }
841
+ const t = pb.tool, bad = toolProblem(t);
842
+ if (bad) { history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + bad }); continue; }
706
843
  const res = await runTool(t);
707
844
  history.push({ role: "user", content: "Werkzeug-Ergebnis:\n" + res });
708
845
  }
@@ -739,6 +876,7 @@ async function turn(history, input, images) {
739
876
  const took = fmt(Date.now() - T0);
740
877
  console.log((stopped ? C.r + "■ Gestoppt nach " + took : C.g + "✓ Mythos hat " + took + " gearbeitet") + C.x +
741
878
  C.d + " · ≈ " + fmtTokens(tokens.in + tokens.out - tok0) + " Tokens (Sitzung ≈ " + fmtTokens(tokens.in + tokens.out) + ")" + C.x + "\n");
879
+ if (!stopped && status !== "error") await maybeAd();
742
880
  if (status === "reached") { console.log(C.g + "🎯 Ziel erreicht: " + wasGoal + C.x + "\n"); goal = ""; }
743
881
  // Handy-Benachrichtigung bei /goal oder längeren Aufgaben.
744
882
  if (cfg.notify && (wasGoal || Date.now() - T0 > 60000)) {
@@ -764,7 +902,7 @@ async function commit(msg) {
764
902
  stopped = false;
765
903
  try {
766
904
  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) }]))
767
- .replace(/<tool>[\s\S]*?<\/tool>/g, "").replace(/^```\w*\n?|```$/g, "").trim();
905
+ .replace(TOOL_BLOCK_RE, "").replace(/^```\w*\n?|```$/g, "").trim();
768
906
  } catch (e) { clearLine(); return say(C.r + "⚠ " + e.message + C.x); }
769
907
  clearLine();
770
908
  }
@@ -906,7 +1044,7 @@ async function command(q, history) {
906
1044
  let sum = "";
907
1045
  try {
908
1046
  sum = (await call([{ role: "user", content: "Fasse den bisherigen Verlauf dieses Coding-Chats so zusammen, dass ein Agent nahtlos weiterarbeiten kann: **Ziel**, **Stand**, **Wichtige Dateien & Entscheidungen**, **Offene Punkte**. Maximal 25 Zeilen, ohne Werkzeuge.\n\nVERLAUF:\n" + transcript }]))
909
- .replace(/<tool>[\s\S]*?<\/tool>/g, "").trim();
1047
+ .replace(TOOL_BLOCK_RE, "").trim();
910
1048
  } catch (e) { clearLine(); return console.log(C.r + "⚠ " + e.message + C.x + "\n"); }
911
1049
  clearLine();
912
1050
  history.length = 0;
@@ -916,7 +1054,7 @@ async function command(q, history) {
916
1054
  if (cmd === "/stats") { const b = gitBranch(); return console.log(mdLines(projectStats(ROOT) + (b ? "\n**Git:** Branch `" + b + "`" : "")) + "\n"); }
917
1055
  if (cmd === "/export") {
918
1056
  const file = path.resolve(ROOT, arg || "mythos-chat-" + new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-") + ".md");
919
- const msgs = history.map((m) => ({ role: /^Werkzeug-Ergebnis/.test(m.content) ? "tool" : m.role, text: String(m.content).replace(/<tool>[\s\S]*?<\/tool>/g, "").slice(0, 20000) })).filter((m) => m.text.trim());
1057
+ const msgs = history.map((m) => ({ role: /^Werkzeug-Ergebnis/.test(m.content) ? "tool" : m.role, text: String(m.content).replace(TOOL_BLOCK_RE, "").slice(0, 20000) })).filter((m) => m.text.trim());
920
1058
  fs.writeFileSync(file, chatToMarkdown("Mythos-Chat – " + path.basename(ROOT), msgs));
921
1059
  return console.log(C.g + "✓ Gespeichert: " + file + C.x + "\n");
922
1060
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mythos-code",
3
- "version": "1.7.3",
3
+ "version": "1.7.8",
4
4
  "description": "Mythos Code – KI-Coding-Agent im Terminal (by Mythoscraft)",
5
5
  "bin": {
6
6
  "mythos": "bin/mythos.js",