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.
- package/README.md +10 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +231 -7
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +42 -4
- package/dist/adapter/git.js +34 -8
- package/dist/adapter/hermes.js +73 -1
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +2 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +2 -1
- package/dist/cli/hilfe.js +98 -0
- package/dist/cli/main.js +97 -3
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +361 -19
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +1 -1
- package/dist/gui/preload.cjs +16 -1
- package/dist/gui/renderer/index.html +96 -6
- package/dist/gui/renderer/renderer.js +654 -38
- package/dist/gui/renderer/styles.css +65 -2
- package/dist/kern/anwendungsfaelle.js +175 -4
- package/dist/kern/modell.js +141 -3
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +3 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/package.json +43 -1
|
@@ -91,7 +91,7 @@ a{color:var(--signal-tief)}
|
|
|
91
91
|
|
|
92
92
|
/* ---------- Quellen: Icon, Name, Sprung zur Quelle (E60) ---------- */
|
|
93
93
|
/* Der Name bleibt ruhig, das kleine Pfeilsymbol traegt das Signalorange und sagt: das geht nach
|
|
94
|
-
draussen, zur Repo-Seite oder in den
|
|
94
|
+
draussen, zur Repo-Seite oder in den Dateimanager. Keine Unterstreichung, die flackert in einer Tabelle
|
|
95
95
|
mit vielen Zeilen. */
|
|
96
96
|
.qlink{
|
|
97
97
|
display:inline-flex;align-items:center;gap:5px;
|
|
@@ -192,6 +192,8 @@ a{color:var(--signal-tief)}
|
|
|
192
192
|
.ansichtskopf p{margin-top:10px;color:var(--tinte-weich);font-size:17px}
|
|
193
193
|
|
|
194
194
|
.werkzeuge{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-bottom:18px}
|
|
195
|
+
/* „Verzeichnis hinzufügen" unter der letzten Verzeichnis-Karte (2026-09-05). */
|
|
196
|
+
.zielfussleiste{margin-top:18px}
|
|
195
197
|
.suche{position:relative;flex:1 1 320px;min-width:260px}
|
|
196
198
|
.suche input{
|
|
197
199
|
width:100%;font:inherit;font-size:16px;padding:11px 14px 11px 44px;background:var(--blech);
|
|
@@ -371,6 +373,10 @@ img.hsymbol{border-radius:3px}
|
|
|
371
373
|
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
|
|
372
374
|
}
|
|
373
375
|
.linkliste .was.fehlt{color:var(--tinte-leise);font-style:italic}
|
|
376
|
+
/* Beschreibung plus Frontmatter-Marken (E55) in einer Zelle: die Marken stehen unter der
|
|
377
|
+
geklammerten Beschreibung, nicht in ihr, sonst würde die Zeilen-Klammer sie abschneiden. */
|
|
378
|
+
.linkliste .wasfeld{min-width:0}
|
|
379
|
+
.linkliste .wasfeld .marken{margin-top:5px}
|
|
374
380
|
.linkliste .quelle{display:flex;flex-wrap:wrap;align-items:center;gap:3px 9px;line-height:1.4}
|
|
375
381
|
.linkliste .quelle .ref{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte-leise)}
|
|
376
382
|
/* Der Grund steht nur da, wenn er nicht „von Hand" heisst (der Normalfall entfällt ersatzlos): ein
|
|
@@ -602,7 +608,7 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
|
|
|
602
608
|
.linkliste li{grid-template-columns:20px minmax(0,1fr) auto;gap:3px 12px}
|
|
603
609
|
.linkliste .repo{grid-column:1;grid-row:1}
|
|
604
610
|
.linkliste .name{grid-column:2;grid-row:1}
|
|
605
|
-
.linkliste .
|
|
611
|
+
.linkliste .wasfeld{grid-column:2;grid-row:2}
|
|
606
612
|
.linkliste .quelle{grid-column:2;grid-row:3}
|
|
607
613
|
.linkliste .knopf.klein,.linkliste .kein-weg{grid-column:3;grid-row:1 / span 3;align-self:center}
|
|
608
614
|
.linkliste .knopf.klein{margin-top:0}
|
|
@@ -636,3 +642,60 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
|
|
|
636
642
|
*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}
|
|
637
643
|
.zeile:hover{transform:none}
|
|
638
644
|
}
|
|
645
|
+
|
|
646
|
+
/* ---------- MCP (E61-Folgerunde) ---------- */
|
|
647
|
+
/* Ein Katalog-Eintrag ist keine Ziel-Karte: er ist ein Ding, kein Ort, also traegt er keine
|
|
648
|
+
Emaille-Kopfzeile, sondern steht ganz auf Blech (entwurf/gui.html). Die Reihenfolge im Kasten ist
|
|
649
|
+
die Reihenfolge der Fragen: was ist das, wie verbindet es sich, was muss ich dazu wissen, wo
|
|
650
|
+
haengt es. */
|
|
651
|
+
.mcpliste{display:flex;flex-direction:column;gap:14px}
|
|
652
|
+
.mcp{background:var(--blech);border:1px solid var(--strich);border-radius:3px;padding:16px 18px}
|
|
653
|
+
.mcpkopf{display:flex;flex-wrap:wrap;align-items:baseline;gap:7px 14px;margin-bottom:6px}
|
|
654
|
+
.mcpkopf b{font-family:var(--schrift);font-weight:700;font-size:19px;letter-spacing:-.018em}
|
|
655
|
+
.mcpkopf .knopf{margin-left:auto;align-self:center}
|
|
656
|
+
.mcp .was{font-size:16px;color:var(--tinte-weich)}
|
|
657
|
+
/* Der Konfigblock in einer Zeile: das Kennwort der Verbindung als Glied wie in der Plugin-Kette,
|
|
658
|
+
dahinter die Adresse bzw. das Kommando im Wortlaut. Hier stehen die ${VAR}-Verweise, deshalb
|
|
659
|
+
sitzt die Warnung direkt darunter. */
|
|
660
|
+
.mcpkonfig{margin-top:11px;display:flex;flex-wrap:wrap;align-items:baseline;gap:7px 10px}
|
|
661
|
+
.mcpkonfig .mono{color:var(--tinte-weich);overflow-wrap:anywhere}
|
|
662
|
+
.mcp .ruhezeile{margin-top:10px;margin-bottom:0}
|
|
663
|
+
|
|
664
|
+
/* Notizen sind das Langsame am Eintrag: sie stehen zugeklappt da und oeffnen sich nur, wenn jemand
|
|
665
|
+
sie sucht. Der Wortlaut bleibt, wie er in der mcp.yaml steht, Zeilenumbrueche eingeschlossen. */
|
|
666
|
+
.notizen{margin-top:12px}
|
|
667
|
+
.notizen summary{
|
|
668
|
+
display:inline-flex;align-items:center;gap:8px;width:max-content;
|
|
669
|
+
list-style:none;cursor:pointer;border-radius:2px;
|
|
670
|
+
font-family:var(--schrift);font-size:16px;color:var(--tinte-leise);
|
|
671
|
+
transition:color .12s ease;
|
|
672
|
+
}
|
|
673
|
+
.notizen summary::-webkit-details-marker{display:none}
|
|
674
|
+
.notizen summary:hover{color:var(--tinte)}
|
|
675
|
+
.notizen summary .keil{flex:none;width:11px;height:11px;transition:transform .14s ease}
|
|
676
|
+
.notizen[open] summary .keil{transform:rotate(90deg)}
|
|
677
|
+
.notizen p{
|
|
678
|
+
margin-top:9px;padding-left:13px;border-left:2px solid var(--strich);
|
|
679
|
+
font-size:16px;color:var(--tinte-weich);white-space:pre-line;max-width:62ch;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht (F46: die einzige Stelle,
|
|
683
|
+
die noch zeigt, wo ein Server hängt): eigener Abschnitt unter der Skill-Link-Tabelle, Name,
|
|
684
|
+
Beschreibung/Vermerk, Aktion rechtsbündig, flach in der Karte statt in einem eigenen Kasten. */
|
|
685
|
+
.mcpkarteabschnitt{margin-top:16px;padding-top:14px;border-top:1px solid var(--strich)}
|
|
686
|
+
.mcpkarteabschnitt > .etikett{display:block;margin-bottom:10px}
|
|
687
|
+
.mcpkarteleer{margin:0;color:var(--tinte-leise);font-style:italic}
|
|
688
|
+
.mcpkartezeilen{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
689
|
+
.mcpkartezeilen li{display:flex;flex-wrap:wrap;align-items:center;gap:5px 14px}
|
|
690
|
+
.mcpkartezeilen .name{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte)}
|
|
691
|
+
.mcpkartezeilen .was{font-size:16px;color:var(--tinte-weich)}
|
|
692
|
+
.mcpkartezeilen .was.fehlt{color:var(--tinte-leise);font-style:italic}
|
|
693
|
+
.mcpkartezeilen .knopf{margin-left:auto}
|
|
694
|
+
.mcpkartezeilen .kein-weg{margin-left:auto;font-size:16px;color:var(--tinte-leise);text-align:right}
|
|
695
|
+
/* Wo ein Eintrag steht (E64): kein margin-left:auto, sonst teilte er sich den freien Platz mit dem
|
|
696
|
+
Übernehmen-Knopf und stünde in der Mitte statt beim Text. */
|
|
697
|
+
.mcpkartezeilen .geteiltvermerk{font-size:16px;color:var(--tinte-leise)}
|
|
698
|
+
/* Leerer Katalog: keine Aktion, sondern der Weg zur Datei. */
|
|
699
|
+
.startfeld.mcpstart{display:block}
|
|
700
|
+
.startfeld.mcpstart p{max-width:62ch}
|
|
701
|
+
.startfeld.mcpstart p + p{margin-top:11px}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
3
|
+
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
|
-
import { kannIstRegistriert, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } from "../ports/harness.js";
|
|
5
|
+
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } from "../ports/harness.js";
|
|
6
6
|
/**
|
|
7
7
|
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
8
8
|
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
@@ -24,6 +24,22 @@ function ignorierterEintragGleich(i, args) {
|
|
|
24
24
|
function fehlertext(fehler) {
|
|
25
25
|
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* `${VAR}`- und `${env:VAR}`-Verweise in einem MCP-Konfigblock, ohne Dubletten (E61, F39).
|
|
29
|
+
* Exportiert, damit die GUI dieselbe Prüfung für den Warn-Vermerk an der Katalog-Karte
|
|
30
|
+
* wiederverwenden kann (E61-Folgerunde), statt sie nachzubauen.
|
|
31
|
+
*/
|
|
32
|
+
export function variablenVerweise(konfig) {
|
|
33
|
+
const texte = 'command' in konfig
|
|
34
|
+
? [konfig.command, ...(konfig.args ?? []), ...Object.values(konfig.env ?? {})]
|
|
35
|
+
: [konfig.url, ...Object.values(konfig.headers ?? {})];
|
|
36
|
+
const namen = new Set();
|
|
37
|
+
const muster = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
38
|
+
for (const text of texte)
|
|
39
|
+
for (const treffer of text.matchAll(muster))
|
|
40
|
+
namen.add(treffer[1]);
|
|
41
|
+
return [...namen];
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Quellenhinweis bei Mehrdeutigkeit (E12): Teilstring des Schlüssels oder der Schreibweise. Beides
|
|
29
45
|
* zählt, damit `host/besitzer/repo` genauso greift wie ein Stück der URL, unter der die Quelle
|
|
@@ -174,6 +190,160 @@ export class Anwendungsfaelle {
|
|
|
174
190
|
const harness = await this.harnessGeprueft(ziel.harness);
|
|
175
191
|
await this.harnessMitSkillOrdner(harness.name).skillOrdnerLoeschen(ziel.ort, pfad);
|
|
176
192
|
}
|
|
193
|
+
/** Ein Harness mit der Fähigkeit MCP-Server (E61), sonst `McpFaehigkeitFehlt`. */
|
|
194
|
+
harnessMitMcp(name) {
|
|
195
|
+
const harness = this.harness(name);
|
|
196
|
+
if (!kannMcp(harness))
|
|
197
|
+
throw new McpFaehigkeitFehlt(name);
|
|
198
|
+
return harness;
|
|
199
|
+
}
|
|
200
|
+
/** MCP-Katalog, unverändert wie in ~/.hmgr/mcp.yaml gepflegt (E61). hmgr schreibt ihn nie. */
|
|
201
|
+
async mcpKatalog() {
|
|
202
|
+
return await this.p.mcpKatalog.lesen();
|
|
203
|
+
}
|
|
204
|
+
async mcpEintragFinden(mcpName) {
|
|
205
|
+
const eintrag = (await this.p.mcpKatalog.lesen()).find((e) => e.name === mcpName);
|
|
206
|
+
if (eintrag === undefined)
|
|
207
|
+
throw new McpUnbekannt(mcpName);
|
|
208
|
+
return eintrag;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* F39: fehlende `${VAR}`-Verweise als Warnung, nie als Abbruch. Geprüft wird gegen die eigene
|
|
212
|
+
* Prozess-Umgebung und, wenn die Fähigkeit eine `.env`-Datei nennt (Hermes), zusätzlich gegen sie.
|
|
213
|
+
*/
|
|
214
|
+
async mcpVariablenWarnung(faehig, eintrag, ort) {
|
|
215
|
+
const verweise = variablenVerweise(eintrag.konfig);
|
|
216
|
+
if (verweise.length === 0)
|
|
217
|
+
return undefined;
|
|
218
|
+
const prozessEnv = this.p.umgebung.variablen();
|
|
219
|
+
const envPfad = faehig.mcp.envDatei?.(ort);
|
|
220
|
+
const zusatzEnv = envPfad === undefined ? {} : await this.p.umgebung.envDatei(envPfad);
|
|
221
|
+
const fehlend = verweise.filter((v) => !(v in prozessEnv) && !(v in zusatzEnv));
|
|
222
|
+
if (fehlend.length === 0)
|
|
223
|
+
return undefined;
|
|
224
|
+
const mehrzahl = fehlend.length > 1;
|
|
225
|
+
return (`hmgr kann nicht wissen, was die Shell beim Start des Harness setzt: ` +
|
|
226
|
+
`${mehrzahl ? 'Variablen' : 'Variable'} ${fehlend.join(', ')} ${mehrzahl ? 'sind' : 'ist'} weder in der ` +
|
|
227
|
+
'aktuellen Umgebung noch in einer bekannten .env-Datei zu finden.');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* MCP einhängen (E61): Katalog-Eintrag an ein Ziel legen, Projekt oder global. Die Fähigkeit hängt
|
|
231
|
+
* am Adapter (E16): ein Harness ohne MCP-Fähigkeit oder ohne diese Ebene ist ein Fehler. Ein
|
|
232
|
+
* fehlender `${VAR}`-Verweis bricht nicht ab, sondern kommt als Warnung zurück (F39).
|
|
233
|
+
*/
|
|
234
|
+
async mcpEinhaengen(args) {
|
|
235
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
236
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
237
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
238
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
239
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
240
|
+
await faehig.mcp.einhaengen(eintrag.name, eintrag.konfig, args.ziel.ort);
|
|
241
|
+
const warnung = await this.mcpVariablenWarnung(faehig, eintrag, args.ziel.ort);
|
|
242
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
243
|
+
return {
|
|
244
|
+
eintrag,
|
|
245
|
+
ziel: args.ziel,
|
|
246
|
+
...(hinweis === undefined ? {} : { hinweis }),
|
|
247
|
+
...(warnung === undefined ? {} : { warnung }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** MCP aushängen (E61): Gegenstück zu `mcpEinhaengen`, ohne F39-Prüfung. */
|
|
251
|
+
async mcpAushaengen(args) {
|
|
252
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
253
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
254
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
255
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
256
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
257
|
+
await faehig.mcp.aushaengen(eintrag.name, args.ziel.ort);
|
|
258
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
259
|
+
return { eintrag, ziel: args.ziel, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* MCP übernehmen (E64, F48=O2): einen Eintrag aus der geteilten Konfigurationsdatei eines Ortes
|
|
263
|
+
* (bei Claude Code die `.mcp.json` der Projektwurzel) in den privaten Bereich desselben Ortes
|
|
264
|
+
* holen — eintragen und aus der geteilten Datei entfernen. Der Konfigblock kommt 1:1 aus der
|
|
265
|
+
* Datei, nicht aus dem Katalog: es gibt Einträge, die der Katalog nicht kennt, und beide Sorten
|
|
266
|
+
* müssen gehen. Die Fähigkeit hängt am Adapter (E16), nicht am Namen: fehlt sie, kennt dieser
|
|
267
|
+
* Harness-Typ keine geteilte Datei.
|
|
268
|
+
*/
|
|
269
|
+
async mcpUebernehmen(args) {
|
|
270
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
271
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
272
|
+
const uebernehmen = faehig.mcp.geteiltenUebernehmen;
|
|
273
|
+
if (uebernehmen === undefined)
|
|
274
|
+
throw new McpUebernahmeNichtMoeglich(args.ziel);
|
|
275
|
+
const { datei } = await uebernehmen.call(faehig.mcp, args.name, args.ziel.ort);
|
|
276
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
277
|
+
return { name: args.name, ziel: args.ziel, datei, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Alle Orte, an denen MCP-Server grundsätzlich hängen können (E61): je Harness mit der Fähigkeit
|
|
281
|
+
* (E16, am Adapter abgefragt, nie am Namen) seine Ebenen aufgelöst zu tatsächlichen Zielen — Home,
|
|
282
|
+
* jedes bekannte Profil, jedes bekannte Arbeitsverzeichnis. Unabhängig davon, ob dort schon etwas
|
|
283
|
+
* hängt oder der Harness gerade aktiv ist (E38): das entscheidet der Aufrufer. Geteilt zwischen
|
|
284
|
+
* `mcpUebersicht` (dort mit `eingehaengte` abgeglichen) und `mcpPlaetze` (roh für den
|
|
285
|
+
* Einhängen-Dialog der GUI).
|
|
286
|
+
*/
|
|
287
|
+
async mcpZiele() {
|
|
288
|
+
const ziele = [];
|
|
289
|
+
const zustand = await this.p.zustand.lesen();
|
|
290
|
+
for (const harness of await this.vorhandeneHarnesses()) {
|
|
291
|
+
if (!kannMcp(harness))
|
|
292
|
+
continue;
|
|
293
|
+
const ebenen = harness.mcp.ebenen();
|
|
294
|
+
if (ebenen.includes('home'))
|
|
295
|
+
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
296
|
+
if (ebenen.includes('profil') && kannProfile(harness))
|
|
297
|
+
for (const name of await harness.profile())
|
|
298
|
+
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
299
|
+
if (ebenen.includes('arbeitsverzeichnis'))
|
|
300
|
+
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
301
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
302
|
+
}
|
|
303
|
+
return ziele;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
|
|
307
|
+
* `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
|
|
308
|
+
* Adapter, nicht am Harness-Namen (E16): agents kommt nie vor, weil es `mcp` nicht implementiert.
|
|
309
|
+
*/
|
|
310
|
+
async mcpPlaetze() {
|
|
311
|
+
return this.mcpZiele();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Übersicht für `hmgr mcp list` (E61): je Katalog-Eintrag, wo er eingehängt ist, dazu Einträge in
|
|
315
|
+
* Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt, nicht angefasst).
|
|
316
|
+
*/
|
|
317
|
+
async mcpUebersicht() {
|
|
318
|
+
const katalog = (await this.p.mcpKatalog.lesen()).map((eintrag) => ({
|
|
319
|
+
eintrag,
|
|
320
|
+
eingehaengt: [],
|
|
321
|
+
geteilteFundorte: [],
|
|
322
|
+
}));
|
|
323
|
+
const nachName = new Map(katalog.map((k) => [k.eintrag.name, k]));
|
|
324
|
+
const unbekannt = [];
|
|
325
|
+
const unbekannteGeteilteFundorte = [];
|
|
326
|
+
for (const ziel of await this.mcpZiele()) {
|
|
327
|
+
const faehig = this.harnessMitMcp(ziel.harness);
|
|
328
|
+
const namen = await faehig.mcp.eingehaengte(ziel.ort).catch(() => []);
|
|
329
|
+
for (const mcpName of namen) {
|
|
330
|
+
const eintrag = nachName.get(mcpName);
|
|
331
|
+
if (eintrag !== undefined)
|
|
332
|
+
eintrag.eingehaengt.push(ziel);
|
|
333
|
+
else
|
|
334
|
+
unbekannt.push({ ziel, name: mcpName });
|
|
335
|
+
}
|
|
336
|
+
const geteilt = (await faehig.mcp.geteilteFundorte?.(ziel.ort).catch(() => [])) ?? [];
|
|
337
|
+
for (const fundort of geteilt) {
|
|
338
|
+
const eintrag = nachName.get(fundort.name);
|
|
339
|
+
if (eintrag !== undefined)
|
|
340
|
+
eintrag.geteilteFundorte.push({ ziel, datei: fundort.datei, scope: fundort.scope });
|
|
341
|
+
else
|
|
342
|
+
unbekannteGeteilteFundorte.push({ ziel, name: fundort.name, datei: fundort.datei, scope: fundort.scope });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { katalog, unbekannt, unbekannteGeteilteFundorte };
|
|
346
|
+
}
|
|
177
347
|
/**
|
|
178
348
|
* Säubern (E38): entfernt in allen Zielen dieses Harness-Typs (Home, Profile, bekannte
|
|
179
349
|
* Arbeitsverzeichnisse) die von hmgr angelegten Links und baut harness-eigene Einträge zurück, die
|
|
@@ -1171,8 +1341,9 @@ export class Anwendungsfaelle {
|
|
|
1171
1341
|
if (!enthalten)
|
|
1172
1342
|
return unbekannt('lokale-aenderungen');
|
|
1173
1343
|
const quelle = normalisiert(quelleRoh);
|
|
1174
|
-
// Der
|
|
1175
|
-
//
|
|
1344
|
+
// Der Katalog (skills()) kennt einen Wurzel-Skill zwar seit dem E11-Nachtrag, aber er gilt
|
|
1345
|
+
// für die geführte Quelle am gefragten Ref. Hier liegt eine fremde Arbeitskopie an Ort und
|
|
1346
|
+
// Stelle: ihr Wurzel-Skill wird direkt von der Platte gelesen, ohne Clone (E48 a).
|
|
1176
1347
|
const skill = await this.p.quellen.wurzelSkill(entryPfad).catch(() => null);
|
|
1177
1348
|
if (skill === null)
|
|
1178
1349
|
return unbekannt('skill-unbekannt');
|
package/dist/kern/modell.js
CHANGED
|
@@ -63,10 +63,15 @@ export function quelleSchluessel(quelle) {
|
|
|
63
63
|
const ort = urlOrt(quelle.url);
|
|
64
64
|
return ort === undefined ? quelle.url : ortSchluessel(ort);
|
|
65
65
|
}
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche.
|
|
68
|
+
* Trenner sind `/` und `\` (E65/B7-Nachbar B8): unter Windows kommt der Pfad als `C:\work\quelle`
|
|
69
|
+
* herein und ergab sonst den Namen „\work\quelle". Ein POSIX-Ordnername mit Backslash wird dadurch
|
|
70
|
+
* zerteilt — akzeptiert, das hier ist reine Anzeige-Ableitung und geht in keinen Schlüssel ein.
|
|
71
|
+
*/
|
|
67
72
|
function letztesSegment(text) {
|
|
68
|
-
const bereinigt = text.replace(
|
|
69
|
-
const segmente = bereinigt.split(
|
|
73
|
+
const bereinigt = text.replace(/[/\\]+$/, '');
|
|
74
|
+
const segmente = bereinigt.split(/[/\\]/).filter((s) => s.length > 0);
|
|
70
75
|
return segmente[segmente.length - 1] ?? bereinigt;
|
|
71
76
|
}
|
|
72
77
|
/**
|
|
@@ -130,6 +135,25 @@ export class NameBelegt extends Error {
|
|
|
130
135
|
this.skillName = skillName;
|
|
131
136
|
}
|
|
132
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Der Link am Ziel ließ sich im Dateisystem nicht anlegen (E66). Ohne diesen Fehler käme ein roher
|
|
140
|
+
* Node-Stack beim Nutzer an, der weder Pfad noch Ausweg nennt. Der Handlungshinweis steht mit in
|
|
141
|
+
* der Meldung statt in `naechsterSchritt` der CLI: er nennt kein hmgr-Kommando, sondern eine
|
|
142
|
+
* Einstellung des Betriebssystems, und die GUI zeigt nur die Meldung.
|
|
143
|
+
*/
|
|
144
|
+
export class LinkAnlegenFehlgeschlagen extends Error {
|
|
145
|
+
linkPfad;
|
|
146
|
+
zielPfad;
|
|
147
|
+
ursache;
|
|
148
|
+
hinweis;
|
|
149
|
+
constructor(linkPfad, zielPfad, ursache, hinweis) {
|
|
150
|
+
super(`Der Link „${linkPfad}" auf „${zielPfad}" ließ sich nicht anlegen: ${ursache}\n${hinweis}`);
|
|
151
|
+
this.linkPfad = linkPfad;
|
|
152
|
+
this.zielPfad = zielPfad;
|
|
153
|
+
this.ursache = ursache;
|
|
154
|
+
this.hinweis = hinweis;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
133
157
|
export class SkillMehrdeutig extends Error {
|
|
134
158
|
name;
|
|
135
159
|
quellen;
|
|
@@ -318,3 +342,117 @@ export class SkillOrdnerUngueltig extends Error {
|
|
|
318
342
|
this.grund = grund;
|
|
319
343
|
}
|
|
320
344
|
}
|
|
345
|
+
/** Ein Name, der im MCP-Katalog nicht steht (E61): mcp add und mcp remove kennen nur Katalog-Einträge. */
|
|
346
|
+
export class McpUnbekannt extends Error {
|
|
347
|
+
mcpName;
|
|
348
|
+
constructor(mcpName) {
|
|
349
|
+
super(`MCP „${mcpName}" steht nicht im Katalog`);
|
|
350
|
+
this.mcpName = mcpName;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Ein Harness-Typ ohne die Fähigkeit MCP-Server (E61): heute nur claude-code und hermes. */
|
|
354
|
+
export class McpFaehigkeitFehlt extends Error {
|
|
355
|
+
harness;
|
|
356
|
+
constructor(harness) {
|
|
357
|
+
super(`Harness ${harness} kennt keine MCP-Server`);
|
|
358
|
+
this.harness = harness;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Ein Ort, an dem dieser Harness-Typ keine MCP-Server verwaltet (E61): die Fähigkeit hängt am
|
|
363
|
+
* Adapter, nicht am Harness-Namen (E16), aber Hermes kennt grundsätzlich keine Projektebene für
|
|
364
|
+
* MCPs, das bekommt eine eigene Erklärung.
|
|
365
|
+
*/
|
|
366
|
+
export class McpEbeneNichtUnterstuetzt extends Error {
|
|
367
|
+
harness;
|
|
368
|
+
ort;
|
|
369
|
+
constructor(harness, ort) {
|
|
370
|
+
const erklaerung = harness === 'hermes' && ort === 'arbeitsverzeichnis' ? ' – Hermes kennt keine Projektebene für MCPs' : '';
|
|
371
|
+
super(`${harness} kennt für MCP-Server den Ort „${ort}" nicht${erklaerung}`);
|
|
372
|
+
this.harness = harness;
|
|
373
|
+
this.ort = ort;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Am Ziel liegt unter diesem Namen schon ein MCP-Server, den hmgr nicht selbst gelegt hat oder der
|
|
378
|
+
* vom Katalogblock abweicht (E61). hmgr überschreibt so etwas nicht stillschweigend.
|
|
379
|
+
*/
|
|
380
|
+
export class McpAbweichenderEintrag extends Error {
|
|
381
|
+
ziel;
|
|
382
|
+
mcpName;
|
|
383
|
+
constructor(ziel, mcpName) {
|
|
384
|
+
super(`In ${zielSchluessel(ziel)} liegt schon ein MCP-Server „${mcpName}", der nicht von hmgr stammt oder vom ` +
|
|
385
|
+
'Katalogblock abweicht. hmgr überschreibt ihn nicht stillschweigend.');
|
|
386
|
+
this.ziel = ziel;
|
|
387
|
+
this.mcpName = mcpName;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* `aushaengen` trifft einen Namen, der nicht im schreibbaren Bereich liegt, aber in einem geteilten
|
|
392
|
+
* Fundort steht (E61-Nachtrag), etwa der `.mcp.json` einer Projektwurzel bei Claude Code. Anders als
|
|
393
|
+
* der reguläre idempotente Fall (nirgends eingehängt) bleibt hier tatsächlich ein Eintrag liegen:
|
|
394
|
+
* die Meldung darf das nicht als Erfolg verkaufen. Seit E64 gibt es dafür einen Weg in hmgr selbst,
|
|
395
|
+
* die Meldung nennt ihn statt „von Hand ändern".
|
|
396
|
+
*/
|
|
397
|
+
export class McpNurGeteilterFundort extends Error {
|
|
398
|
+
ziel;
|
|
399
|
+
mcpName;
|
|
400
|
+
datei;
|
|
401
|
+
constructor(ziel, mcpName, datei) {
|
|
402
|
+
super(`„${mcpName}" steht in der geteilten ${datei}, nicht in deiner privaten Konfiguration. `
|
|
403
|
+
+ `hmgr mcp migrate ${mcpName} übernimmt ihn in den privaten Scope und entfernt ihn aus der Datei.`);
|
|
404
|
+
this.ziel = ziel;
|
|
405
|
+
this.mcpName = mcpName;
|
|
406
|
+
this.datei = datei;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Übernehmen (E64) an einem Ort, der keine geteilte Konfigurationsdatei kennt: ein Harness ohne
|
|
411
|
+
* diese Fähigkeit (Hermes kennt keine geteilte Datei) oder eine Ebene ohne sie (bei Claude Code hat
|
|
412
|
+
* nur ein Arbeitsverzeichnis eine `.mcp.json`, das Home nicht).
|
|
413
|
+
*/
|
|
414
|
+
export class McpUebernahmeNichtMoeglich extends Error {
|
|
415
|
+
ziel;
|
|
416
|
+
constructor(ziel) {
|
|
417
|
+
super(`In ${zielSchluessel(ziel)} gibt es keine geteilte Konfigurationsdatei, aus der hmgr einen MCP-Server übernehmen könnte.`);
|
|
418
|
+
this.ziel = ziel;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Übernehmen (E64) eines Namens, der in der geteilten Datei gar nicht steht. */
|
|
422
|
+
export class McpNichtInGeteilterDatei extends Error {
|
|
423
|
+
ziel;
|
|
424
|
+
mcpName;
|
|
425
|
+
datei;
|
|
426
|
+
constructor(ziel, mcpName, datei) {
|
|
427
|
+
super(`„${mcpName}" steht nicht in ${datei}, da ist nichts zu übernehmen.`);
|
|
428
|
+
this.ziel = ziel;
|
|
429
|
+
this.mcpName = mcpName;
|
|
430
|
+
this.datei = datei;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Teilerfolg beim Übernehmen (E64): der Eintrag steht schon im privaten Scope, ließ sich aber nicht
|
|
435
|
+
* aus der geteilten Datei entfernen. Die Reihenfolge ist bewusst so gewählt (erst eintragen, dann
|
|
436
|
+
* entfernen), damit in diesem Fall nichts verloren ist. Die Meldung sagt, was der Nutzer jetzt
|
|
437
|
+
* vorfindet, statt nur den Fehlschlag zu nennen.
|
|
438
|
+
*/
|
|
439
|
+
export class McpUebernahmeUnvollstaendig extends Error {
|
|
440
|
+
ziel;
|
|
441
|
+
mcpName;
|
|
442
|
+
datei;
|
|
443
|
+
constructor(ziel, mcpName, datei, grund) {
|
|
444
|
+
super(`„${mcpName}" steht jetzt in deiner privaten Konfiguration, ließ sich aber nicht aus ${datei} entfernen: ${grund}. `
|
|
445
|
+
+ 'Der Eintrag steht damit doppelt; wirksam ist der private. Entferne ihn aus der Datei von Hand, dann ist die Übernahme fertig.');
|
|
446
|
+
this.ziel = ziel;
|
|
447
|
+
this.mcpName = mcpName;
|
|
448
|
+
this.datei = datei;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/** Der MCP-Katalog ~/.hmgr/mcp.yaml lässt sich nicht lesen (E61): kaputtes YAML oder falsches Format. */
|
|
452
|
+
export class McpKatalogFehler extends Error {
|
|
453
|
+
pfad;
|
|
454
|
+
constructor(pfad, grund) {
|
|
455
|
+
super(`MCP-Katalog ${pfad}: ${grund}`);
|
|
456
|
+
this.pfad = pfad;
|
|
457
|
+
}
|
|
458
|
+
}
|
package/dist/kern/quelle-ort.js
CHANGED
|
@@ -12,9 +12,20 @@ export function ortAusSegmenten(host, segmente) {
|
|
|
12
12
|
const besitzer = gefiltert[gefiltert.length - 2] ?? '_';
|
|
13
13
|
return { host: host.toLowerCase(), besitzer, repo };
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Ein Windows-Laufwerkspfad (E65/B12): ein einzelner ASCII-Buchstabe, ein Doppelpunkt und dann ein
|
|
17
|
+
* Trenner — `C:\work\quelle` oder `C:/work/quelle`. Genau diese Form sieht sonst aus wie eine
|
|
18
|
+
* scp-Kurzform `host:pfad` und wurde als Host „C" mit dem Pfad „\work\quelle" gelesen. Ein Hostname
|
|
19
|
+
* aus einem einzelnen Buchstaben ist theoretisch erlaubt, kommt als Git-Remote aber nicht vor; der
|
|
20
|
+
* Windows-Pfad kommt vor.
|
|
21
|
+
*/
|
|
22
|
+
export function istWindowsLaufwerkspfad(text) {
|
|
23
|
+
return /^[A-Za-z]:[\\/]/.test(text.trim());
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* Zerlegt eine Git-URL: `git@host:besitzer/repo.git`, `ssh://git@host/besitzer/repo`,
|
|
17
|
-
* `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined
|
|
27
|
+
* `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined —
|
|
28
|
+
* darunter jeder nackte Pfad, der lokale POSIX-Pfad ebenso wie der Windows-Laufwerkspfad (E65/B12).
|
|
18
29
|
*/
|
|
19
30
|
export function urlOrt(url) {
|
|
20
31
|
const text = url.trim().replace(/\/+$/, '');
|
|
@@ -32,6 +43,8 @@ export function urlOrt(url) {
|
|
|
32
43
|
return ortAusSegmenten('local', segmente);
|
|
33
44
|
return ortAusSegmenten(zerlegt.hostname, segmente);
|
|
34
45
|
}
|
|
46
|
+
if (istWindowsLaufwerkspfad(text))
|
|
47
|
+
return undefined;
|
|
35
48
|
const scp = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(text);
|
|
36
49
|
if (scp)
|
|
37
50
|
return ortAusSegmenten(scp[1], scp[2].split('/'));
|
package/dist/ports/harness.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hmgr",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tim Kaufmann",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"main": "dist/gui/main.js",
|
|
8
9
|
"bin": {
|
|
9
10
|
"hmgr": "dist/cli/main.js"
|
|
10
11
|
},
|
|
@@ -21,22 +22,63 @@
|
|
|
21
22
|
"build": "tsc -p tsconfig.build.json && node scripts/copy-gui-assets.mjs",
|
|
22
23
|
"gui": "npm run build && electron dist/gui/main.js",
|
|
23
24
|
"gui:smoke": "npm run build && node scripts/gui-smoke-test.ts",
|
|
25
|
+
"gui:app": "npm run build && electron-builder --mac --arm64",
|
|
26
|
+
"gui:app:win": "npm run build && electron-builder --win --x64",
|
|
27
|
+
"gui:install": "npm run build && electron-builder --mac dir --arm64 && ditto release/mac-arm64/hmgr.app /Applications/hmgr.app",
|
|
24
28
|
"test": "vitest run",
|
|
25
29
|
"test:watch": "vitest",
|
|
26
30
|
"type-check": "tsc --noEmit",
|
|
27
31
|
"prepack": "npm run build"
|
|
28
32
|
},
|
|
29
33
|
"dependencies": {
|
|
34
|
+
"cross-spawn": "^7.0.6",
|
|
30
35
|
"yaml": "^2.8.0"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
38
|
+
"@types/cross-spawn": "^6.0.6",
|
|
33
39
|
"@types/node": "^24.0.0",
|
|
34
40
|
"electron": "^44.0.0",
|
|
41
|
+
"electron-builder": "^26.15.3",
|
|
35
42
|
"typescript": "^5.9.0",
|
|
36
43
|
"vitest": "^3.2.0"
|
|
37
44
|
},
|
|
38
45
|
"repository": {
|
|
39
46
|
"type": "git",
|
|
40
47
|
"url": "git+https://gitea.timkaufmann.de/tim/hmgr.git"
|
|
48
|
+
},
|
|
49
|
+
"build": {
|
|
50
|
+
"appId": "de.timkaufmann.hmgr",
|
|
51
|
+
"productName": "hmgr",
|
|
52
|
+
"directories": {
|
|
53
|
+
"output": "release",
|
|
54
|
+
"buildResources": "build"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist/**/*",
|
|
58
|
+
"package.json"
|
|
59
|
+
],
|
|
60
|
+
"asar": true,
|
|
61
|
+
"mac": {
|
|
62
|
+
"category": "public.app-category.developer-tools",
|
|
63
|
+
"icon": "build/icon.icns",
|
|
64
|
+
"target": [
|
|
65
|
+
{ "target": "dir", "arch": ["arm64"] },
|
|
66
|
+
{ "target": "zip", "arch": ["arm64"] }
|
|
67
|
+
],
|
|
68
|
+
"identity": null
|
|
69
|
+
},
|
|
70
|
+
"win": {
|
|
71
|
+
"icon": "build/icon.ico",
|
|
72
|
+
"target": [
|
|
73
|
+
{ "target": "nsis", "arch": ["x64"] }
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"nsis": {
|
|
77
|
+
"oneClick": false,
|
|
78
|
+
"perMachine": false,
|
|
79
|
+
"allowElevation": false,
|
|
80
|
+
"allowToChangeInstallationDirectory": true,
|
|
81
|
+
"artifactName": "${productName}-Setup-${version}-${arch}.${ext}"
|
|
82
|
+
}
|
|
41
83
|
}
|
|
42
84
|
}
|