hmgr 0.0.5 → 0.0.6

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.
@@ -12,16 +12,34 @@
12
12
  return el;
13
13
  }
14
14
 
15
- // Home-Verzeichnis, einmal beim Laden über hmgr:home geholt (siehe ladeHome unten). Nur für die
16
- // Anzeige: fließt nie in einen IPC-Aufruf oder einen zielKey zurück.
15
+ // Home-Verzeichnis und Plattform, einmal beim Laden über hmgr:home und hmgr:plattform geholt
16
+ // (siehe ladeUmgebung unten). Nur für die Anzeige: beides fließt nie in einen IPC-Aufruf oder
17
+ // einen zielKey zurück.
17
18
  var HOME = '';
18
- /** Kürzt einen absoluten Pfad auf ~, wenn er mit dem Home-Verzeichnis beginnt (Grenze am „/"). */
19
+ // Pfadtrenner der Anzeige (E68). Aus der Plattform und nicht aus dem Home-Pfad abgeleitet: die
20
+ // Pfade, die hier angezeigt werden, baut der Kern mit `node:path`, also mit dem Trenner der
21
+ // Plattform — auch dann, wenn HOME selbst in POSIX-Schreibweise gesetzt ist (MSYS/Git-Bash).
22
+ var TRENNER = '/';
23
+ /** Trenner einer Plattform, wie `process.platform` sie nennt. Rein, nur für die Anzeige. */
24
+ function plattformTrenner(plattform) { return plattform === 'win32' ? '\\' : '/'; }
25
+ /**
26
+ * Kürzt einen absoluten Pfad auf ~, wenn er mit dem Home-Verzeichnis beginnt. Grenze ist einer
27
+ * der beiden Trenner (E68): unter Windows steht dort „\", und der Rest des Pfads bleibt wörtlich
28
+ * stehen, die Kürzung heißt dort also von selbst `~\…` statt `~/…`.
29
+ */
19
30
  function pfadKurz(p) {
20
31
  if (!HOME || typeof p !== 'string') return p;
21
32
  if (p === HOME) return '~';
22
- if (p.indexOf(HOME + '/') === 0) return '~' + p.slice(HOME.length);
33
+ var grenze = p.charAt(HOME.length);
34
+ if (p.indexOf(HOME) === 0 && (grenze === '/' || grenze === '\\')) return '~' + p.slice(HOME.length);
23
35
  return p;
24
36
  }
37
+ /**
38
+ * Schreibt einen fest im Quelltext stehenden POSIX-Pfad in der Schreibweise der Plattform (E68),
39
+ * damit `~/.claude.json` unter Windows nicht neben `~\Code\…` steht. Nur für Anzeigetexte, die
40
+ * hmgr selbst formuliert — nie für einen Pfad, der aus dem Kern kommt.
41
+ */
42
+ function pfadSchreibweise(p) { return TRENNER === '/' ? p : p.split('/').join(TRENNER); }
25
43
 
