feat(app+brain): App-Bugfixes + Skill-Mgmt-Tools + Voice-Speed persistent + Skill-Browser

App-Bugs:
- Trigger-Liste war leer: brainApi.listTriggers() cast'te {triggers: [...]}
  direkt als Array, t.sort() warf — TriggerBrowser blieb leer. Fix: unwrap.
- GPS-Tracking startete erst bei SettingsScreen-Mount, nicht beim App-Boot.
  Wenn Stefan direkt in den Chat ging, blieb GPS aus. Fix: restoreFromStorage()
  in App.tsx useEffect.
- Text in Chat-Bubbles nicht markierbar / kein Copy-Mechanismus: Bubble jetzt
  Pressable mit onLongPress + neues ⎘-Icon in Status-Row → openBubbleActions().
  Alert-Menu mit "Ganzen Text teilen" + pro extrahierte URL/Mail/Tel eine
  eigene Option. Share.share() — keine neuen Native-Deps noetig.

Brain — Skill-Mgmt:
- ARIA legte beim Skill-Umbau neue Versionen mit Suffix an (Skill-Friedhof),
  weil sie kein Update/Delete-Tool kannte. Zwei neue META_TOOLS in agent.py:
  skill_update (kann entry_code, readme, pip_packages, args, description,
  active patchen — venv wird bei pip_packages-Aenderung rebuilt) + skill_delete.
- skills.py update_skill um entry_code/readme/pip_packages erweitert,
  venv-Rebuild bei pip-Aenderung.

Bridge — Voice-Speed persistent:
- _next_speed_override war pro-Request-Override ohne Persistenz. Bei
  Diagnostic-Chats / Trigger-Replies ohne vorherigen App-Chat fiel der Speed
  auf 1.0 zurueck, ebenso nach Bridge-Restart. Jetzt: _persistent_xtts_speed
  aus voice_config.json (xttsSpeed), wird nach jedem App-chat mit speed
  autopersistiert. TTS-Generation faellt zurueck: per-Request > persistent > 1.0.

App — Feature 6:
- SkillBrowser.tsx: Liste aller Skills, Toggle aktiv/inaktiv, Detail-Modal
  mit Args-Inputs, Ausfuehren mit Live-stdout/stderr, Logs der letzten 20
  Runs, Loeschen. Settings-Sektion "Skills" (🛠️) zwischen Trigger und
  Protokoll. brainApi.listSkills/getSkill/runSkill/updateSkill/deleteSkill/
  getSkillLogs ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:24:03 +02:00
parent 9ed9c99b0e
commit 30c1dd7473
8 changed files with 862 additions and 8 deletions
+101
View File
@@ -123,6 +123,67 @@ META_TOOLS = [
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"function": {
"name": "skill_update",
"description": (
"Aktualisiere einen EXISTIERENDEN Skill statt eine zweite Version "
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
"auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n"
"Du kannst gleichzeitig `entry_code` (Python-Code austauschen), "
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
"neu aufgebaut), `args`, `description` und `active` setzen. Felder "
"die Du weglaesst bleiben unberuehrt.\n\n"
"WENN Du Dir bei einem grundlegenden API-Bruch unsicher bist ob "
"der Skill noch zum Namen passt: lieber `skill_delete` + "
"`skill_create` mit neuem semantischen Namen statt eines "
"halbgaren Updates."
),
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Bestehender Skill-Name"},
"entry_code": {"type": "string", "description": "Neuer Python-Code (optional)"},
"readme": {"type": "string", "description": "Neuer README-Inhalt (optional)"},
"pip_packages": {
"type": "array",
"items": {"type": "string"},
"description": "Neue pip-Pakete (ueberschreibt komplette Liste; triggert venv-Rebuild)",
},
"args": {
"type": "array",
"items": {"type": "object"},
"description": "Neues Args-Schema (optional)",
},
"description": {"type": "string", "description": "Neue Beschreibung (optional)"},
"active": {"type": "boolean", "description": "Aktivieren/deaktivieren (optional)"},
},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
"name": "skill_delete",
"description": (
"Loescht einen Skill samt venv und Logs. Nutze das wenn:\n"
"1. Stefan explizit sagt der Skill soll weg\n"
"2. Du eine alte Skill-Version losgeworden bist nachdem `skill_create` "
"mit besserem Namen erfolgreich war (Aufraeumen statt Skill-Friedhof)\n"
"3. Ein Skill grundlegend kaputt und ein Update sich nicht mehr lohnt — "
"in dem Fall bestaetige vorher kurz bei Stefan.\n\n"
"Nicht rueckholbar."
),
"parameters": {
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
@@ -746,6 +807,46 @@ class Agent:
f"- {s['name']} ({s['execution']}) {'aktiv' if s.get('active', True) else 'DEAKTIVIERT'}: {s.get('description', '')}"
for s in items
)
if name == "skill_update":
skill_name = (arguments.get("name") or "").strip()
if not skill_name:
return "FEHLER: name ist Pflicht."
patch: dict = {}
for k in ("entry_code", "readme", "description", "args", "active"):
if k in arguments and arguments[k] is not None:
patch[k] = arguments[k]
if "pip_packages" in arguments and isinstance(arguments["pip_packages"], list):
patch["pip_packages"] = arguments["pip_packages"]
if not patch:
return "FEHLER: keine Felder zum Update angegeben."
try:
manifest = skills_mod.update_skill(skill_name, patch)
except ValueError as exc:
return f"FEHLER: {exc}"
# Side-Channel-Event als skill_created getarnt — gleiche Bubble-Mechanik
# in App/Diagnostic; das Update soll fuer Stefan ebenfalls sichtbar werden.
self._pending_events.append({
"type": "skill_created",
"skill": {
"name": manifest["name"],
"description": manifest.get("description", ""),
"execution": manifest.get("execution", ""),
"active": manifest.get("active", True),
"setup_error": manifest.get("setup_error"),
"updated": True,
},
})
changed = ", ".join(sorted(patch.keys()))
return f"OK — Skill '{skill_name}' aktualisiert ({changed}). active={manifest['active']}"
if name == "skill_delete":
skill_name = (arguments.get("name") or "").strip()
if not skill_name:
return "FEHLER: name ist Pflicht."
try:
skills_mod.delete_skill(skill_name)
except ValueError as exc:
return f"FEHLER: {exc}"
return f"OK — Skill '{skill_name}' geloescht."
if name.startswith("run_"):
skill_name = name[len("run_"):]
res = skills_mod.run_skill(skill_name, args=arguments)
+45
View File
@@ -194,14 +194,59 @@ def _setup_venv(skill_dir: Path, pip_packages: list[str]) -> None:
def update_skill(name: str, patch: dict) -> dict:
"""Aktualisiert einen bestehenden Skill. Manifest-Felder ueber den
`allowed`-Filter, Code-Aenderungen ueber dedizierte Keys:
- `entry_code` (str) → ueberschreibt run.py / run.sh
- `readme` (str) → ueberschreibt README.md
- `pip_packages` (list) → ueberschreibt requirements.txt + venv-Rebuild
(nur bei local-venv)
"""
manifest = read_manifest(name)
if manifest is None:
raise ValueError(f"Skill '{name}' nicht gefunden")
d = _skill_dir(name)
allowed = {"description", "args", "requires", "active", "version", "entry"}
for k, v in patch.items():
if k in allowed:
manifest[k] = v
# Code austauschen
if "entry_code" in patch and patch["entry_code"]:
execution = manifest.get("execution", "local-venv")
if execution == "local-venv":
entry_path = d / "run.py"
entry_path.write_text(patch["entry_code"], encoding="utf-8")
else:
entry_path = d / "run.sh"
content = patch["entry_code"] if patch["entry_code"].startswith("#!") else "#!/usr/bin/env bash\nset -euo pipefail\n" + patch["entry_code"]
entry_path.write_text(content, encoding="utf-8")
entry_path.chmod(0o755)
# README austauschen
if "readme" in patch and patch["readme"] is not None:
(d / "README.md").write_text(patch["readme"], encoding="utf-8")
# pip_packages geaendert → requirements.txt + venv neu aufbauen
if "pip_packages" in patch and manifest.get("execution") == "local-venv":
pip_packages = patch["pip_packages"] or []
(d / "requirements.txt").write_text("\n".join(pip_packages) + "\n", encoding="utf-8")
# venv loeschen + neu aufbauen, damit alte Pakete weg sind
venv = d / "venv"
if venv.exists():
shutil.rmtree(venv, ignore_errors=True)
try:
_setup_venv(d, pip_packages)
# Falls vorher wegen Setup-Error deaktiviert war: jetzt frei
manifest.pop("setup_error", None)
manifest["active"] = patch.get("active", True)
except Exception as exc:
manifest["active"] = False
manifest["setup_error"] = str(exc)[:500]
logger.warning("Skill %s: venv-Rebuild fehlgeschlagen: %s", name, exc)
write_manifest(name, manifest)
logger.info("Skill aktualisiert: %s (keys=%s)", name, sorted(patch.keys()))
return manifest