hmgr 0.0.52 → 0.0.54

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.
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.54": [
5
+ "Der Dialog „Einhängen“ eines MCP-Servers hat jetzt ein Suchfeld. Tippen filtert die Orte nach Name und Pfad, so findest du ein Arbeitsverzeichnis ohne langes Scrollen."
6
+ ],
7
+ "0.0.53": [
8
+ "MCP-Server stehen an der Ordnerkarte jetzt in derselben Tabelle wie die Skills, mit dem Zeichen des Harness davor. Ein Tooltip erklärt, warum ein Server für Hermes fehlt: Hermes liest MCP-Server nur im Home oder in einem Profil, nicht pro Ordner. `hmgr mcp list` sagt das am Ende ebenfalls."
9
+ ],
4
10
  "0.0.52": [
5
11
  "Liegt ein Skill in einem Repo in mehreren Fassungen, je Werkzeug eine (etwa `.claude/skills` und `.hermes/skills`), verlinkt hmgr jetzt für jeden Harness die passende Fassung. Gibt es keine passende, nennt die Meldung die Pfade, und `hmgr link … --pfad` wählt einen aus."
6
12
  ],
package/dist/cli/hilfe.js CHANGED
@@ -424,6 +424,10 @@ global (Scope user). Hermes kennt nur global, mcp_servers in seiner config.yaml
424
424
  mit --ort profil:<name>); ein Projekt-Ziel ist bei Hermes ein Fehler, es kennt keine Projektebene für
425
425
  MCPs. agents kennt MCP-Server gar nicht.
426
426
 
427
+ Hängt ein Eintrag an einem Arbeitsverzeichnis und ist ein aktiver Harness ohne Projektebene (Hermes) da,
428
+ endet mcp list mit einer Zeile, die sagt, dass dieser Harness den Eintrag dort nicht liest, und auf
429
+ --ort home oder --ort profil:<name> verweist.
430
+
427
431
  Bei claude-code führt der Scope local die Server je Git-Repo, nicht je Unterordner (E91). Ein
428
432
  Arbeitsverzeichnis, das in einem Repo liegt, ohne dessen Wurzel zu sein, bekommt deshalb kein mcp add,
429
433
  mcp remove und mcp migrate: die Meldung nennt den Wurzelordner, dort gehören die Server hin (ist er
package/dist/cli/main.js CHANGED
@@ -741,6 +741,28 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
741
741
  for (const f of [...uebersicht.unbekannteGeteilteFundorte].sort((a, b) => nachName(a.name, b.name)))
742
742
  console.log(` ${f.name} ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope}`);
743
743
  }
744
+ // E120: hängt etwas an einem Arbeitsverzeichnis, erklärt eine Zeile, warum ein aktiver Harness
745
+ // ohne Ordner-MCP (Hermes) es dort nicht hat. Abgeleitet aus den Ebenen der Fähigkeit (E16).
746
+ const anOrdner = [
747
+ ...uebersicht.katalog.flatMap((k) => [...k.eingehaengt, ...k.geteilteFundorte.map((g) => g.ziel)]),
748
+ ...uebersicht.unbekannt.map((u) => u.ziel),
749
+ ...uebersicht.unbekannteGeteilteFundorte.map((u) => u.ziel),
750
+ ].some((z) => z.ort.art === 'arbeitsverzeichnis');
751
+ if (anOrdner) {
752
+ const aktive = await anwendungsfaelle.aktiveHarnesses();
753
+ const anzeigeNamen = { 'claude-code': 'Claude Code', hermes: 'Hermes', agents: 'Agents' };
754
+ for (const { harness, ebenen } of anwendungsfaelle.mcpEbenen()) {
755
+ if (!aktive.includes(harness) || ebenen.includes('arbeitsverzeichnis'))
756
+ continue;
757
+ const home = ebenen.includes('home');
758
+ const profil = ebenen.includes('profil');
759
+ if (!home && !profil)
760
+ continue;
761
+ const wo = home && profil ? 'im Home oder in einem Profil' : home ? 'im Home' : 'in einem Profil';
762
+ const orte = [...(home ? ['--ort home'] : []), ...(profil ? ['--ort profil:<name>'] : [])].join(' oder ');
763
+ console.log(`${anzeigeNamen[harness] ?? harness} liest MCP-Server nicht pro Ordner, nur ${wo} (${orte}).`);
764
+ }
765
+ }
744
766
  return 0;
745
767
  }
746
768
  if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'migrate')
package/dist/gui/main.js CHANGED
@@ -464,6 +464,7 @@ function registriereIpc() {
464
464
  }));
465
465
  // Plätze für den Einhängen-Dialog (F42): die Fähigkeit hängt am Adapter, nicht am Namen (E16),
466
466
  // deshalb kommt die Liste unverändert aus dem Kern statt aus einer Harness-Fallunterscheidung hier.
467
+ ipcMain.handle('hmgr:mcpEbenen', async () => auffangen(async () => anwendungsfaelle.mcpEbenen()));
467
468
  ipcMain.handle('hmgr:mcpPlaetze', async () => auffangen(async () => anwendungsfaelle.mcpPlaetze()));
468
469
  ipcMain.handle('hmgr:mcpEinhaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpEinhaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
469
470
  ipcMain.handle('hmgr:mcpAushaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpAushaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
@@ -997,7 +998,7 @@ function fensterOeffnen() {
997
998
  streifenHarnesse = streifen[0]?.querySelector('.zeilenzeichen')?.getAttribute('aria-label') ?? null;
998
999
  streifenText = streifen[0]?.querySelector('.bundlekopf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
999
1000
  alleGruppeZeilen = Array.from(streifen[0]?.querySelectorAll('.linkliste li .name') ?? []).map(nurName).join(',');
1000
- alleTabelleZeilen = Array.from(streifenKarte?.querySelectorAll('.linkliste li') ?? [])
1001
+ alleTabelleZeilen = Array.from(streifenKarte?.querySelectorAll('.linkliste:not(.mcpzeilen) li') ?? [])
1001
1002
  .filter((li) => li.closest('.bundlegruppe') === null && li.querySelector('.name'))
1002
1003
  .map((li) => nurName(li.querySelector('.name'))).join(',');
1003
1004
  Array.from(streifen[0]?.querySelectorAll('.bundlekopf button') ?? [])
@@ -1045,6 +1046,17 @@ function fensterOeffnen() {
1045
1046
  var mcpWahlTextFrei = null;
1046
1047
  // E91: die Wahlzeile des Unterordners im Repo ist deaktiviert und trägt die Notiz.
1047
1048
  var mcpUnterordnerGesperrt = null;
1049
+ var mcpSucheVorhanden = false;
1050
+ var mcpSucheLeerBeimOeffnen = false;
1051
+ var mcpSucheFokus = false;
1052
+ var mcpSucheZeilenGesamt = 0;
1053
+ var mcpSucheNurTreffer = false;
1054
+ var mcpSucheAndereVersteckt = false;
1055
+ var mcpSucheLeerhinweis = null;
1056
+ var mcpSucheLeerhinweisWeg = false;
1057
+ var mcpSucheAbgehakt = false;
1058
+ var mcpSucheEnterOffen = false;
1059
+ var mcpSucheNachNeuOeffnen = false;
1048
1060
  try {
1049
1061
  var mcpEinKnoepfe = Array.from(document.querySelectorAll('#mcpliste .mcpkopf button'))
1050
1062
  .filter((b) => (b.textContent || '').indexOf('Einhängen') === 0);
@@ -1062,6 +1074,44 @@ function fensterOeffnen() {
1062
1074
  mcpUnterordnerGesperrt = Array.from(document.querySelectorAll('#dm-orte .wahlzeile'))
1063
1075
  .filter((z) => (z.textContent || '').indexOf('unterordner') > -1)
1064
1076
  .map((z) => z.querySelector('input')?.disabled === true);
1077
+ // E122: Suchfeld im Dialog. Fokus, Filter, Leerhinweis, Abhaken beim Ausblenden,
1078
+ // Enter ohne Auswahl, Zustand nach erneutem Öffnen.
1079
+ var dmSuche = document.getElementById('dm-suche');
1080
+ mcpSucheVorhanden = dmSuche !== null;
1081
+ mcpSucheLeerBeimOeffnen = dmSuche?.value === '';
1082
+ mcpSucheFokus = document.activeElement?.id === 'dm-suche';
1083
+ var dmSetze = (wert) => {
1084
+ dmSuche.value = wert;
1085
+ dmSuche.dispatchEvent(new Event('input', { bubbles: true }));
1086
+ };
1087
+ var dmZeilen = () => Array.from(document.querySelectorAll('#dm-orte .wahlzeile'));
1088
+ mcpSucheZeilenGesamt = dmZeilen().length;
1089
+ dmSetze('unterordner');
1090
+ var dmSichtbar = dmZeilen().filter((z) => !z.hidden);
1091
+ mcpSucheNurTreffer = dmSichtbar.length > 0
1092
+ && dmSichtbar.every((z) => (z.textContent || '').indexOf('unterordner') > -1);
1093
+ mcpSucheAndereVersteckt = dmZeilen().some((z) => z.hidden);
1094
+ dmSetze('zzzkeintreffer');
1095
+ mcpSucheLeerhinweis = document.querySelector('#dm-orte .leerhinweis')?.textContent ?? null;
1096
+ dmSetze('');
1097
+ mcpSucheLeerhinweisWeg = document.querySelector('#dm-orte .leerhinweis') === null
1098
+ && dmZeilen().every((z) => !z.hidden);
1099
+ var dmFrei = dmZeilen().find((z) => z.querySelector('input')?.disabled === false);
1100
+ var dmFreiRadio = dmFrei?.querySelector('input');
1101
+ if (dmFreiRadio) {
1102
+ dmFreiRadio.checked = true;
1103
+ dmSetze('unterordner');
1104
+ mcpSucheAbgehakt = dmFrei.hidden === true && dmFreiRadio.checked === false;
1105
+ }
1106
+ dmSetze('');
1107
+ dmSuche.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1108
+ await new Promise((r) => setTimeout(r, 100));
1109
+ mcpSucheEnterOffen = document.getElementById('d-mcp')?.open === true;
1110
+ dmSetze('unterordner');
1111
+ document.querySelector('#d-mcp button[value="abbruch"]')?.click();
1112
+ await mcpWahlOeffnen(mcpEinKnoepfe[1]);
1113
+ mcpSucheNachNeuOeffnen = document.getElementById('dm-suche')?.value === ''
1114
+ && dmZeilen().length > 0 && dmZeilen().every((z) => !z.hidden);
1065
1115
  document.querySelector('#d-mcp button[value="abbruch"]')?.click();
1066
1116
  } catch (fehler) {
1067
1117
  mcpFehler = String(fehler);
@@ -1074,7 +1124,7 @@ function fensterOeffnen() {
1074
1124
  // Seit E64 stehen im MCP-Abschnitt zwei Knopfarten nebeneinander (Entfernen und
1075
1125
  // Übernehmen), deshalb wird hier nicht mehr der erste Knopf genommen, sondern der
1076
1126
  // Entfernen-Knopf an seinem aria-label erkannt.
1077
- var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpkartezeilen button'));
1127
+ var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpzeilen button'));
1078
1128
  try {
1079
1129
  var mcpAusKnopf = mcpZeilenKnoepfe.find(function (b) {
1080
1130
  return (b.getAttribute('aria-label') || '').indexOf('aushängen') > -1;
@@ -1126,13 +1176,20 @@ function fensterOeffnen() {
1126
1176
  var hermesKarte = karteMitKopftext('Hermes · Home');
1127
1177
  var hermesMcpText = hermesKarte?.querySelector('.mcpkarteabschnitt')?.textContent
1128
1178
  ?.replace(/\\s+/g, ' ').trim() ?? null;
1129
- var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
1179
+ var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpzeilen button').length ?? 0;
1130
1180
  var hermesMcpKnopfVorhanden = mcpHinzuKnopfAn(hermesKarte) !== undefined;
1131
1181
  var kundeXKarte = karteMitKopftext('kunde-x');
1132
1182
  var kundeXMcpText = kundeXKarte?.querySelector('.mcpkarteabschnitt')?.textContent
1133
1183
  ?.replace(/\\s+/g, ' ').trim() ?? null;
1134
- var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
1184
+ var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpzeilen button').length ?? 0;
1135
1185
  var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
1186
+ // E120: Tabellenkopf des MCP-Abschnitts und die Zeile mail (Katalog, unter „Alle").
1187
+ var kundeXMcpKopf = Array.from(kundeXKarte?.querySelectorAll('.mcpkarteabschnitt .linkkopf .etikett') ?? [])
1188
+ .map((x) => x.textContent).join('|');
1189
+ var kundeXMailZeile = Array.from(kundeXKarte?.querySelectorAll('.mcpzeilen li') ?? [])
1190
+ .find((li) => (li.querySelector('.name')?.textContent || '') === 'mail');
1191
+ var kundeXMailQuelle = kundeXMailZeile?.querySelector('.quelle')?.textContent ?? null;
1192
+ var kundeXMailZeichenTitel = kundeXMailZeile?.querySelector('.zeilenzeichen')?.getAttribute('title') ?? null;
1136
1193
  // Kartenkopf-Zahl (E71): der Kopf zählt Skills, gebündelt wie der Körper zeilt. An der
1137
1194
  // Verzeichnis-Karte kunde-x hängen dieselben zwei Skills an zwei Harnesses, eine Summe
1138
1195
  // über die Abschnitte ergäbe hier 4.
@@ -1359,6 +1416,17 @@ function fensterOeffnen() {
1359
1416
  mcpWahlText,
1360
1417
  mcpWahlTextFrei,
1361
1418
  mcpUnterordnerGesperrt,
1419
+ mcpSucheVorhanden,
1420
+ mcpSucheLeerBeimOeffnen,
1421
+ mcpSucheFokus,
1422
+ mcpSucheZeilenGesamt,
1423
+ mcpSucheNurTreffer,
1424
+ mcpSucheAndereVersteckt,
1425
+ mcpSucheLeerhinweis,
1426
+ mcpSucheLeerhinweisWeg,
1427
+ mcpSucheAbgehakt,
1428
+ mcpSucheEnterOffen,
1429
+ mcpSucheNachNeuOeffnen,
1362
1430
  mcpFehler,
1363
1431
  dMcpAusOffenNachOeffnen,
1364
1432
  dMcpAusOffenNachAbbrechen,
@@ -1373,6 +1441,9 @@ function fensterOeffnen() {
1373
1441
  kundeXMcpText,
1374
1442
  kundeXMcpZeilenKnopfZahl,
1375
1443
  kundeXMcpKnopfVorhanden,
1444
+ kundeXMcpKopf,
1445
+ kundeXMailQuelle,
1446
+ kundeXMailZeichenTitel,
1376
1447
  kundeXKopfZahl,
1377
1448
  hermesKopfZahl,
1378
1449
  hermesWarnzahl,
@@ -100,6 +100,7 @@ contextBridge.exposeInMainWorld('hmgr', {
100
100
  mcpUebersicht: () => ipcRenderer.invoke('hmgr:mcpUebersicht'),
101
101
  /** Plätze für den Einhängen-Dialog (F42): kommt aus dem Kern, hängt an der Fähigkeit (E16). */
102
102
  mcpPlaetze: () => ipcRenderer.invoke('hmgr:mcpPlaetze'),
103
+ mcpEbenen: () => ipcRenderer.invoke('hmgr:mcpEbenen'),
103
104
  mcpEinhaengen: (args) => ipcRenderer.invoke('hmgr:mcpEinhaengen', args),
104
105
  mcpAushaengen: (args) => ipcRenderer.invoke('hmgr:mcpAushaengen', args),
105
106
  /** Übernehmen aus der geteilten .mcp.json in den privaten Scope (E64), nach Bestätigung im Renderer. */
@@ -573,7 +573,13 @@
573
573
  <h3>Wohin soll <span id="dm-name">der Server</span>?</h3>
574
574
  <p id="dm-text">Ein Ort je Einhängen. agents kennt keine MCP-Server und steht deshalb nicht zur Wahl.</p>
575
575
  </div>
576
- <div class="dkoerper"><div class="wahl" id="dm-orte"></div></div>
576
+ <div class="dkoerper">
577
+ <div class="suche">
578
+ <svg viewBox="0 0 16 16" stroke-width="1.6" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14.5 14.5"/></svg>
579
+ <input type="search" id="dm-suche" placeholder="Ort oder Pfad suchen" aria-label="Orte durchsuchen" spellcheck="false" autocomplete="off">
580
+ </div>
581
+ <div class="wahl" id="dm-orte"></div>
582
+ </div>
577
583
  <div class="dfuss">
578
584
  <button class="knopf leise" value="abbruch">Abbrechen</button>
579
585
  <button class="knopf signal" value="los" id="dm-los">Einhängen</button>
@@ -339,6 +339,8 @@
339
339
  // Kern (F42, E16): Grundlage für den MCP-Abschnitt und den „MCP hinzufügen …"-Knopf an den
340
340
  // Ziel-Karten — nie am Harness-Namen entschieden, sondern an dieser Liste.
341
341
  var MCP_PLAETZE = [];
342
+ // MCP-Ebenen je Harness (E120), aus der Fähigkeit am Adapter: Grundlage der Tooltip-Erklärung.
343
+ var MCP_EBENEN = [];
342
344
  var quellenFilter = 'alle';
343
345
  // Sortierung der Skills-Rubrik (E79, Richtung E81): hält für die Sitzung wie quellenFilter,
344
346
  // übersteht also ladeAlles. SKILLS selbst bleibt unsortiert nach Datum, damit li.dataset.index
@@ -473,6 +475,7 @@
473
475
  rufe(function () { return window.hmgr.mcpPlaetze(); }),
474
476
  rufe(function () { return window.hmgr.bundles(); }),
475
477
  rufe(function () { return window.hmgr.harnessHeime(); }),
478
+ rufe(function () { return window.hmgr.mcpEbenen(); }),
476
479
  ]);
477
480
  // Alter Stand der Verzeichnis-Pfade, vor dem Überschreiben unten (E84 Teil 1): zielKarten()
478
481
  // liest hier noch ZIELE/AKTIVE_HARNESSE von vor diesem Lauf. null heißt „kein Vergleich", der
@@ -495,6 +498,7 @@
495
498
  BUNDLES.sort(function (a, b) { return a.name.localeCompare(b.name, 'de') || a.quelleText.localeCompare(b.quelleText, 'de'); });
496
499
  BUNDLES_KAPUTT = ergebnisse[9].kaputte;
497
500
  HARNESS_HEIME = ergebnisse[10] || [];
501
+ MCP_EBENEN = ergebnisse[11] || [];
498
502
  STANDARD_REF = {};
499
503
  SKILLS.forEach(function (s) { if (STANDARD_REF[s.quelleText] === undefined) STANDARD_REF[s.quelleText] = s.ref; });
500
504
 
@@ -1112,54 +1116,115 @@
1112
1116
  return zeilen;
1113
1117
  }
1114
1118
 
1115
- /** Der MCP-Abschnitt unter der Skill-Link-Tabelle einer Ziel-Karte (E61-GUI-Nachtrag): nur, wenn
1116
- dort etwas eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). */
1117
- function mcpAbschnitt(ziel) {
1118
- var wurzel = mcpGitWurzel(ziel);
1119
- if (wurzel !== null) {
1120
- var gesperrt = e('div', 'mcpkarteabschnitt');
1121
- gesperrt.appendChild(e('span', 'etikett', 'MCP'));
1122
- gesperrt.appendChild(e('p', 'ohne mcpkarteleer', mcpWurzelNotiz(wurzel)));
1123
- return gesperrt;
1124
- }
1125
- var zeilen = mcpZeilenFuerZiel(ziel);
1126
- if (zeilen.length === 0 && !mcpKannAn(ziel)) return null;
1119
+ /** Quelle-Spalte und Beschreibung einer MCP-Zeile (E120, F142). */
1120
+ function mcpQuelleText(row) {
1121
+ if (row.art === 'katalog') return 'Katalog';
1122
+ if (row.art === 'fremd') return 'nicht im Katalog';
1123
+ return '.mcp.json des Projekts';
1124
+ }
1125
+
1126
+ /** Tooltip an den Harness-Zeichen einer MCP-Zeile unter „Alle" (E120): wo der Server hier hängt und,
1127
+ je weiterem aktivem Harness des Orts, warum nicht. Der Grund kommt aus den MCP-Ebenen der
1128
+ Fähigkeit (MCP_EBENEN, E16), nie aus dem Harness-Namen. Ein Harness, der den Server hier haben
1129
+ könnte (Ebene arbeitsverzeichnis), bekommt keinen Satz: ein Grund wäre geraten (etwa fehlende
1130
+ CLI, E103). */
1131
+ function mcpZeichenTitel(name, hatHarnesse, teile) {
1132
+ var saetze = ['Hier hängt ' + name + ' für ' + hatHarnesse.map(harnessName).join(', ') + '.'];
1133
+ var gesehen = {};
1134
+ teile.forEach(function (t) {
1135
+ var h = t.ziel.harness;
1136
+ if (gesehen[h] || hatHarnesse.indexOf(h) > -1 || !harnessAktiv(h)) return;
1137
+ gesehen[h] = true;
1138
+ var f = MCP_EBENEN.find(function (x) { return x.harness === h; });
1139
+ if (!f) return;
1140
+ if (f.ebenen.indexOf('arbeitsverzeichnis') > -1) return;
1141
+ if (f.ebenen.length === 0) {
1142
+ saetze.push(harnessName(h) + ' kennt keine MCP-Server.');
1143
+ } else {
1144
+ saetze.push(harnessName(h) + ' liest MCP-Server nicht pro Ordner, nur im Home'
1145
+ + (f.ebenen.indexOf('profil') > -1 ? ' oder in einem Profil' : '') + '.');
1146
+ }
1147
+ });
1148
+ return saetze.join(' ');
1149
+ }
1150
+
1151
+ /** Der MCP-Abschnitt einer Ziel-Karte (E61-GUI-Nachtrag, E120): eine Tabelle mit denselben Spalten
1152
+ wie die Skill-Tabelle darüber. `ziele` ist ein Ziel (Harness-Chip) oder mehrere (Chip „Alle",
1153
+ `teile` gesetzt: dann stehen die Harness-Zeichen vor dem Namen). Nur, wenn dort etwas
1154
+ eingehängt ist oder der „hinzufügen"-Knopf ohnehin sichtbar wäre (mcpKannAn). Sperrnotiz (E91)
1155
+ und „Noch nichts eingehängt." bleiben eine Zeile über die Breite. */
1156
+ function mcpAbschnitt(ziele, teile) {
1157
+ var alle = teile !== undefined;
1158
+ if (!Array.isArray(ziele)) ziele = [ziele];
1159
+ var notizen = [];
1160
+ var zeilen = [];
1161
+ var kannAn = false;
1162
+ ziele.forEach(function (ziel) {
1163
+ var wurzel = mcpGitWurzel(ziel);
1164
+ if (wurzel !== null) { notizen.push(mcpWurzelNotiz(wurzel)); return; }
1165
+ if (mcpKannAn(ziel)) kannAn = true;
1166
+ mcpZeilenFuerZiel(ziel).forEach(function (row) { row.ziel = ziel; zeilen.push(row); });
1167
+ });
1168
+ if (zeilen.length === 0 && notizen.length === 0 && !kannAn) return null;
1169
+ zeilen.sort(function (a, b) {
1170
+ return a.name.localeCompare(b.name, 'de') || harnessName(a.ziel.harness).localeCompare(harnessName(b.ziel.harness), 'de');
1171
+ });
1127
1172
  var abschnitt = e('div', 'mcpkarteabschnitt');
1128
- abschnitt.appendChild(e('span', 'etikett', 'MCP'));
1129
1173
  if (zeilen.length === 0) {
1130
- abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
1174
+ abschnitt.appendChild(e('span', 'etikett', 'MCP'));
1175
+ if (notizen.length === 0) abschnitt.appendChild(e('p', 'ohne mcpkarteleer', 'Noch nichts eingehängt.'));
1176
+ notizen.forEach(function (n) { abschnitt.appendChild(e('p', 'ohne mcpkarteleer', n)); });
1131
1177
  return abschnitt;
1132
1178
  }
1133
- var ul = e('ul', 'mcpkartezeilen');
1179
+ var kopf = e('div', 'linkkopf');
1180
+ kopf.appendChild(e('span'));
1181
+ kopf.appendChild(e('span', 'etikett', 'MCP'));
1182
+ kopf.appendChild(e('span', 'etikett', 'Beschreibung'));
1183
+ kopf.appendChild(e('span', 'etikett', 'Quelle'));
1184
+ kopf.appendChild(e('span'));
1185
+ abschnitt.appendChild(kopf);
1186
+ var ul = e('ul', 'linkliste mcpzeilen');
1134
1187
  zeilen.forEach(function (row) {
1188
+ var ziel = row.ziel;
1135
1189
  var li = e('li');
1136
- li.appendChild(e('span', 'name', row.name));
1190
+ li.appendChild(e('span', 'repo'));
1191
+ var nameSpan = e('span', 'name');
1192
+ if (alle) {
1193
+ var hat = [];
1194
+ zeilen.forEach(function (r) {
1195
+ if (r.name === row.name && hat.indexOf(r.ziel.harness) === -1) hat.push(r.ziel.harness);
1196
+ });
1197
+ hat.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
1198
+ var zeichen = harnesseZeichenSammel([ziel.harness]);
1199
+ zeichen.title = mcpZeichenTitel(row.name, hat, teile);
1200
+ nameSpan.appendChild(zeichen);
1201
+ }
1202
+ nameSpan.appendChild(document.createTextNode(row.name));
1203
+ li.appendChild(nameSpan);
1204
+ var wasFeld = e('span', 'wasfeld');
1205
+ if (row.art === 'katalog' || row.art === 'geteilt') wasFeld.appendChild(e('span', 'was', row.beschreibung || ''));
1206
+ else if (row.art === 'fremd-geteilt') wasFeld.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
1207
+ li.appendChild(wasFeld);
1208
+ li.appendChild(e('span', 'quelle', mcpQuelleText(row)));
1137
1209
  if (row.art === 'katalog') {
1138
- li.appendChild(e('span', 'was', row.beschreibung || ''));
1139
1210
  var weg = e('button', 'knopf leise klein', 'Entfernen');
1140
1211
  weg.type = 'button';
1141
1212
  weg.setAttribute('aria-label', row.name + ' aus ' + zielTitel({ ziel: ziel }) + ' aushängen');
1142
1213
  weg.addEventListener('click', function () { frageMcpAus(row.eintrag, ziel); });
1143
1214
  li.appendChild(weg);
1144
1215
  } else if (row.art === 'geteilt' || row.art === 'fremd-geteilt') {
1145
- // E64: der Vermerk nennt die Bedeutung („steht im Projekt, nicht bei dir"), nicht mehr bloß
1146
- // den Dateinamen — und daneben steht der Weg, das zu ändern.
1147
- li.appendChild(row.art === 'geteilt'
1148
- ? e('span', 'was', row.beschreibung || '')
1149
- : e('span', 'was fehlt', 'nicht im Katalog'));
1150
- li.appendChild(e('span', 'geteiltvermerk', 'in der .mcp.json des Projekts'));
1151
1216
  var her = e('button', 'knopf leise klein', 'Übernehmen …');
1152
1217
  her.type = 'button';
1153
1218
  her.setAttribute('aria-label', row.name + ' aus der .mcp.json des Projekts in deine private Konfiguration übernehmen');
1154
1219
  her.addEventListener('click', function () { frageMcpUebernahme(row.name, ziel, row.datei); });
1155
1220
  li.appendChild(her);
1156
1221
  } else {
1157
- li.appendChild(e('span', 'was fehlt', 'nicht im Katalog'));
1158
1222
  li.appendChild(e('span'));
1159
1223
  }
1160
1224
  ul.appendChild(li);
1161
1225
  });
1162
1226
  abschnitt.appendChild(ul);
1227
+ notizen.forEach(function (n) { abschnitt.appendChild(e('p', 'ohne mcpkarteleer', n)); });
1163
1228
  return abschnitt;
1164
1229
  }
1165
1230
 
@@ -1357,10 +1422,8 @@
1357
1422
  if (mcpKnopfLeer) leer.appendChild(mcpKnopfLeer);
1358
1423
  });
1359
1424
  teil.appendChild(leer);
1360
- verzMcpZiele.forEach(function (mz) {
1361
- var mcpAbschnLeer = mcpAbschnitt(mz);
1362
- if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1363
- });
1425
+ var mcpAbschnLeer = mcpAbschnitt(verzMcpZiele, teile);
1426
+ if (mcpAbschnLeer) teil.appendChild(mcpAbschnLeer);
1364
1427
  return teil;
1365
1428
  }
1366
1429
 
@@ -1494,10 +1557,8 @@
1494
1557
 
1495
1558
  // MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
1496
1559
  // der an diesem Pfad MCP kann.
1497
- verzMcpZiele.forEach(function (mz) {
1498
- var mcpAbschnAlle = mcpAbschnitt(mz);
1499
- if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1500
- });
1560
+ var mcpAbschnAlle = mcpAbschnitt(verzMcpZiele, teile);
1561
+ if (mcpAbschnAlle) teil.appendChild(mcpAbschnAlle);
1501
1562
 
1502
1563
  var fuss = e('div', 'zielfuss');
1503
1564
  fuss.appendChild(alleSkillsKnopf(pfad, teile));
@@ -3636,6 +3697,7 @@
3636
3697
  $('dm-text').textContent = 'Ein Ort je Einhängen. agents kennt keine MCP-Server und steht deshalb nicht zur Wahl.';
3637
3698
  var box = $('dm-orte');
3638
3699
  box.textContent = '';
3700
+ $('dm-suche').value = '';
3639
3701
  // Die Plätze kommen aus dem Kern (F42, E16): die Fähigkeit hängt am Adapter, nicht am
3640
3702
  // Harness-Namen, hier wird nicht selbst entschieden, wer welche Ebene kennt.
3641
3703
  var plaetze = await rufe(function () { return window.hmgr.mcpPlaetze(); });
@@ -3650,6 +3712,7 @@
3650
3712
  var schon = k.eingehaengt.some(function (z) { return zielKey(z) === zielKey(ziel); });
3651
3713
  var aktiv = harnessAktiv(ziel.harness);
3652
3714
  var label = e('label', 'wahlzeile');
3715
+ label.dataset.index = i;
3653
3716
  var input = document.createElement('input');
3654
3717
  input.type = 'radio';
3655
3718
  input.name = 'mcport';
@@ -3681,7 +3744,41 @@
3681
3744
  box.appendChild(label);
3682
3745
  });
3683
3746
  zeige($('d-mcp'));
3747
+ $('dm-suche').focus();
3748
+ }
3749
+
3750
+ /** E122: filtert die Wahlzeilen des Einhängen-Dialogs live. Die Zeilen bleiben stehen (der
3751
+ Radio-Wert ist der Index in mcpPlaetzeAuswahl), nicht passende werden nur ausgeblendet. Eine
3752
+ angehakte Zeile, die verschwindet, wird abgehakt: sonst ginge „Einhängen" an einen Ort, den
3753
+ niemand mehr sieht. */
3754
+ function filtereMcpOrte() {
3755
+ var begriff = $('dm-suche').value.trim();
3756
+ var suche = suchform(begriff);
3757
+ var box = $('dm-orte');
3758
+ var gezeigt = 0;
3759
+ box.querySelectorAll('.wahlzeile').forEach(function (label) {
3760
+ var ziel = mcpPlaetzeAuswahl[+label.dataset.index];
3761
+ var passt = true;
3762
+ if (suche) {
3763
+ var pfad = ziel.ort.art === 'arbeitsverzeichnis' ? ziel.ort.pfad : '';
3764
+ var text = suchform(zielTitel({ ziel: ziel }) + ' ' + mcpOrtSpur(ziel) + ' ' + pfad + ' ' + pfadKurz(pfad));
3765
+ passt = text.indexOf(suche) > -1;
3766
+ }
3767
+ label.hidden = !passt;
3768
+ if (passt) gezeigt++;
3769
+ else {
3770
+ var radio = label.querySelector('input');
3771
+ if (radio) radio.checked = false;
3772
+ }
3773
+ });
3774
+ var alt = box.querySelector('.leerhinweis');
3775
+ if (alt) alt.remove();
3776
+ if (suche && gezeigt === 0) {
3777
+ box.appendChild(e('p', 'leerhinweis', 'Kein Ort passt zu „' + begriff + '".'));
3778
+ }
3684
3779
  }
3780
+ $('dm-suche').addEventListener('input', filtereMcpOrte);
3781
+ enterBestaetigt($('dm-suche'), $('dm-los'));
3685
3782
 
3686
3783
  $('d-mcp').addEventListener('submit', function (ev) {
3687
3784
  if (ev.submitter && ev.submitter.value === 'los') {
@@ -771,21 +771,11 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
771
771
  }
772
772
 
773
773
  /* MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht (F46: die einzige Stelle,
774
- die noch zeigt, wo ein Server hängt): eigener Abschnitt unter der Skill-Link-Tabelle, Name,
775
- Beschreibung/Vermerk, Aktion rechtsbündig, flach in der Karte statt in einem eigenen Kasten. */
774
+ die noch zeigt, wo ein Server hängt): eigener Abschnitt unter der Skill-Link-Tabelle, seit E120 mit
775
+ derselben Tabelle (linkkopf/linkliste) wie die Skills; flach in der Karte statt in einem eigenen Kasten. */
776
776
  .mcpkarteabschnitt{margin-top:16px;padding-top:14px;border-top:1px solid var(--strich)}
777
777
  .mcpkarteabschnitt > .etikett{display:block;margin-bottom:10px}
778
778
  .mcpkarteleer{margin:0;color:var(--tinte-leise);font-style:italic}
779
- .mcpkartezeilen{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
780
- .mcpkartezeilen li{display:flex;flex-wrap:wrap;align-items:center;gap:5px 14px}
781
- .mcpkartezeilen .name{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte)}
782
- .mcpkartezeilen .was{font-size:16px;color:var(--tinte-weich)}
783
- .mcpkartezeilen .was.fehlt{color:var(--tinte-leise);font-style:italic}
784
- .mcpkartezeilen .knopf{margin-left:auto}
785
- .mcpkartezeilen .kein-weg{margin-left:auto;font-size:16px;color:var(--tinte-leise);text-align:right}
786
- /* Wo ein Eintrag steht (E64): kein margin-left:auto, sonst teilte er sich den freien Platz mit dem
787
- Übernehmen-Knopf und stünde in der Mitte statt beim Text. */
788
- .mcpkartezeilen .geteiltvermerk{font-size:16px;color:var(--tinte-leise)}
789
779
  /* Bundle-Gruppe an der Ziel-Karte (E69, F68): eine Einheit über ihren Skill-Zeilen, sichtbar
790
780
  zusammengefasst durch eine linke Kante statt durch einen eigenen Kasten — die Zeilen darin sind
791
781
  dieselbe Tabelle wie darunter und sollen mit ihr fluchten. */
@@ -578,6 +578,15 @@ export class Anwendungsfaelle {
578
578
  }
579
579
  return ziele;
580
580
  }
581
+ /**
582
+ * Die MCP-Ebenen je Harness-Typ (E120), aus der Fähigkeit am Adapter, nie am Namen (E16): eine
583
+ * leere Liste heißt, der Harness kennt MCP-Server gar nicht (agents). Grundlage für die Erklärung,
584
+ * warum ein Server an einem Ort für einen Harness fehlt (GUI-Tooltip, Hinweis von `mcp list`).
585
+ * Unabhängig von Installation und Aktivierung; das entscheidet der Aufrufer.
586
+ */
587
+ mcpEbenen() {
588
+ return this.p.harnesses.map((h) => ({ harness: h.name, ebenen: kannMcp(h) ? h.mcp.ebenen() : [] }));
589
+ }
581
590
  /**
582
591
  * Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
583
592
  * `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.52",
3
+ "version": "0.0.54",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",