26
44
  function melde(text) {
27
45
  $('meldungtext').textContent = text;
@@ -163,10 +181,13 @@
163
181
  * dasselbe (`quelleAlsText`), aber nur so findet auch die Einstellungen-Liste ihr Icon.
164
182
  */
165
183
  function quelleAdresse(quelle) { return quelle.text != null ? quelle.text : quelleKurz(quelle); }
166
- /** Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche. */
184
+ /**
185
+ * Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche.
186
+ * Trenner sind `/` und `\`, wie in `letztesSegment` im Kern (E65/B8, dort führend).
187
+ */
167
188
  function letztesSegment(text) {
168
- var bereinigt = text.replace(/\/+$/, '');
169
- var segmente = bereinigt.split('/').filter(function (s) { return s.length > 0; });
189
+ var bereinigt = text.replace(/[/\\]+$/, '');
190
+ var segmente = bereinigt.split(/[/\\]/).filter(function (s) { return s.length > 0; });
170
191
  return segmente.length > 0 ? segmente[segmente.length - 1] : bereinigt;
171
192
  }
172
193
  /**
@@ -256,7 +277,7 @@
256
277
  /**
257
278
  * Eine Quelle, angeschrieben mit ihrem Namen und anklickbar (E60): keine Unterstreichung, das
258
279
  * kleine Pfeilsymbol dahinter trägt das Signalorange. Klick öffnet über den Main-Prozess die
259
- * Repo-Seite im Browser oder den Ordner im Finder (`window.hmgr.quelleOeffnen`), still im
280
+ * Repo-Seite im Browser oder den Ordner im Dateimanager (`window.hmgr.quelleOeffnen`), still im
260
281
  * Fehlerfall statt in der Meldungsleiste, ein Öffnen-Fehlschlag soll die Ansicht nicht stören.
261
282
  */
262
283
  function quelleLinkElement(quelle) {
@@ -265,7 +286,9 @@
265
286
  if (lokal) el.type = 'button'; else el.href = '#';
266
287
  el.className = 'qlink';
267
288
  el.title = quelleAdresse(quelle);
268
- el.setAttribute('aria-label', quelleAnzeigename(quelle) + ' ' + (lokal ? 'im Finder öffnen' : 'im Browser öffnen'));
289
+ // „Dateimanager" statt „Finder" (E68): die GUI läuft auch unter Windows, wo das Ding Explorer
290
+ // heißt, und unter Linux gibt es weder das eine noch das andere.
291
+ el.setAttribute('aria-label', quelleAnzeigename(quelle) + ' ' + (lokal ? 'im Dateimanager öffnen' : 'im Browser öffnen'));
269
292
  el.appendChild(document.createTextNode(quelleAnzeigename(quelle)));
270
293
  el.appendChild(externSvg());
271
294
  el.addEventListener('click', function (ev) {
@@ -284,6 +307,19 @@
284
307
  var HARNESSE_ALLE = [];
285
308
  var AKTIVE_HARNESSE = [];
286
309
  var HARNESSE_NICHT_INSTALLIERT = [];
310
+ // MCP-Katalog (E61-Folgerunde): je Eintrag { eintrag, eingehaengt, fehlendeVariablen }, dazu
311
+ // Einträge in Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt). Seit F46 nur
312
+ // noch Grundlage für den MCP-Abschnitt der Ziel-Karten (mcpZeilenFuerZiel), die MCP-Ansicht selbst
313
+ // zeigt keine Fundorte mehr.
314
+ var MCP_KATALOG = [];
315
+ var MCP_FREMD = [];
316
+ // Nicht-Katalog-Einträge aus geteilten, nur lesbaren Fundorten (E61-Nachtrag), etwa der
317
+ // .mcp.json einer Projektwurzel.
318
+ var MCP_FREMD_GETEILT = [];
319
+ // Alle Orte, an denen grundsätzlich ein MCP-Server hängen könnte (E61-GUI-Nachtrag), roh aus dem
320
+ // Kern (F42, E16): Grundlage für den MCP-Abschnitt und den „MCP hinzufügen …"-Knopf an den
321
+ // Ziel-Karten — nie am Harness-Namen entschieden, sondern an dieser Liste.
322
+ var MCP_PLAETZE = [];
287
323
  var quellenFilter = 'alle';
288
324
  var zielFilter = 'alle';
289
325
  // Standard-Ref je Quelle (E60), aus dem Katalog abgeleitet (SKILLS trägt ihn schon je Skill): ein
@@ -340,9 +376,18 @@
340
376
 
341
377
  /* ---------------- Laden ---------------- */
342
378
 
343
- /** Home-Verzeichnis einmal beim Start holen (nur für pfadKurz, siehe oben). */
344
- async function ladeHome() {
345
- HOME = await rufe(function () { return window.hmgr.home(); });
379
+ /**
380
+ * Home-Verzeichnis und Plattform einmal beim Start holen (nur für die Anzeige, siehe oben).
381
+ * Danach stehen die fest formulierten Pfade im Bild in der Schreibweise der Plattform (E68).
382
+ */
383
+ async function ladeUmgebung() {
384
+ var werte = await Promise.all([
385
+ rufe(function () { return window.hmgr.home(); }),
386
+ rufe(function () { return window.hmgr.plattform(); }),
387
+ ]);
388
+ HOME = werte[0];
389
+ TRENNER = plattformTrenner(werte[1]);
390
+ $('mcpkatalogpfad').textContent = pfadSchreibweise('~/.hmgr/mcp.yaml');
346
391
  }
347
392
 
348
393
  async function ladeAlles() {
@@ -357,6 +402,8 @@
357
402
  rufe(function () { return window.hmgr.harnessTypen(); }),
358
403
  rufe(function () { return window.hmgr.aktiveHarnesses(); }),
359
404
  rufe(function () { return window.hmgr.nichtVorhandeneHarnesses(); }),
405
+ rufe(function () { return window.hmgr.mcpUebersicht(); }),
406
+ rufe(function () { return window.hmgr.mcpPlaetze(); }),
360
407
  ]);
361
408
  ZIELE = ergebnisse[0];
362
409
  SKILLS = ergebnisse[1];
@@ -367,6 +414,10 @@
367
414
  HARNESSE_ALLE = ergebnisse[4];
368
415
  AKTIVE_HARNESSE = ergebnisse[5];
369
416
  HARNESSE_NICHT_INSTALLIERT = ergebnisse[6];
417
+ MCP_KATALOG = ergebnisse[7].katalog;
418
+ MCP_FREMD = ergebnisse[7].unbekannt;
419
+ MCP_FREMD_GETEILT = ergebnisse[7].unbekannteGeteilteFundorte;
420
+ MCP_PLAETZE = ergebnisse[8];
370
421
  STANDARD_REF = {};
371
422
  SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
372
423
 
@@ -375,6 +426,7 @@
375
426
  baueQuellenfilter();
376
427
  baueMarketplaces();
377
428
  baueEinstellungen();
429
+ baueMcp();
378
430
  aktualisiereKopf();
379
431
  }
380
432
 
@@ -425,7 +477,7 @@
425
477
 
426
478
  /* ---------------- Navigation ---------------- */
427
479
 
428
- var ANSICHTEN = ['ziele', 'katalog', 'update', 'plugins'];
480
+ var ANSICHTEN = ['ziele', 'katalog', 'update', 'plugins', 'mcp'];
429
481
  function zeigeAnsicht(name) {
430
482
  document.querySelectorAll('.plakette').forEach(function (a) {
431
483
  if (a.dataset.ansicht === name) a.setAttribute('aria-current', 'page');
@@ -628,11 +680,12 @@
628
680
  }
629
681
  }
630
682
 
683
+ var fussContainer;
631
684
  if (eigene.length === 0) {
632
685
  var leer = e('div', 'leerzeile');
633
686
  leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
634
- leer.appendChild(skillsKnopf(z));
635
687
  teil.appendChild(leer);
688
+ fussContainer = leer;
636
689
  } else {
637
690
  // Fünf Spalten (E60): Icon, Skill, Beschreibung (die breiteste, sie beantwortet die Frage,
638
691
  // wegen der die Tabelle gelesen wird), Quelle, Aktion.
@@ -666,14 +719,7 @@
666
719
 
667
720
  // Die Beschreibung aus der SKILL.md (E39): die Frage, wegen der die Zeile gelesen wird,
668
721
  // deshalb die breiteste Spalte. Ein Link ohne bekannte Quelle kennt seinen Skill nicht.
669
- var beschreibung = ei.art === 'link' ? ei.link.skill.beschreibung : undefined;
670
- if (beschreibung) {
671
- var wasEl = e('span', 'was', beschreibung);
672
- if (beschreibung.length > 88) wasEl.title = beschreibung;
673
- li.appendChild(wasEl);
674
- } else {
675
- li.appendChild(e('span', 'was fehlt', 'keine Beschreibung in der SKILL.md'));
676
- }
722
+ li.appendChild(wasZelle(ei.art === 'link' ? ei.link.skill : null));
677
723
 
678
724
  // Quelle: der Name, anklickbar, dahinter nur, was vom Normalfall abweicht — ein Ref, das
679
725
  // vom Standard-Ref der Quelle abweicht, und der Grund „über Zeiger" (E57). „Manuell" (der
@@ -709,11 +755,27 @@
709
755
  });
710
756
  teil.appendChild(ul);
711
757
 
758
+ // MCP-Abschnitt (E61-GUI-Nachtrag): unter der Skill-Link-Tabelle, vor dem Kartenfuß mit den
759
+ // Knöpfen, Gegenrichtung zur MCP-Ansicht.
760
+ var mcpAbschnMitTabelle = mcpAbschnitt(z.ziel);
761
+ if (mcpAbschnMitTabelle) teil.appendChild(mcpAbschnMitTabelle);
762
+
712
763
  var fuss = e('div', 'zielfuss');
713
- fuss.appendChild(skillsKnopf(z));
714
764
  teil.appendChild(fuss);
765
+ fussContainer = fuss;
766
+ }
767
+
768
+ // Ohne eigene Links (leer-Fall) steht der MCP-Abschnitt hinter der Leerzeile, die dort schon den
769
+ // Kartenfuß bildet (Nachricht + Knöpfe in einer Zeile, wie überall sonst im leeren Zustand).
770
+ if (eigene.length === 0) {
771
+ var mcpAbschnOhneTabelle = mcpAbschnitt(z.ziel);
772
+ if (mcpAbschnOhneTabelle) teil.appendChild(mcpAbschnOhneTabelle);
715
773
  }
716
774
 
775
+ fussContainer.appendChild(skillsKnopf(z));
776
+ var mcpKnopf = mcpHinzufuegenKnopf(z.ziel);
777
+ if (mcpKnopf) fussContainer.appendChild(mcpKnopf);
778
+
717
779
  return teil;
718
780
  }
719
781
 
@@ -739,6 +801,99 @@
739
801
  return knopf;
740
802
  }
741
803
 
804
+ /* ---------------- MCP an der Ziel-Karte (E61-GUI-Nachtrag) ---------------- */
805
+ /* Gegenrichtung zur MCP-Ansicht (E47-Muster): ob ein Harness an einem Ort MCP kann, entscheidet
806
+ nie der Harness-Name (E16), sondern einzig, ob MCP_PLAETZE (roh aus dem Kern, F42) diesen Ort
807
+ nennt. */
808
+ function mcpKannAn(ziel) {
809
+ var zk = zielKey(ziel);
810
+ return MCP_PLAETZE.some(function (p) { return zielKey(p) === zk; });
811
+ }
812
+
813
+ /** Die MCP-Zeilen eines Ziels, aus den drei Quellen der MCP-Ansicht zusammengesucht: eingehängte
814
+ Katalog-Einträge (mit Entfernen-Knopf), ihre Fundorte in der geteilten .mcp.json des Projekts
815
+ (E61-Nachtrag, seit E64 mit Übernehmen-Knopf) und Einträge, die dort hängen, aber nicht im
816
+ Katalog stehen (stiller Vermerk). */
817
+ function mcpZeilenFuerZiel(ziel) {
818
+ var zk = zielKey(ziel);
819
+ var zeilen = [];
820
+ MCP_KATALOG.forEach(function (k) {
821
+ if (k.eingehaengt.some(function (z) { return zielKey(z) === zk; })) {
822
+ zeilen.push({ art: 'katalog', name: k.eintrag.name, beschreibung: k.eintrag.beschreibung, eintrag: k.eintrag });
823
+ }
824
+ k.geteilteFundorte.forEach(function (g) {
825
+ if (zielKey(g.ziel) === zk)
826
+ zeilen.push({ art: 'geteilt', name: k.eintrag.name, beschreibung: k.eintrag.beschreibung, datei: g.datei });
827
+ });
828
+ });
829
+ MCP_FREMD.forEach(function (x) { if (zielKey(x.ziel) === zk) zeilen.push({ art: 'fremd', name: x.name }); });
830
+ // Ein Eintrag aus der geteilten .mcp.json, den der Katalog nicht kennt: „nicht im Katalog" allein
831
+ // läse sich wie eine Einladung, ihn in die mcp.yaml zu schreiben. Wo er wirklich steht, gehört
832
+ // daneben — und übernehmen lässt er sich auch ohne Katalog-Eintrag (E64, der Block kommt aus der
833
+ // Datei).
834
+ MCP_FREMD_GETEILT.forEach(function (x) {
835
+ if (zielKey(x.ziel) === zk) zeilen.push({ art: 'fremd-geteilt', name: x.name, datei: x.datei });
836
+ });
837
+ zeilen.sort(function (a, b) { return a.name.localeCompare(b.name, 'de'); });
838
+ return zeilen;
839
+ }
840
+
841
+ /** Der MCP-Abschnitt unter der Skill-Link-Tabelle einer Ziel-Karte (E61-GUI-Nachtrag): nur, wenn
842
+ dort etwas eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). */
843
+ function mcpAbschnitt(ziel) {
844
+ var zeilen = mcpZeilenFuerZiel(ziel);
845
+ if (zeilen.length === 0 && !mcpKannAn(ziel)) return null;
846
+ var abschnitt = e('div', 'mcpkarteabschnitt');
847
+ abschnitt.appendChild(e('span', 'etikett', 'MCP'));
848
+ if (zeilen.length === 0) {
849
+ abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
850
+ return abschnitt;
851
+ }
852
+ var ul = e('ul', 'mcpkartezeilen');
853
+ zeilen.forEach(function (row) {
854
+ var li = e('li');
855
+ li.appendChild(e('span', 'name', row.name));
856
+ if (row.art === 'katalog') {
857
+ li.appendChild(e('span', 'was', row.beschreibung || ''));
858
+ var weg = e('button', 'knopf leise klein', 'Entfernen');
859
+ weg.type = 'button';
860
+ weg.setAttribute('aria-label', row.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen');
861
+ weg.addEventListener('click', function () { frageMcpAus(row.eintrag, ziel); });
862
+ li.appendChild(weg);
863
+ } else if (row.art === 'geteilt' || row.art === 'fremd-geteilt') {
864
+ // E64: der Vermerk nennt die Bedeutung („steht im Projekt, nicht bei dir"), nicht mehr bloß
865
+ // den Dateinamen — und daneben steht der Weg, das zu ändern.
866
+ li.appendChild(row.art === 'geteilt'
867
+ ? e('span', 'was', row.beschreibung || '')
868
+ : e('span', 'was fehlt', 'nicht im Katalog'));
869
+ li.appendChild(e('span', 'geteiltvermerk', 'in der .mcp.json des Projekts'));
870
+ var her = e('button', 'knopf leise klein', 'Übernehmen …');
871
+ her.type = 'button';
872
+ her.setAttribute('aria-label', row.name + ' aus der .mcp.json des Projekts in deine private Konfiguration übernehmen');
873
+ her.addEventListener('click', function () { frageMcpUebernahme(row.name, ziel, row.datei); });
874
+ li.appendChild(her);
875
+ } else {
876
+ li.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
877
+ li.appendChild(e('span'));
878
+ }
879
+ ul.appendChild(li);
880
+ });
881
+ abschnitt.appendChild(ul);
882
+ return abschnitt;
883
+ }
884
+
885
+ /** Knopf „MCP hinzufügen …" im Kartenfuß neben „Skills hinzufügen" (E61-GUI-Nachtrag, Gegenrichtung
886
+ zur MCP-Ansicht): nur, wo der Harness an diesem Ort MCP kann (mcpKannAn, E16). */
887
+ function mcpHinzufuegenKnopf(ziel) {
888
+ if (!mcpKannAn(ziel)) return null;
889
+ var knopf = e('button', 'knopf leise klein mitsymbol', 'MCP hinzufügen …');
890
+ knopf.type = 'button';
891
+ knopf.insertBefore(harnessZeichen(ziel.harness), knopf.firstChild);
892
+ knopf.setAttribute('aria-label', 'MCP-Server zu ' + zielTitel({ ziel: ziel }) + ' hinzufügen (' + harnessName(ziel.harness) + ')');
893
+ knopf.addEventListener('click', function () { oeffneMcpHinzufuegen(ziel); });
894
+ return knopf;
895
+ }
896
+
742
897
  function zielKarte(z) {
743
898
  var karte = e('div', 'ziel');
744
899
  var kopf = e('div', 'zielkopf');
@@ -849,7 +1004,7 @@
849
1004
  quelle: defekt ? null : ei.link.checkout.quelle,
850
1005
  ref: defekt ? null : ei.link.checkout.ref,
851
1006
  einschraenkung: defekt ? null : z.einschraenkungen[name],
852
- beschreibung: defekt ? undefined : ei.link.skill.beschreibung,
1007
+ skill: defekt ? null : ei.link.skill,
853
1008
  teile: [],
854
1009
  };
855
1010
  reihenfolge.push(key);
@@ -858,11 +1013,28 @@
858
1013
  });
859
1014
  });
860
1015
 
1016
+ // MCPs hängen am Verzeichnis, nicht am Harness-Chip (E61-GUI-Nachtrag): der Abschnitt unter
1017
+ // „Alle" zeigt jedes Ziel, das an genau diesem Pfad MCP kann — dieselbe Fähigkeitsfrage wie unter
1018
+ // dem Harness-Chip, allein über den Ort aus MCP_PLAETZE gestellt. Kein Harness-Name im Filter
1019
+ // (E16): heute liefert das nur Claude Code, weil Hermes keine Projektebene für MCP kennt; käme
1020
+ // ein Harness mit Projektebene dazu, stünde er hier von selbst und fiele nicht stumm weg.
1021
+ var verzMcpZiele = MCP_PLAETZE.filter(function (p) {
1022
+ return p.ort.art === 'arbeitsverzeichnis' && p.ort.pfad === pfad;
1023
+ });
1024
+
861
1025
  if (reihenfolge.length === 0) {
862
1026
  var leer = e('div', 'leerzeile');
863
1027
  leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
864
1028
  leer.appendChild(alleSkillsKnopf(pfad, teile));
1029
+ verzMcpZiele.forEach(function (mz) {
1030
+ var mcpKnopfLeer = mcpHinzufuegenKnopf(mz);
1031
+ if (mcpKnopfLeer) leer.appendChild(mcpKnopfLeer);
1032
+ });
865
1033
  teil.appendChild(leer);
1034
+ verzMcpZiele.forEach(function (mz) {
1035
+ var mcpAbschnLeer = mcpAbschnitt(mz);
1036
+ if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1037
+ });
866
1038
  return teil;
867
1039
  }
868
1040
 
@@ -896,13 +1068,7 @@
896
1068
  nameSpan.appendChild(document.createTextNode(row.name));
897
1069
  li.appendChild(nameSpan);
898
1070
 
899
- if (row.beschreibung) {
900
- var wasEl = e('span', 'was', row.beschreibung);
901
- if (row.beschreibung.length > 88) wasEl.title = row.beschreibung;
902
- li.appendChild(wasEl);
903
- } else {
904
- li.appendChild(e('span', 'was fehlt', 'keine Beschreibung in der SKILL.md'));
905
- }
1071
+ li.appendChild(wasZelle(row.skill));
906
1072
 
907
1073
  var qz = e('span', 'quelle');
908
1074
  if (row.defekt) {
@@ -949,8 +1115,19 @@
949
1115
  });
950
1116
  teil.appendChild(ul);
951
1117
 
1118
+ // MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
1119
+ // der an diesem Pfad MCP kann.
1120
+ verzMcpZiele.forEach(function (mz) {
1121
+ var mcpAbschnAlle = mcpAbschnitt(mz);
1122
+ if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1123
+ });
1124
+
952
1125
  var fuss = e('div', 'zielfuss');
953
1126
  fuss.appendChild(alleSkillsKnopf(pfad, teile));
1127
+ verzMcpZiele.forEach(function (mz) {
1128
+ var mcpKnopfAlle = mcpHinzufuegenKnopf(mz);
1129
+ if (mcpKnopfAlle) fuss.appendChild(mcpKnopfAlle);
1130
+ });
954
1131
  teil.appendChild(fuss);
955
1132
 
956
1133
  return teil;
@@ -1048,7 +1225,10 @@
1048
1225
  // gehört zum gewählten Harness, nicht zur Karte: er steht rechts in der Chip-Leiste, relativ
1049
1226
  // zum Verzeichnis, mit der Inaktiv-Marke daneben.
1050
1227
  if (gewaehlt) {
1051
- var unterhalb = gewaehlt.verzeichnis.indexOf(pfad + '/') === 0;
1228
+ // Nur die Anzeige: liegt der gelesene Ordner unterhalb des Verzeichnisses, steht er
1229
+ // relativ, sonst gekürzt. Beide Trenner, sonst greift das unter Windows nie (E68).
1230
+ var unterhalb = gewaehlt.verzeichnis.indexOf(pfad + '/') === 0
1231
+ || gewaehlt.verzeichnis.indexOf(pfad + '\\') === 0;
1052
1232
  chipleiste.appendChild(e('span', 'kartenordner mono',
1053
1233
  unterhalb ? gewaehlt.verzeichnis.slice(pfad.length + 1) : pfadKurz(gewaehlt.verzeichnis)));
1054
1234
  if (!harnessAktiv(aktivChip)) chipleiste.appendChild(e('span', 'ruhend', 'Harness inaktiv'));
@@ -1215,7 +1395,7 @@
1215
1395
  function frageLoeschen(name, ziel, verzeichnis) {
1216
1396
  loeschenKontext = { name: name, ziel: ziel };
1217
1397
  $('dl-titel').textContent = '„' + name + '“ löschen?';
1218
- $('dl-text').textContent = 'hmgr löscht ' + pfadKurz(verzeichnis) + '/' + name
1398
+ $('dl-text').textContent = 'hmgr löscht ' + pfadKurz(verzeichnis) + TRENNER + name
1219
1399
  + ' unwiderruflich von der Platte, ein Ordner mit allem Inhalt oder ein Link. Das lässt sich nicht rückgängig machen.';
1220
1400
  zeige($('d-loeschen'));
1221
1401
  }
@@ -1400,7 +1580,7 @@
1400
1580
  chip.addEventListener('click', function () { setzeQuellenfilter(s.quelleSchluessel); });
1401
1581
 
1402
1582
  // Quellen-Chip-Paar (E60): links filtert der Name, rechts öffnet das Pfeilsymbol die Quelle
1403
- // (Repo-Seite im Browser, lokale Arbeitskopie im Finder). Kein <a> im <button>, deshalb
1583
+ // (Repo-Seite im Browser, lokale Arbeitskopie im Dateimanager). Kein <a> im <button>, deshalb
1404
1584
  // stehen sie nebeneinander in einer gemeinsamen Hülle.
1405
1585
  var quelle = QUELLEN.filter(function (q) { return q.text === s.quelleText; })[0];
1406
1586
  if (quelle) {
@@ -1411,7 +1591,7 @@
1411
1591
  if (lokal) oeffnen.type = 'button'; else oeffnen.href = '#';
1412
1592
  oeffnen.className = 'chipoeffnen';
1413
1593
  oeffnen.title = quelle.text;
1414
- oeffnen.setAttribute('aria-label', s.quelleName + ' ' + (lokal ? 'im Finder öffnen' : 'im Browser öffnen'));
1594
+ oeffnen.setAttribute('aria-label', s.quelleName + ' ' + (lokal ? 'im Dateimanager öffnen' : 'im Browser öffnen'));
1415
1595
  oeffnen.appendChild(externSvg());
1416
1596
  oeffnen.addEventListener('click', function (ev) {
1417
1597
  ev.preventDefault();
@@ -1455,6 +1635,24 @@
1455
1635
  return box;
1456
1636
  }
1457
1637
 
1638
+ /** Beschreibungs-Zelle der Ziel-Tabellen: die Beschreibung (E39) und darunter die immer
1639
+ sichtbaren Frontmatter-Marken (E55), derselbe Helfer wie im Katalog. Ohne Skill (Link ohne
1640
+ Quelle) bleibt nur die Fehlt-Zeile. */
1641
+ function wasZelle(skill) {
1642
+ var feld = e('span', 'wasfeld');
1643
+ var beschreibung = skill ? skill.beschreibung : undefined;
1644
+ if (beschreibung) {
1645
+ var wasEl = e('span', 'was', beschreibung);
1646
+ if (beschreibung.length > 88) wasEl.title = beschreibung;
1647
+ feld.appendChild(wasEl);
1648
+ } else {
1649
+ feld.appendChild(e('span', 'was fehlt', 'keine Beschreibung in der SKILL.md'));
1650
+ }
1651
+ var marken = skill ? skillMarkenBox(skill) : null;
1652
+ if (marken) feld.appendChild(marken);
1653
+ return feld;
1654
+ }
1655
+
1458
1656
  /** Zeilen des Aufklappbereichs einer Skill-Zeile aus dem Frontmatter (E55), nur wenn vorhanden. */
1459
1657
  function skillDetails(skill) {
1460
1658
  var zeilen = [];
@@ -1629,7 +1827,7 @@
1629
1827
  li.appendChild(e('span', r.ok ? 'ok' : 'aus', r.ok ? 'Link' : 'Fehler'));
1630
1828
  var s = e('span');
1631
1829
  s.appendChild(beschriftet(e('b', null, zielTitel(r.ziel)), r.ziel.ziel.harness));
1632
- s.appendChild(e('span', 'mono', r.ok ? pfadKurz(r.ziel.verzeichnis) + '/' + skill.name : r.fehler));
1830
+ s.appendChild(e('span', 'mono', r.ok ? pfadKurz(r.ziel.verzeichnis) + TRENNER + skill.name : r.fehler));
1633
1831
  li.appendChild(s);
1634
1832
  ul.appendChild(li);
1635
1833
  });
@@ -1939,6 +2137,35 @@
1939
2137
  });
1940
2138
  window.hmgr.onAktualisierenAnstossen(function () { aktualisierenLauf(true); });
1941
2139
 
2140
+ // Stilles Neuladen bei Änderungen außerhalb der GUI (E62, Fokus-Wechsel und Datei-Watcher): nur
2141
+ // ladeAlles, keine Meldung, kein Spinner-Zwang. Nachrutsch-sicher: höchstens ein Durchlauf läuft,
2142
+ // höchstens ein weiterer ist vorgemerkt, ein laufender Durchlauf wird nie von einem zweiten
2143
+ // überholt. Fehler landen wie sonst über rufe() in der Meldungsleiste, kein stiller Absturz.
2144
+ var stillerReloadLaeuft = false;
2145
+ var stillerReloadVorgemerkt = false;
2146
+ async function stillerReload() {
2147
+ // Läuft gerade ein Aktualisieren-Lauf, wird nicht parallel geladen: er endet ohnehin mit
2148
+ // ladeAlles und nimmt die Änderung mit. Sonst bauten zwei Läufe gleichzeitig die Ansicht auf.
2149
+ if ($('updatestart').disabled) return;
2150
+ if (stillerReloadLaeuft) {
2151
+ stillerReloadVorgemerkt = true;
2152
+ return;
2153
+ }
2154
+ stillerReloadLaeuft = true;
2155
+ try {
2156
+ await ladeAlles();
2157
+ } catch (fehler) {
2158
+ // rufe() hat den Fehler schon gemeldet, hier bleibt nur: den Durchlauf nicht wiederholen.
2159
+ } finally {
2160
+ stillerReloadLaeuft = false;
2161
+ if (stillerReloadVorgemerkt) {
2162
+ stillerReloadVorgemerkt = false;
2163
+ void stillerReload();
2164
+ }
2165
+ }
2166
+ }
2167
+ window.hmgr.onNeuLadenAnstossen(function () { void stillerReload(); });
2168
+
1942
2169
  function baueBericht(ergebnis) {
1943
2170
  var box = $('bericht');
1944
2171
  box.textContent = '';
@@ -2030,6 +2257,391 @@
2030
2257
  });
2031
2258
  }
2032
2259
 
