feat(omos): interactive init and setup commands
- omos init [--fill]: create user mapping file, interactively resolve PLACEHOLDER classes by picking from models discovered in opencode.json(c) - omos setup: guided flow - show profile summary, create/fill missing mappings with explicit user choice, then render - README quickstart updated to the two-command path
This commit is contained in:
parent
f035902f20
commit
b897bb4b17
2 changed files with 210 additions and 7 deletions
|
|
@ -19,6 +19,22 @@ team-profile.yaml + model-mapping.yaml → omos render → .opencode/
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Modell-Mapping anlegen – interaktiv, mit Auswahl aus deinen opencode-Modellen
|
||||||
|
python3 omos.py init --fill
|
||||||
|
|
||||||
|
# 2. Geführt: Team-Profil lesen, Mapping prüfen/auffüllen, rendern
|
||||||
|
cd <projekt>
|
||||||
|
python3 /pfad/zu/omos.py setup --package /pfad/zum/package
|
||||||
|
|
||||||
|
# 3. Aktivieren
|
||||||
|
opencode # Projekt-Config wird geladen
|
||||||
|
/preset acme-job-applications
|
||||||
|
# Reload OpenCode → Team aktiv (by Design: kein Hot-Swap)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ manuell:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Modell-Mapping einmalig anlegen
|
# 1. Modell-Mapping einmalig anlegen
|
||||||
mkdir -p ~/.config/apm-team
|
mkdir -p ~/.config/apm-team
|
||||||
|
|
@ -31,11 +47,6 @@ python3 omos.py check --package examples/package
|
||||||
# 3. Rendern
|
# 3. Rendern
|
||||||
cd <projekt>
|
cd <projekt>
|
||||||
python3 /pfad/zu/omos.py render --package /pfad/zum/package
|
python3 /pfad/zu/omos.py render --package /pfad/zum/package
|
||||||
|
|
||||||
# 4. Aktivieren
|
|
||||||
opencode # Projekt-Config wird geladen
|
|
||||||
/preset acme-job-applications
|
|
||||||
# Reload OpenCode → Team aktiv (by Design: kein Hot-Swap)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Die drei Dateien
|
## Die drei Dateien
|
||||||
|
|
@ -51,10 +62,22 @@ Dazu eine Provenance-Datei `.opencode/oh-my-opencode-slim.omos-provenance.json`,
|
||||||
## Kommandoreferenz
|
## Kommandoreferenz
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 omos.py render --package DIR [--mapping FILE] [--output FILE] [--dry-run]
|
python3 omos.py init [--mapping FILE] [--fill] # Mapping anlegen/füllen
|
||||||
python3 omos.py check --package DIR [--mapping FILE]
|
python3 omos.py setup --package DIR [--mapping F] ... # geführter Ablauf
|
||||||
|
python3 omos.py render --package DIR [--mapping F] [--output F] [--dry-run]
|
||||||
|
python3 omos.py check --package DIR [--mapping F] # Dry-Run-Validierung
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `init` – Mapping anlegen
|
||||||
|
|
||||||
|
Erstellt `~/.config/apm-team/model-mapping.yaml` (falls nicht vorhanden). Mit `--fill` werden alle `PLACEHOLDER`-Klassen interaktiv abgefragt: `omos` listet alle Modelle aus deiner `opencode.json(c)` auf, du wählst per Nummer oder gibst eine Modell-ID manuell ein.
|
||||||
|
|
||||||
|
### `setup` – Geführter Ablauf
|
||||||
|
|
||||||
|
Der Ein-Kommando-Weg: zeigt das Team-Profil verständlich an, legt fehlendes Mapping an, fragt fehlende Modellklassen interaktiv ab (gleiche Auswahl wie `init --fill`), rendert und gibt die Aktivierungsschritte aus. Entspricht dem Resolve–Validate–Render-Ablauf.
|
||||||
|
|
||||||
|
### `render` / `check`
|
||||||
|
|
||||||
| Option | Default | Bedeutung |
|
| Option | Default | Bedeutung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `--package` | `.` | APM-Package-Verzeichnis mit `team-profile.yaml` |
|
| `--package` | `.` | APM-Package-Verzeichnis mit `team-profile.yaml` |
|
||||||
|
|
|
||||||
180
omos/omos.py
180
omos/omos.py
|
|
@ -256,6 +256,46 @@ def configured_mcps() -> set[str]:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def available_models() -> list[str]:
|
||||||
|
"""Listet alle provider/model-Kombinationen aus der opencode.json(c)."""
|
||||||
|
candidates = [
|
||||||
|
Path.home() / ".config" / "opencode" / "opencode.jsonc",
|
||||||
|
Path.home() / ".config" / "opencode" / "opencode.json",
|
||||||
|
]
|
||||||
|
models: list[str] = []
|
||||||
|
for path in candidates:
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = json.loads(strip_jsonc(path.read_text(encoding="utf-8")))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
for provider, pconf in (data.get("provider") or {}).items():
|
||||||
|
for model in (pconf.get("models") or {}):
|
||||||
|
models.append(f"{provider}/{model}")
|
||||||
|
break
|
||||||
|
return sorted(models)
|
||||||
|
|
||||||
|
|
||||||
|
INIT_TEMPLATE = """\
|
||||||
|
# Lokale Mapping-Tabelle des NUTZERS.
|
||||||
|
# Ordnet abstrakte Modellklassen konkrete Modell-IDs zu.
|
||||||
|
# Formate:
|
||||||
|
# klasse: provider/modell
|
||||||
|
# klasse:
|
||||||
|
# model: provider/modell
|
||||||
|
# variant: thinking # optional
|
||||||
|
# temperature: 0.7 # optional
|
||||||
|
|
||||||
|
model_classes:
|
||||||
|
strong-generalist: PLACEHOLDER
|
||||||
|
fast-research: PLACEHOLDER
|
||||||
|
strong-writing: PLACEHOLDER
|
||||||
|
high-reasoning: PLACEHOLDER
|
||||||
|
cheap-reliable: PLACEHOLDER
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def check_mcps(preset: dict, warnings: list[str]) -> None:
|
def check_mcps(preset: dict, warnings: list[str]) -> None:
|
||||||
available = configured_mcps()
|
available = configured_mcps()
|
||||||
if not available:
|
if not available:
|
||||||
|
|
@ -372,6 +412,115 @@ def _report(warnings: list[str], dry_run: bool = False) -> None:
|
||||||
print("\nKeine Warnungen.")
|
print("\nKeine Warnungen.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interaktive Assistenten
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_model(model_class: str, models: list[str]) -> str:
|
||||||
|
"""Interaktive Auswahl eines Modells für eine Modellklasse."""
|
||||||
|
print(f"\nFür '{model_class}' ist kein Modell zugeordnet.")
|
||||||
|
if models:
|
||||||
|
print("Verfügbare Modelle (aus opencode.json(c)):")
|
||||||
|
for i, model in enumerate(models, 1):
|
||||||
|
print(f" {i}. {model}")
|
||||||
|
print(" 0. Modell-ID manuell eingeben")
|
||||||
|
while True:
|
||||||
|
choice = input("Auswahl: ").strip()
|
||||||
|
if choice == "0":
|
||||||
|
return input("Modell-ID (provider/model): ").strip()
|
||||||
|
if choice.isdigit() and 1 <= int(choice) <= len(models):
|
||||||
|
return models[int(choice) - 1]
|
||||||
|
print("Ungültige Auswahl.")
|
||||||
|
else:
|
||||||
|
print("Keine Modelle in opencode.json(c) gefunden.")
|
||||||
|
return input("Modell-ID (provider/model): ").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_init(args: argparse.Namespace) -> int:
|
||||||
|
mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH
|
||||||
|
if mapping_path.exists():
|
||||||
|
print(f"Mapping existiert bereits: {mapping_path}")
|
||||||
|
else:
|
||||||
|
mapping_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
mapping_path.write_text(INIT_TEMPLATE, encoding="utf-8")
|
||||||
|
print(f"Angelegt: {mapping_path}")
|
||||||
|
|
||||||
|
if args.fill:
|
||||||
|
missing = [k for k, v in load_mapping(mapping_path).items()
|
||||||
|
if v == "PLACEHOLDER"]
|
||||||
|
models = available_models()
|
||||||
|
if missing and not models:
|
||||||
|
raise OmosError(
|
||||||
|
"Keine Modelle in ~/.config/opencode/opencode.json(c) gefunden. "
|
||||||
|
"Trage die Modell-IDs manuell ein oder richte opencode ein."
|
||||||
|
)
|
||||||
|
data = load_yaml(mapping_path)
|
||||||
|
for model_class in missing:
|
||||||
|
data["model_classes"][model_class] = _choose_model(model_class, models)
|
||||||
|
mapping_path.write_text(
|
||||||
|
yaml.safe_dump(data, sort_keys=False, allow_unicode=True),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
remaining = sum(1 for v in data["model_classes"].values()
|
||||||
|
if v == "PLACEHOLDER")
|
||||||
|
if remaining:
|
||||||
|
print(f"\n{remaining} Eintrag/Einträge bleiben PLACEHOLDER – "
|
||||||
|
"bitte manuell ergänzen.")
|
||||||
|
|
||||||
|
print("\nNächster Schritt:")
|
||||||
|
print(f" omos.py check --package <package-dir> --mapping {mapping_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_setup(args: argparse.Namespace) -> int:
|
||||||
|
"""Geführter Ablauf: Profil lesen → Mapping auffüllen → rendern."""
|
||||||
|
package_dir = Path(args.package).resolve()
|
||||||
|
profile, profile_path = load_team_profile(package_dir, args.team_file)
|
||||||
|
mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH
|
||||||
|
|
||||||
|
print(f"Team-Profil: {profile['id']}")
|
||||||
|
if profile.get("description"):
|
||||||
|
print(f" {str(profile['description']).strip()}")
|
||||||
|
print("Rollen:")
|
||||||
|
for role in profile["roles"]:
|
||||||
|
mcps = (role.get("capabilities", {}) or {}).get("mcps", [])
|
||||||
|
skills = (role.get("capabilities", {}) or {}).get("skills", [])
|
||||||
|
print(f" - {role['id']} → {role.get('model_class')} "
|
||||||
|
f"(MCPs: {', '.join(mcps) or 'keine'}; "
|
||||||
|
f"Skills: {', '.join(skills) or 'keine'})")
|
||||||
|
|
||||||
|
if not mapping_path.exists():
|
||||||
|
print(f"\nMapping fehlt – lege an: {mapping_path}")
|
||||||
|
args_init = argparse.Namespace(mapping=str(mapping_path), fill=True)
|
||||||
|
cmd_init(args_init)
|
||||||
|
|
||||||
|
classes = load_mapping(mapping_path)
|
||||||
|
needed = sorted({r.get("model_class") for r in profile["roles"]
|
||||||
|
if r.get("model_class")} - set(classes))
|
||||||
|
models = available_models()
|
||||||
|
changed = False
|
||||||
|
for model_class in needed:
|
||||||
|
chosen = _choose_model(model_class, models)
|
||||||
|
classes[model_class] = {"model": chosen, "# selected_by": "user"}
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
mapping_path.write_text(
|
||||||
|
yaml.safe_dump(classes, sort_keys=False, allow_unicode=True),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"\nMapping aktualisiert: {mapping_path}")
|
||||||
|
|
||||||
|
print("\nRendern …")
|
||||||
|
args_render = argparse.Namespace(
|
||||||
|
package=str(package_dir),
|
||||||
|
team_file=args.team_file,
|
||||||
|
mapping=str(mapping_path),
|
||||||
|
output=args.output,
|
||||||
|
dry_run=args.dry_run,
|
||||||
|
)
|
||||||
|
return cmd_render(args_render)
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="omos",
|
prog="omos",
|
||||||
|
|
@ -415,6 +564,37 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
common(p_check)
|
common(p_check)
|
||||||
p_check.set_defaults(func=cmd_check)
|
p_check.set_defaults(func=cmd_check)
|
||||||
|
|
||||||
|
p_init = sub.add_parser(
|
||||||
|
"init",
|
||||||
|
help="Modell-Mapping-Datei anlegen und optional Platzhalter füllen",
|
||||||
|
)
|
||||||
|
p_init.add_argument(
|
||||||
|
"--mapping",
|
||||||
|
help=f"Zielpfad (Default: {DEFAULT_MAPPING_PATH})",
|
||||||
|
)
|
||||||
|
p_init.add_argument(
|
||||||
|
"--fill",
|
||||||
|
action="store_true",
|
||||||
|
help="Fehlende Modellklassen interaktiv aus opencode.json(c)-Modellen wählen",
|
||||||
|
)
|
||||||
|
p_init.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
p_setup = sub.add_parser(
|
||||||
|
"setup",
|
||||||
|
help="Geführt: Profil lesen, Mapping auffüllen, rendern",
|
||||||
|
)
|
||||||
|
common(p_setup)
|
||||||
|
p_setup.add_argument(
|
||||||
|
"--output",
|
||||||
|
help=f"Zieldatei (Default: {DEFAULT_OUTPUT} relativ zum Package)",
|
||||||
|
)
|
||||||
|
p_setup.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Nur anzeigen, nichts schreiben",
|
||||||
|
)
|
||||||
|
p_setup.set_defaults(func=cmd_setup)
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
try:
|
try:
|
||||||
return args.func(args)
|
return args.func(args)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue