hmgr 0.0.1 → 0.0.2

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 CHANGED
@@ -46,7 +46,7 @@ status Ist-Zustand der Ziele
46
46
  Optionen:
47
47
 
48
48
  ```
49
- --harness <name> claude-code oder hermes, Standard claude-code
49
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
50
50
  --ort home|dir:<pfad>|profil:<name> Standard home
51
51
  --quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
52
52
  --version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger
@@ -0,0 +1,43 @@
1
+ // Das universelle Skill-Verzeichnis `.agents/skills`: ein Harness-Typ ohne Programm dahinter (E35).
2
+ // Home ist `~/.agents/skills`, das Arbeitsverzeichnis `<pfad>/.agents/skills`.
3
+ import { mkdir, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { OrtNichtUnterstuetzt } from "../kern/modell.js";
6
+ import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
7
+ const AGENTS_VERZEICHNIS = '.agents';
8
+ const SKILLS_ORDNER = 'skills';
9
+ const GITIGNORE_ZEILE = '.agents/';
10
+ export class Agents {
11
+ name = 'agents';
12
+ home;
13
+ /** `home` ist das HOME des Nutzers, das Verzeichnis liegt darunter als `.agents`. */
14
+ constructor(home) {
15
+ this.home = home;
16
+ }
17
+ /**
18
+ * Wie bei den anderen Harness-Typen zählt das angelegte Verzeichnis. Legt es niemand an, gibt es
19
+ * auf diesem Rechner keinen Konsumenten dafür und `status` überspringt das Ziel.
20
+ */
21
+ async vorhanden() {
22
+ return (await stat(path.join(this.home, AGENTS_VERZEICHNIS)).catch(() => undefined)) !== undefined;
23
+ }
24
+ orte() {
25
+ return ['home', 'arbeitsverzeichnis'];
26
+ }
27
+ zielVerzeichnis(ort) {
28
+ if (ort.art === 'home')
29
+ return path.join(this.home, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
30
+ if (ort.art === 'arbeitsverzeichnis')
31
+ return path.join(ort.pfad, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
32
+ throw new OrtNichtUnterstuetzt(this.name, ort.art);
33
+ }
34
+ async vorbereiten(ort) {
35
+ await mkdir(this.zielVerzeichnis(ort), { recursive: true });
36
+ // .agents/ darf nicht ins Repo (B7).
37
+ if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
38
+ await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
39
+ }
40
+ folgtLinks() {
41
+ return true;
42
+ }
43
+ }
@@ -120,8 +120,9 @@ class ClaudePlugins {
120
120
  const { scope, cwd } = this.scope(ziel);
121
121
  await this.rufen(['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
122
122
  }
123
+ /** Ohne Namen aktualisiert die CLI alle Marketplaces. Je Plugin verlangt sie einen Namen, das übernimmt hmgr nicht (E14). */
123
124
  async aktualisieren() {
124
- await this.rufen(['plugin', 'update']);
125
+ await this.rufen(['plugin', 'marketplace', 'update']);
125
126
  }
126
127
  /**
127
128
  * Die Liste gilt nur für das cwd des Aufrufs und enthält dort alle Scopes. Zum Ort gehören im
@@ -1,6 +1,6 @@
1
1
  // Hermes Agent: Grundverhalten plus Profile entdecken und Verzeichnis in der config.yaml registrieren.
2
2
  // Skills landen nie in <hermes-home>/skills, sondern in einem Ordner aus skills.external_dirs (E13).
3
- import { readFileSync } from 'node:fs';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { Document, isSeq, parseDocument } from 'yaml';
@@ -29,6 +29,23 @@ function externalDirs(daten) {
29
29
  return [];
30
30
  return dirs.filter((d) => typeof d === 'string');
31
31
  }
32
+ /**
33
+ * Liest skills.trusted_project_dirs aus einer geparsten config.yaml. Hermes nimmt dort auch einen
34
+ * einzelnen String, der zählt wie eine Liste mit einem Eintrag.
35
+ */
36
+ function trustedProjectDirs(daten) {
37
+ if (typeof daten !== 'object' || daten === null)
38
+ return [];
39
+ const skills = daten['skills'];
40
+ if (typeof skills !== 'object' || skills === null)
41
+ return [];
42
+ const dirs = skills['trusted_project_dirs'];
43
+ if (typeof dirs === 'string')
44
+ return [dirs];
45
+ if (!Array.isArray(dirs))
46
+ return [];
47
+ return dirs.filter((d) => typeof d === 'string');
48
+ }
32
49
  export class Hermes {
33
50
  name = 'hermes';
34
51
  home;
@@ -125,6 +142,69 @@ export class Hermes {
125
142
  folgtLinks() {
126
143
  return true;
127
144
  }
145
+ /** Vergleich wie bei Hermes über den aufgelösten Pfad, Symlinks eingeschlossen. */
146
+ echterPfad(pfad) {
147
+ try {
148
+ return realpathSync(path.resolve(pfad));
149
+ }
150
+ catch {
151
+ return path.resolve(pfad);
152
+ }
153
+ }
154
+ /** Steht die Wurzel schon in `skills.trusted_project_dirs` der übergebenen config.yaml. */
155
+ vertraut(daten, hermesHome, wurzel) {
156
+ for (const eintrag of trustedProjectDirs(daten)) {
157
+ const pfad = this.aufloesen(eintrag, hermesHome);
158
+ if (pfad !== undefined && this.echterPfad(pfad) === wurzel)
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ /**
164
+ * Fähigkeit Hinweis nach dem Link (E34): Hermes lädt `<wurzel>/.hermes/skills` nur, wenn die
165
+ * Wurzel in `skills.trusted_project_dirs` der config.yaml steht. hmgr trägt sie nicht von sich
166
+ * aus ein, sondern bietet es als Abhilfe an, entscheiden soll der Nutzer. Maßgeblich ist das
167
+ * Hermes-Home, ein Profil kennt der Ort Arbeitsverzeichnis nicht. Relative Einträge löst Hermes
168
+ * gegen das cwd auf, hmgr wie überall gegen das Hermes-Home, in der Praxis stehen dort absolute
169
+ * Pfade.
170
+ */
171
+ async hinweisNachLink(ort) {
172
+ if (ort.art !== 'arbeitsverzeichnis')
173
+ return undefined;
174
+ const hermesHome = path.join(this.home, HERMES_VERZEICHNIS);
175
+ const wurzel = ort.pfad;
176
+ if (this.vertraut(this.gelesen(hermesHome), hermesHome, this.echterPfad(wurzel)))
177
+ return undefined;
178
+ return {
179
+ hinweis: `Hermes lädt Projekt-Skills in ${wurzel} erst nach \`hermes skills trust ${wurzel}\`.`,
180
+ abhilfe: {
181
+ frage: `Hermes lädt Projekt-Skills in ${wurzel} erst nach Vertrauen. Jetzt als vertrauenswürdig eintragen? [j/N]`,
182
+ erledigt: `${wurzel} als vertrauenswürdig in Hermes eingetragen.`,
183
+ ausfuehren: async () => await this.vertrauen(wurzel),
184
+ },
185
+ };
186
+ }
187
+ /**
188
+ * Trägt die Wurzel in `skills.trusted_project_dirs` der config.yaml des Hermes-Home ein, falls
189
+ * sie fehlt. Geschrieben wird der aufgelöste absolute Pfad, so vergleicht Hermes selbst
190
+ * (HARNESS-FAKTEN). Kommentare bleiben erhalten, ein einzelner String wird zur Liste.
191
+ */
192
+ async vertrauen(wurzel) {
193
+ const hermesHome = path.join(this.home, HERMES_VERZEICHNIS);
194
+ const pfad = path.join(hermesHome, CONFIG);
195
+ const text = await readFile(pfad, 'utf8').catch(() => undefined);
196
+ const doc = text === undefined ? new Document({ skills: { trusted_project_dirs: [] } }) : parseDocument(text);
197
+ const eintrag = this.echterPfad(wurzel);
198
+ if (this.vertraut(doc.toJS(), hermesHome, eintrag))
199
+ return;
200
+ const liste = doc.getIn(['skills', 'trusted_project_dirs']);
201
+ if (isSeq(liste))
202
+ liste.add(eintrag);
203
+ else
204
+ doc.setIn(['skills', 'trusted_project_dirs'], [...trustedProjectDirs(doc.toJS()), eintrag]);
205
+ await mkdir(hermesHome, { recursive: true });
206
+ await writeFile(pfad, doc.toString());
207
+ }
128
208
  /** Fähigkeit Profile entdecken: Verzeichnisse mit config.yaml, bei jedem Aufruf frisch gelesen (E19). */
129
209
  async profile() {
130
210
  const basis = path.join(this.home, HERMES_VERZEICHNIS, PROFILE_VERZEICHNIS);
@@ -0,0 +1,24 @@
1
+ // Rückfrage auf dem Terminal. Bleibt dünn: eine Weiche, eine gelesene Zeile, kein eigenes Urteil.
2
+ import { createInterface } from 'node:readline/promises';
3
+ /** Antworten, die als Ja zählen. Alles andere, auch die leere Eingabe, ist Nein. */
4
+ const JA = new Set(['j', 'ja']);
5
+ export class TerminalRueckfrage {
6
+ /**
7
+ * Ohne Terminal auf beiden Seiten gibt es niemanden, der antworten könnte: hmgr läuft dann in
8
+ * einem Composer-Script, einer Pipe oder auf einem Deployment-Rechner (E20). Eine Frage ginge
9
+ * dort ins Leere und das Lesen von stdin bliebe hängen oder liefe sofort auf das Dateiende. Also
10
+ * gilt Nein, ohne Ausgabe: der Aufrufer bekommt seinen Hinweis und niemand wartet.
11
+ */
12
+ async bestaetigen(frage) {
13
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
14
+ return false;
15
+ const leser = createInterface({ input: process.stdin, output: process.stdout });
16
+ try {
17
+ const antwort = await leser.question(`${frage} `);
18
+ return JA.has(antwort.trim().toLowerCase());
19
+ }
20
+ finally {
21
+ leser.close();
22
+ }
23
+ }
24
+ }
package/dist/cli/main.js CHANGED
@@ -6,12 +6,14 @@ import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
8
8
  import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, pluginSchluessel, } from "../kern/modell.js";
9
+ import { Agents } from "../adapter/agents.js";
9
10
  import { ClaudeCode } from "../adapter/claude-code.js";
10
11
  import { Hermes } from "../adapter/hermes.js";
11
12
  import { ComposerZeiger } from "../adapter/composer.js";
12
13
  import { DateisystemZiele } from "../adapter/dateisystem.js";
13
14
  import { GitQuellen } from "../adapter/git.js";
14
15
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
16
+ import { TerminalRueckfrage } from "../adapter/terminal.js";
15
17
  import { ZustandJson } from "../adapter/zustand-json.js";
16
18
  export function umgebungLesen(env = process.env) {
17
19
  const home = env.HOME ?? homedir();
@@ -24,11 +26,12 @@ export function bauen(umgebung, optionen = {}) {
24
26
  ? new ClaudeCode(umgebung.home)
25
27
  : new ClaudeCode(umgebung.home, optionen.claudeAusfuehrer);
26
28
  const ports = {
27
- harnesses: [claude, new Hermes(umgebung.home)],
29
+ harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
28
30
  quellen: new GitQuellen(wurzel),
29
31
  ziele: new DateisystemZiele(wurzel, zustand),
30
32
  zeiger: new ComposerZeiger(),
31
33
  zustand,
34
+ rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
32
35
  };
33
36
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
34
37
  }
@@ -83,7 +86,7 @@ const HILFE = `hmgr <kommando>
83
86
  update Alle Checkouts aktualisieren
84
87
  status Ist-Zustand der Ziele
85
88
 
86
- --harness <name> claude-code oder hermes, Standard claude-code
89
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
87
90
  --ort home|dir:<pfad>|profil:<name> Standard home
88
91
  --quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
89
92
  --version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger (E33)
@@ -292,8 +295,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
292
295
  console.log(` Skills: ${ergebnis.skills.join(', ')}`);
293
296
  if (ergebnis.imZeiger)
294
297
  console.log(' im Zeiger vermerkt');
298
+ // Ein Hinweis kann mehrzeilig sein, jede Zeile bekommt dieselbe Einrückung.
295
299
  if (ergebnis.hinweis !== undefined)
296
- console.log(` ${ergebnis.hinweis}`);
300
+ for (const zeile of ergebnis.hinweis.split('\n'))
301
+ console.log(` ${zeile}`);
297
302
  return 0;
298
303
  }
299
304
  if (kommando === 'sync') {
@@ -11,6 +11,8 @@ function fehlertext(fehler) {
11
11
  }
12
12
  export class Anwendungsfaelle {
13
13
  p;
14
+ /** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
15
+ beantwortet = new Map();
14
16
  constructor(p) {
15
17
  this.p = p;
16
18
  }
@@ -280,19 +282,47 @@ export class Anwendungsfaelle {
280
282
  const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
281
283
  const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
282
284
  await this.zeigerErgaenzen(zeigerPfad, fund, version);
283
- return link;
285
+ const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
286
+ return hinweis === undefined ? link : { ...link, hinweis };
284
287
  }
285
288
  /** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
286
289
  async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
287
290
  const link = await this.linkAnlegen(harness, ziel, fund, ersetzen);
288
- if (lokal === true || ziel.ort.art !== 'arbeitsverzeichnis')
289
- return link;
290
- const zeigerPfad = await this.bindungFinden(ziel);
291
- // Ziel ohne Zeiger: der Link wird trotzdem gelegt, hmgr weist darauf hin (E26).
292
- if (zeigerPfad === undefined)
293
- return { ...link, hinweis: `Ohne Zeiger bleibt „${link.name}" lokal, der Link ist gelegt (E26).` };
294
- await this.zeigerErgaenzen(zeigerPfad, fund);
295
- return link;
291
+ const hinweise = [];
292
+ if (lokal !== true && ziel.ort.art === 'arbeitsverzeichnis') {
293
+ const zeigerPfad = await this.bindungFinden(ziel);
294
+ // Ziel ohne Zeiger: der Link wird trotzdem gelegt, hmgr weist darauf hin (E26).
295
+ if (zeigerPfad === undefined)
296
+ hinweise.push(`Ohne Zeiger bleibt „${link.name}" lokal, der Link ist gelegt (E26).`);
297
+ else
298
+ await this.zeigerErgaenzen(zeigerPfad, fund);
299
+ }
300
+ const vomHarness = await this.harnessHinweis(harness, ziel.ort);
301
+ if (vomHarness !== undefined)
302
+ hinweise.push(vomHarness);
303
+ return hinweise.length === 0 ? link : { ...link, hinweis: hinweise.join('\n') };
304
+ }
305
+ /**
306
+ * Offener Punkt des Harness zu einem Ort (E34), etwa fehlendes Vertrauen für Projekt-Skills. Ein
307
+ * Harness ohne diese Fähigkeit schweigt. Nennt der Harness eine Abhilfe, fragt hmgr den Nutzer:
308
+ * bei Ja leistet der Harness sie und die Ergebniszeile tritt an die Stelle des Hinweises, bei
309
+ * Nein bleibt es beim Hinweis. Je Lauf und Ziel höchstens eine Frage, die Antwort gilt für den
310
+ * ganzen Lauf.
311
+ */
312
+ async harnessHinweis(harness, ort) {
313
+ const schluessel = zielSchluessel({ harness: harness.name, ort });
314
+ if (this.beantwortet.has(schluessel))
315
+ return this.beantwortet.get(schluessel);
316
+ const punkt = await harness.hinweisNachLink?.(ort);
317
+ let hinweis = punkt?.hinweis;
318
+ const abhilfe = punkt?.abhilfe;
319
+ if (abhilfe !== undefined && (await this.p.rueckfrage.bestaetigen(abhilfe.frage))) {
320
+ await abhilfe.ausfuehren();
321
+ hinweis = abhilfe.erledigt;
322
+ }
323
+ if (punkt !== undefined)
324
+ this.beantwortet.set(schluessel, hinweis);
325
+ return hinweis;
296
326
  }
297
327
  /** Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). */
298
328
  async linkEntfernen(args) {
@@ -325,7 +355,15 @@ export class Anwendungsfaelle {
325
355
  const skills = await this.flacheSkillListe(fund.plugin.name, fund.quelle);
326
356
  for (const skill of skills)
327
357
  await this.linkAnlegen(harness, ziel, { quelle: fund.quelle, ref: fund.ref, skill });
328
- return { plugin: fund.plugin, art: 'verlinkt', skills: skills.map((s) => s.name), imZeiger: false };
358
+ // Der Hinweis des Harness gilt dem Ort, also einmal für das ganze Plugin, nicht je Skill (E34).
359
+ const hinweis = await this.harnessHinweis(harness, ziel.ort);
360
+ return {
361
+ plugin: fund.plugin,
362
+ art: 'verlinkt',
363
+ skills: skills.map((s) => s.name),
364
+ imZeiger: false,
365
+ ...(hinweis === undefined ? {} : { hinweis }),
366
+ };
329
367
  }
330
368
  /**
331
369
  * Plugin legen (E16, E17, E24, E27). In ein Arbeitsverzeichnis mit Zeiger ist das Plugin
@@ -338,8 +376,11 @@ export class Anwendungsfaelle {
338
376
  if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
339
377
  return ergebnis;
340
378
  const zeigerPfad = await this.bindungFinden(args.ziel);
341
- if (zeigerPfad === undefined)
342
- return { ...ergebnis, hinweis: `Ohne Zeiger bleibt „${args.plugin}" lokal, das Plugin liegt am Ziel (E26).` };
379
+ if (zeigerPfad === undefined) {
380
+ const ohneZeiger = `Ohne Zeiger bleibt „${args.plugin}" lokal, das Plugin liegt am Ziel (E26).`;
381
+ const hinweis = ergebnis.hinweis === undefined ? ohneZeiger : `${ohneZeiger}\n${ergebnis.hinweis}`;
382
+ return { ...ergebnis, hinweis };
383
+ }
343
384
  const zeiger = await this.p.zeiger.lesen(zeigerPfad);
344
385
  const key = quelleSchluessel(fund.quelle);
345
386
  if (!zeiger.quellen.some((q) => quelleSchluessel(q) === key))
@@ -540,6 +581,10 @@ export class Anwendungsfaelle {
540
581
  continue;
541
582
  ergebnis.abweichend.push(`${harness.name}: ${name} steht nicht im Zeiger`);
542
583
  }
584
+ // Hinweis des Harness zum Ort, einmal je Harness und Lauf, nicht je Skill (E34).
585
+ const vomHarness = await this.harnessHinweis(harness, ziel.ort);
586
+ if (vomHarness !== undefined)
587
+ ergebnis.hinweise.push(vomHarness);
543
588
  }
544
589
  return ergebnis;
545
590
  }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",
@@ -31,5 +31,9 @@
31
31
  "@types/node": "^24.0.0",
32
32
  "typescript": "^5.9.0",
33
33
  "vitest": "^3.2.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://gitea.timkaufmann.de/tim/hmgr.git"
34
38
  }
35
39
  }