2260
+ /* ================= MCP (E61-Folgerunde, F46: reiner Katalog) ================= */
2261
+ /* Der Katalog aus ~/.hmgr/mcp.yaml wird hier nicht bearbeitet, hmgr liest ihn nur: kein Anlegen,
2262
+ kein Ändern, nur Einhängen (entwurf/gui.html). Abweichung vom Entwurf (F43): kein Erfolgsdialog
2263
+ und kein Wirkungs-Vermerk an der Orts-Zeile, Erfolg läuft über die bestehende Meldungsleiste
2264
+ (melde()), bei Claude Code mit dem Hinweis des Kerns zur nächsten Sitzung angehängt, bei Hermes
2265
+ ohne (der Kern liefert den Hinweis nur für Claude Code).
2266
+ F46 („die Ziele gehören nicht in die MCP, sondern die MCP in die Ziele"): wo ein Server hängt und
2267
+ das Aushängen zeigen seither nur noch die Ziel-Karten (mcpAbschnitt weiter unten), diese Ansicht
2268
+ bleibt reiner Katalog — Name, Beschreibung, Homepage, Konfigblock, F39-Warnzeile, Notizen,
2269
+ Einhängen. mcpOrtDatei/-Scope/-Spur bleiben hier stehen, sie tragen weiterhin den Einhängen-
2270
+ Dialog (baueMcpWahl) und den Aushängen-Text (frageMcpAus), beide von den Ziel-Karten geteilt. */
2271
+
2272
+ /* Wohin der Eintrag geschrieben wird, rein zur Anzeige: Claude Code führt Projekt- wie globale
2273
+ Server in ~/.claude.json (die geteilte .mcp.json der Projektwurzel bleibt unangetastet, E57),
2274
+ Hermes je Ort in seiner config.yaml. */
2275
+ function mcpOrtDatei(ziel) {
2276
+ if (ziel.harness === 'hermes')
2277
+ return ziel.ort.art === 'profil'
2278
+ ? pfadSchreibweise('~/.hermes/profiles/' + ziel.ort.name + '/config.yaml')
2279
+ : pfadSchreibweise('~/.hermes/config.yaml');
2280
+ return pfadSchreibweise('~/.claude.json');
2281
+ }
2282
+ function mcpOrtScope(ziel) {
2283
+ if (ziel.harness !== 'claude-code') return null;
2284
+ return ziel.ort.art === 'arbeitsverzeichnis' ? 'Scope local' : 'Scope user';
2285
+ }
2286
+ /** Was in der Ort-Zeile hinter dem Namen steht: die Datei mit dem Scope. Anders als im Entwurf
2287
+ auch beim Arbeitsverzeichnis, denn der Ortsname der App trägt den Pfad schon (`ortName`); im
2288
+ Entwurf hieß er nur „Arbeitsverzeichnis", weshalb der Pfad dort hier stand. Jede Angabe
2289
+ einmal: Pfad im Namen, Datei und Scope daneben. */
2290
+ function mcpOrtSpur(ziel) {
2291
+ var scope = mcpOrtScope(ziel);
2292
+ return mcpOrtDatei(ziel) + (scope ? ' · ' + scope : '');
2293
+ }
2294
+
2295
+ function mcpKonfigArt(konfig) { return 'command' in konfig ? 'stdio' : 'HTTP'; }
2296
+ /** Der Konfigblock im Wortlaut, so wie er in der mcp.yaml steht: die ${VAR}-Verweise darin bleiben
2297
+ sichtbar, nur sie sind der Grund für die Warnzeile darunter. Nie ein Klartext-Secret (F41). */
2298
+ function mcpKonfigWortlaut(konfig) {
2299
+ if ('command' in konfig) {
2300
+ var text = [konfig.command].concat(konfig.args || []).join(' ');
2301
+ Object.keys(konfig.env || {}).forEach(function (k) { text += ' · ' + k + '=' + konfig.env[k]; });
2302
+ return text;
2303
+ }
2304
+ var url = konfig.url;
2305
+ Object.keys(konfig.headers || {}).forEach(function (k) { url += ' · ' + k + ': ' + konfig.headers[k]; });
2306
+ return url;
2307
+ }
2308
+
2309
+ /** Die Homepage eines Servers, dasselbe Muster wie eine Quelle (E60): der Name bleibt ruhig, das
2310
+ Signalorange trägt nur das Pfeilsymbol. Öffnet über denselben Kanal wie eine Quelle (E60): eine
2311
+ http(s)-Adresse geht in den Browser. */
2312
+ function mcpHeimLink(url) {
2313
+ var a = document.createElement('a');
2314
+ a.className = 'qlink';
2315
+ a.href = '#';
2316
+ a.title = url;
2317
+ a.setAttribute('aria-label', url + ' im Browser öffnen');
2318
+ a.appendChild(document.createTextNode(url));
2319
+ a.appendChild(externSvg());
2320
+ a.addEventListener('click', function (ev) {
2321
+ ev.preventDefault();
2322
+ window.hmgr.quelleOeffnen(url).catch(function () {});
2323
+ });
2324
+ return a;
2325
+ }
2326
+
2327
+ /** F39: die Prüf-Funktion ist dieselbe wie beim tatsächlichen Einhängen, hier aber nur gegen die
2328
+ Prozess-Umgebung von hmgr (der Main-Prozess liefert `fehlendeVariablen` schon fertig mit der
2329
+ Übersicht, ohne den ortsbezogenen .env-Abgleich, der bleibt dem Einhängen selbst vorbehalten
2330
+ und kommt dann als `warnung` zurück). Wiederverwendet die bestehende .ruhezeile-Optik. */
2331
+ function mcpWarnZeile(fehlend) {
2332
+ var mehrzahl = fehlend.length > 1;
2333
+ var namen = fehlend.map(function (v) { return '${' + v + '}'; }).join(', ');
2334
+ var rz = e('div', 'ruhezeile');
2335
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2336
+ svg.setAttribute('viewBox', '0 0 16 16');
2337
+ svg.setAttribute('stroke-width', '1.6');
2338
+ svg.setAttribute('aria-hidden', 'true');
2339
+ svg.innerHTML = '<circle cx="8" cy="8" r="6.5"/><path d="M8 4.5v4M8 10.5v.5"/>';
2340
+ rz.appendChild(svg);
2341
+ rz.appendChild(e('p', null, namen + (mehrzahl ? ' sind' : ' ist') + ' in der Umgebung von hmgr nicht gesetzt. Ob '
2342
+ + (mehrzahl ? 'die Variablen' : 'die Variable') + ' beim Start des Harness gesetzt ' + (mehrzahl ? 'sind' : 'ist')
2343
+ + ', kann hmgr nicht wissen.'));
2344
+ return rz;
2345
+ }
2346
+
2347
+ /** Notizen sind das Langsame am Eintrag (entwurf/gui.html): zugeklappt, öffnen sich nur, wenn
2348
+ jemand sie sucht. Der Wortlaut bleibt, wie er in der mcp.yaml steht, Zeilenumbrüche
2349
+ eingeschlossen (weiss-raum: pre-line in styles.css). */
2350
+ function mcpNotizenBlock(text) {
2351
+ var d = document.createElement('details');
2352
+ d.className = 'notizen';
2353
+ var s = document.createElement('summary');
2354
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2355
+ pfeil.setAttribute('viewBox', '0 0 16 16');
2356
+ pfeil.setAttribute('class', 'keil');
2357
+ pfeil.setAttribute('fill', 'none');
2358
+ pfeil.setAttribute('stroke', 'currentColor');
2359
+ pfeil.setAttribute('stroke-width', '1.7');
2360
+ pfeil.setAttribute('stroke-linejoin', 'round');
2361
+ pfeil.setAttribute('aria-hidden', 'true');
2362
+ pfeil.innerHTML = '<path d="M4.3 2.5 7.8 6 4.3 9.5"/>';
2363
+ s.appendChild(pfeil);
2364
+ s.appendChild(document.createTextNode('Notizen'));
2365
+ d.appendChild(s);
2366
+ d.appendChild(e('p', null, text));
2367
+ return d;
2368
+ }
2369
+
2370
+ function mcpKarte(k) {
2371
+ var eintrag = k.eintrag;
2372
+ var kEl = e('div', 'mcp');
2373
+
2374
+ var kopf = e('div', 'mcpkopf');
2375
+ kopf.appendChild(e('b', null, eintrag.name));
2376
+ if (eintrag.homepage) kopf.appendChild(mcpHeimLink(eintrag.homepage));
2377
+ var ein = e('button', 'knopf leise klein', 'Einhängen …');
2378
+ ein.type = 'button';
2379
+ ein.setAttribute('aria-label', eintrag.name + ' einhängen');
2380
+ ein.addEventListener('click', function () { oeffneMcpEinhaengen(k); });
2381
+ kopf.appendChild(ein);
2382
+ kEl.appendChild(kopf);
2383
+
2384
+ kEl.appendChild(e('p', 'was', eintrag.beschreibung));
2385
+
2386
+ var konf = e('div', 'kette mcpkonfig');
2387
+ konf.appendChild(e('span', 'glied', mcpKonfigArt(eintrag.konfig)));
2388
+ konf.appendChild(e('span', 'mono', mcpKonfigWortlaut(eintrag.konfig)));
2389
+ kEl.appendChild(konf);
2390
+
2391
+ if (k.fehlendeVariablen.length > 0) kEl.appendChild(mcpWarnZeile(k.fehlendeVariablen));
2392
+ if (eintrag.notizen) kEl.appendChild(mcpNotizenBlock(eintrag.notizen));
2393
+
2394
+ return kEl;
2395
+ }
2396
+
2397
+ /** Ohne Katalog steht hier kein leerer Kasten, sondern der Weg zur Datei: anlegen lässt sich ein
2398
+ Eintrag nur dort, hmgr schreibt sie nie (E61). */
2399
+ function mcpEinladung() {
2400
+ var kasten = e('div', 'startfeld mcpstart');
2401
+ var p1 = e('p');
2402
+ p1.appendChild(document.createTextNode('Noch kein Server im Katalog. hmgr liest ihn aus '));
2403
+ p1.appendChild(e('span', 'mono', pfadSchreibweise('~/.hmgr/mcp.yaml')));
2404
+ p1.appendChild(document.createTextNode(' und schreibt die Datei nie.'));
2405
+ kasten.appendChild(p1);
2406
+ var p2 = e('p');
2407
+ p2.appendChild(document.createTextNode('Trag einen Server dort von Hand ein, oder lass ihn von dem LLM eintragen, das ihn ohnehin gerade einrichtet. Das Format steht in '));
2408
+ p2.appendChild(e('span', 'mono', 'hmgr help mcp'));
2409
+ p2.appendChild(document.createTextNode('.'));
2410
+ kasten.appendChild(p2);
2411
+ return kasten;
2412
+ }
2413
+
2414
+ function baueMcp() {
2415
+ var w = $('mcpliste');
2416
+ w.textContent = '';
2417
+ if (MCP_KATALOG.length === 0) w.appendChild(mcpEinladung());
2418
+ else MCP_KATALOG.forEach(function (k) { w.appendChild(mcpKarte(k)); });
2419
+ $('navzahlmcp').textContent = MCP_KATALOG.length;
2420
+ }
2421
+
2422
+ /* ---------------- Einhängen (F42) ---------------- */
2423
+
2424
+ var mcpLaufend = null;
2425
+ var mcpPlaetzeAuswahl = [];
2426
+ // Sperre gegen Doppel-Anstoß (wie beim Hinzufügen einer Quelle, E54): zwischen Klick und Dialog
2427
+ // liegt ein IPC-Aufruf. Ein zweiter Klick in dieser Lücke riefe showModal() an einem schon
2428
+ // offenen Dialog auf, und das ist ein Fehler, kein zweiter Dialog.
2429
+ var mcpWahlOeffnet = false;
2430
+
2431
+ async function oeffneMcpEinhaengen(k) {
2432
+ if (mcpWahlOeffnet || $('d-mcp').open) return;
2433
+ mcpWahlOeffnet = true;
2434
+ try {
2435
+ await baueMcpWahl(k);
2436
+ } finally {
2437
+ mcpWahlOeffnet = false;
2438
+ }
2439
+ }
2440
+
2441
+ async function baueMcpWahl(k) {
2442
+ mcpLaufend = k;
2443
+ $('dm-name').textContent = k.eintrag.name;
2444
+ $('dm-text').textContent = 'Ein Ort je Einhängen. agents kennt keine MCP-Server und steht deshalb nicht zur Wahl.';
2445
+ var box = $('dm-orte');
2446
+ box.textContent = '';
2447
+ // Die Plätze kommen aus dem Kern (F42, E16): die Fähigkeit hängt am Adapter, nicht am
2448
+ // Harness-Namen, hier wird nicht selbst entschieden, wer welche Ebene kennt.
2449
+ var plaetze = await rufe(function () { return window.hmgr.mcpPlaetze(); });
2450
+ mcpPlaetzeAuswahl = plaetze;
2451
+ plaetze.forEach(function (ziel, i) {
2452
+ var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
2453
+ var aktiv = harnessAktiv(ziel.harness);
2454
+ var label = e('label', 'wahlzeile');
2455
+ var input = document.createElement('input');
2456
+ input.type = 'radio';
2457
+ input.name = 'mcport';
2458
+ input.value = String(i);
2459
+ input.disabled = schon || !aktiv;
2460
+ label.appendChild(input);
2461
+
2462
+ var txt = e('span');
2463
+ txt.appendChild(beschriftet(e('b', null, zielTitel({ ziel: ziel })), ziel.harness));
2464
+ txt.appendChild(e('span', 'mono', mcpOrtSpur(ziel)));
2465
+
2466
+ if (schon) {
2467
+ // F46: die Orte-Liste am Katalog-Eintrag gibt es nicht mehr, der Weg zum Aushängen führt
2468
+ // über den Entfernen-Knopf im MCP-Abschnitt der Ziel-Karte.
2469
+ txt.appendChild(e('span', 'notiz', 'Hängt hier schon. Zum Aushängen den Entfernen-Knopf am MCP-Abschnitt der Ziel-Karte benutzen.'));
2470
+ } else if (!aktiv) {
2471
+ txt.appendChild(e('span', 'notiz', harnessName(ziel.harness) + ' ist inaktiv. Schalt den Harness in den Einstellungen wieder ein, um hier einzuhängen.'));
2472
+ } else if (ziel.ort.art === 'arbeitsverzeichnis') {
2473
+ // E64: die Datei bleibt beim Einhängen unangetastet, aber nicht mehr grundsätzlich — der
2474
+ // Übernehmen-Knopf an der Ziel-Karte fasst sie auf ausdrücklichen Auftrag an.
2475
+ txt.appendChild(e('span', 'notiz', 'Gilt nur für dich. Eingehängt wird neben der geteilten .mcp.json des Projekts, die bleibt dabei unverändert.'));
2476
+ } else if (ziel.harness === 'hermes' && ziel.ort.art === 'home') {
2477
+ txt.appendChild(e('span', 'notiz', 'Hermes kennt keine Projektebene für MCP-Server, hier geht nur global.'));
2478
+ }
2479
+ label.appendChild(txt);
2480
+ box.appendChild(label);
2481
+ });
2482
+ zeige($('d-mcp'));
2483
+ }
2484
+
2485
+ $('d-mcp').addEventListener('submit', function (ev) {
2486
+ if (ev.submitter && ev.submitter.value === 'los') {
2487
+ var gewaehlt = $('dm-orte').querySelector('input:checked');
2488
+ if (!gewaehlt) ev.preventDefault();
2489
+ }
2490
+ });
2491
+
2492
+ nachDialog($('d-mcp'), async function (wert) {
2493
+ var k = mcpLaufend;
2494
+ mcpLaufend = null;
2495
+ if (wert !== 'los' || !k) return;
2496
+ var gewaehlt = $('dm-orte').querySelector('input:checked');
2497
+ if (!gewaehlt) return;
2498
+ var ziel = mcpPlaetzeAuswahl[+gewaehlt.value];
2499
+ var ergebnis = await rufe(function () {
2500
+ return window.hmgr.mcpEinhaengen({ name: k.eintrag.name, harness: ziel.harness, ort: ziel.ort });
2501
+ });
2502
+ await ladeAlles();
2503
+ // Erfolg über die Meldungsleiste (F43): kein Erfolgsdialog, kein Vermerk an der Orts-Zeile. Der
2504
+ // Kern liefert den Hinweis zur Wirkung nur bei Claude Code, Hermes lädt selbst nach.
2505
+ var teile = [k.eintrag.name + ' hängt jetzt in ' + zielTitel({ ziel: ziel }) + '.'];
2506
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2507
+ if (ergebnis.warnung) teile.push(ergebnis.warnung);
2508
+ melde(teile.join('\n'));
2509
+ });
2510
+
2511
+ /* ---------------- Aushängen ---------------- */
2512
+
2513
+ var mcpAusKontext = null;
2514
+ function frageMcpAus(eintrag, ziel) {
2515
+ mcpAusKontext = { eintrag: eintrag, ziel: ziel };
2516
+ $('dma-titel').textContent = eintrag.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen?';
2517
+ $('dma-text').textContent = 'Der Eintrag verschwindet aus ' + mcpOrtDatei(ziel)
2518
+ + '. Der Katalog-Eintrag bleibt, du kannst ihn jederzeit wieder einhängen.';
2519
+ zeige($('d-mcpaus'));
2520
+ }
2521
+ nachDialog($('d-mcpaus'), async function (wert) {
2522
+ var kontext = mcpAusKontext;
2523
+ mcpAusKontext = null;
2524
+ if (wert !== 'ja' || !kontext) return;
2525
+ var ergebnis = await rufe(function () {
2526
+ return window.hmgr.mcpAushaengen({ name: kontext.eintrag.name, harness: kontext.ziel.harness, ort: kontext.ziel.ort });
2527
+ });
2528
+ await ladeAlles();
2529
+ var teile = [kontext.eintrag.name + ' aus ' + zielTitel({ ziel: kontext.ziel }) + ' ausgehängt.'];
2530
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2531
+ melde(teile.join('\n'));
2532
+ });
2533
+
2534
+ /* ---------------- Übernehmen aus der geteilten .mcp.json (E64) ---------------- */
2535
+ /* Der einzige Weg, auf dem hmgr die geteilte Datei anfasst, deshalb eine ausdrückliche Rückfrage:
2536
+ der Eintrag wandert in die private Konfiguration und verschwindet aus der Datei — für alle, die
2537
+ sie teilen. Der Name muss nicht im Katalog stehen, übernommen wird der Block aus der Datei. */
2538
+ var mcpUebernahmeKontext = null;
2539
+ function frageMcpUebernahme(name, ziel, datei) {
2540
+ mcpUebernahmeKontext = { name: name, ziel: ziel };
2541
+ $('dmu-titel').textContent = name + ' übernehmen?';
2542
+ $('dmu-text').textContent = name + ' wird in deine private Konfiguration für '
2543
+ + zielTitel({ ziel: ziel }) + ' übernommen und aus ' + (datei || '.mcp.json')
2544
+ + ' entfernt. Ist die Datei mit dem Team geteilt, verlieren die anderen den Eintrag, sobald die '
2545
+ + 'Änderung committet ist.';
2546
+ zeige($('d-mcpueb'));
2547
+ }
2548
+ nachDialog($('d-mcpueb'), async function (wert) {
2549
+ var kontext = mcpUebernahmeKontext;
2550
+ mcpUebernahmeKontext = null;
2551
+ if (wert !== 'ja' || !kontext) return;
2552
+ var ergebnis = await rufe(function () {
2553
+ return window.hmgr.mcpUebernehmen({ name: kontext.name, harness: kontext.ziel.harness, ort: kontext.ziel.ort });
2554
+ });
2555
+ await ladeAlles();
2556
+ var teile = [kontext.name + ' gehört jetzt zu deiner privaten Konfiguration für '
2557
+ + zielTitel({ ziel: kontext.ziel }) + ' und steht nicht mehr in ' + ergebnis.datei + '.'];
2558
+ if (ergebnis.hinweis) teile.push(ergebnis.hinweis);
2559
+ melde(teile.join('\n'));
2560
+ });
2561
+
2562
+ /* ---------------- MCP hinzufügen (E61-GUI-Nachtrag) ---------------- */
2563
+ /* Gegenrichtung zu oeffneMcpEinhaengen: von der Ziel-Karte aus, Mehrfachauswahl über den ganzen
2564
+ Katalog, dasselbe Dialogmuster wie „Skills hinzufügen" (E47) statt der Ortswahl von d-mcp. */
2565
+ var mcpHinzufuegenKontext = null;
2566
+ function dmhBilanzAktualisieren() {
2567
+ var n = $('dmh-liste').querySelectorAll('input:checked').length;
2568
+ $('dmh-bilanz').textContent = zahlwort(n, 'ausgewählt', 'ausgewählt');
2569
+ }
2570
+ function oeffneMcpHinzufuegen(ziel) {
2571
+ mcpHinzufuegenKontext = { ziel: ziel };
2572
+ $('dmh-ziel').textContent = zielTitel({ ziel: ziel });
2573
+ var box = $('dmh-liste');
2574
+ box.textContent = '';
2575
+ // Ohne Katalog stünde hier ein leerer Kasten mit einem Knopf, der nichts tut: hmgr legt
2576
+ // MCP-Einträge nie selbst an (E61), also nennt der Dialog denselben Weg wie die MCP-Ansicht.
2577
+ if (MCP_KATALOG.length === 0) {
2578
+ box.appendChild(e('p', 'ohne', 'Noch kein Server im Katalog. hmgr liest ihn aus '
2579
+ + pfadSchreibweise('~/.hmgr/mcp.yaml') + ' und schreibt die Datei nie.'));
2580
+ }
2581
+ MCP_KATALOG.forEach(function (k, i) {
2582
+ var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
2583
+ var label = e('label', 'wahlzeile');
2584
+ var input = document.createElement('input');
2585
+ input.type = 'checkbox';
2586
+ input.value = String(i);
2587
+ input.disabled = schon;
2588
+ input.addEventListener('change', dmhBilanzAktualisieren);
2589
+ label.appendChild(input);
2590
+ var txt = e('span');
2591
+ txt.appendChild(e('b', null, k.eintrag.name));
2592
+ if (k.eintrag.beschreibung) txt.appendChild(e('span', 'mono', k.eintrag.beschreibung));
2593
+ if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
2594
+ label.appendChild(txt);
2595
+ box.appendChild(label);
2596
+ });
2597
+ dmhBilanzAktualisieren();
2598
+ zeige($('d-mcp-hinzufuegen'));
2599
+ }
2600
+
2601
+ $('d-mcp-hinzufuegen').addEventListener('submit', function (ev) {
2602
+ if (ev.submitter && ev.submitter.value === 'los') {
2603
+ var gewaehlt = Array.from($('dmh-liste').querySelectorAll('input:checked'));
2604
+ if (gewaehlt.length === 0) ev.preventDefault();
2605
+ }
2606
+ });
2607
+
2608
+ nachDialog($('d-mcp-hinzufuegen'), async function (wert) {
2609
+ if (wert !== 'los' || mcpHinzufuegenKontext === null) return;
2610
+ var ziel = mcpHinzufuegenKontext.ziel;
2611
+ var indizes = Array.from($('dmh-liste').querySelectorAll('input:checked')).map(function (i) { return +i.value; });
2612
+ var erfolge = 0;
2613
+ var hinweise = [];
2614
+ var fehlschlaege = [];
2615
+ for (var n = 0; n < indizes.length; n++) {
2616
+ var k = MCP_KATALOG[indizes[n]];
2617
+ try {
2618
+ var ergebnis = await rufe(function () {
2619
+ return window.hmgr.mcpEinhaengen({ name: k.eintrag.name, harness: ziel.harness, ort: ziel.ort });
2620
+ });
2621
+ erfolge++;
2622
+ if (ergebnis.hinweis && hinweise.indexOf(ergebnis.hinweis) < 0) hinweise.push(ergebnis.hinweis);
2623
+ // F39-Warnungen gesammelt im Erfolgs-Toast statt je Einhängen einzeln zu melden.
2624
+ if (ergebnis.warnung) hinweise.push(k.eintrag.name + ': ' + ergebnis.warnung);
2625
+ } catch (fehler) {
2626
+ // rufe() hat den Fehler zwar schon in die Meldungsleiste gesetzt, die Schluss-Meldung unten
2627
+ // überschreibt sie aber gleich wieder. Ein Teilerfolg muss beide Seiten nennen, sonst behauptet
2628
+ // der Toast einen Lauf ohne Fehler. Die übrigen Einträge laufen weiter.
2629
+ fehlschlaege.push(k.eintrag.name + ': ' + fehler.message);
2630
+ }
2631
+ }
2632
+ ZIEL_OFFEN[kartenKey(ziel)] = true;
2633
+ await ladeAlles();
2634
+ var zeilen = [erfolge > 0
2635
+ ? zahlwort(erfolge, 'MCP-Server hängt', 'MCP-Server hängen') + ' jetzt in ' + zielTitel({ ziel: ziel }) + '.'
2636
+ : 'Nichts eingehängt in ' + zielTitel({ ziel: ziel }) + '.'];
2637
+ hinweise.forEach(function (h) { zeilen.push(h); });
2638
+ if (fehlschlaege.length > 0) {
2639
+ zeilen.push(zahlwort(fehlschlaege.length, 'Eintrag ließ', 'Einträge ließen') + ' sich nicht einhängen:');
2640
+ fehlschlaege.forEach(function (f) { zeilen.push(f); });
2641
+ }
2642
+ melde(zeilen.join('\n'));
2643
+ });
2644
+
2033
2645
  /* ================= EINSTELLUNGEN ================= */
2034
2646
 
2035
2647
  /** Ziele eines Harness-Typs mit mindestens einem eigenen Link oder liegen gebliebener
@@ -2559,7 +3171,11 @@
2559
3171
 
2560
3172
  // Die Fragen kommen fertig formuliert aus dem Kern; für die Anzeige werden Home-Pfade darin
2561
3173
  // wie überall sonst auf ~ gekürzt (nur Anzeige, die Antwort transportiert keinen Text zurück).
2562
- function frageKurz(text) { return HOME ? text.split(HOME + '/').join('~/') : text; }
3174
+ /** Wie pfadKurz, aber mitten im Fließtext einer Rückfrage — und ebenso mit beiden Trennern (E68). */
3175
+ function frageKurz(text) {
3176
+ if (!HOME) return text;
3177
+ return text.split(HOME + '/').join('~/').split(HOME + '\\').join('~\\');
3178
+ }
2563
3179
  window.hmgr.onRueckfrage(function (nachricht) {
2564
3180
  if (nachricht.art === 'bestaetigen') {
2565
3181
  $('db-frage').textContent = frageKurz(nachricht.frage);
@@ -2607,5 +3223,5 @@
2607
3223
 
2608
3224
  /* ---------------- Start ---------------- */
2609
3225
 
2610
- ladeHome().then(ladeAlles).catch(function (fehler) { melde(fehler.message || String(fehler)); });
3226
+ ladeUmgebung().then(ladeAlles).catch(function (fehler) { melde(fehler.message || String(fehler)); });
2611
3227
  })();