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
|
||||
|
||||
```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
|
||||
# 1. Modell-Mapping einmalig anlegen
|
||||
mkdir -p ~/.config/apm-team
|
||||
|
|
@ -31,11 +47,6 @@ python3 omos.py check --package examples/package
|
|||
# 3. Rendern
|
||||
cd <projekt>
|
||||
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
|
||||
|
|
@ -51,10 +62,22 @@ Dazu eine Provenance-Datei `.opencode/oh-my-opencode-slim.omos-provenance.json`,
|
|||
## Kommandoreferenz
|
||||
|
||||
```bash
|
||||
python3 omos.py render --package DIR [--mapping FILE] [--output FILE] [--dry-run]
|
||||
python3 omos.py check --package DIR [--mapping FILE]
|
||||
python3 omos.py init [--mapping FILE] [--fill] # Mapping anlegen/füllen
|
||||
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 |
|
||||
|---|---|---|
|
||||
| `--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()
|
||||
|
||||
|
||||
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:
|
||||
available = configured_mcps()
|
||||
if not available:
|
||||
|
|
@ -372,6 +412,115 @@ def _report(warnings: list[str], dry_run: bool = False) -> None:
|
|||
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:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="omos",
|
||||
|
|
@ -415,6 +564,37 @@ def main(argv: list[str] | None = None) -> int:
|
|||
common(p_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)
|
||||
try:
|
||||
return args.func(args)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue