diff --git a/backend/routers/skills.py b/backend/routers/skills.py
index ed5beff..75800c5 100644
--- a/backend/routers/skills.py
+++ b/backend/routers/skills.py
@@ -1,5 +1,6 @@
import os
import subprocess
+import psutil
from fastapi import APIRouter
from pydantic import BaseModel
@@ -18,6 +19,22 @@ def list_skills():
skills_dir = os.path.join(repo_root, "deploy", "skills")
skills = []
+ # Check currently running hermes skill processes
+ running_skills = set()
+ for p in psutil.process_iter(['name', 'cmdline']):
+ try:
+ cmdline = p.info.get('cmdline')
+ if cmdline and any("hermes" in arg for arg in cmdline):
+ for arg in cmdline:
+ if "Führe den " in arg and " Skill aus" in arg:
+ # Extract skill name from "Führe den 'skill_name' Skill aus"
+ import re
+ match = re.search(r"Führe den '(.+?)' Skill aus", arg)
+ if match:
+ running_skills.add(match.group(1))
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
+ pass
+
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
for entry in os.scandir(skills_dir):
if entry.is_dir():
@@ -40,7 +57,8 @@ def list_skills():
skills.append({
"name": entry.name,
- "description": description or "Keine Beschreibung verfügbar."
+ "description": description or "Keine Beschreibung verfügbar.",
+ "running": entry.name in running_skills
})
return {"skills": skills}
diff --git a/backend/routers/wissen.py b/backend/routers/wissen.py
deleted file mode 100644
index ac92634..0000000
--- a/backend/routers/wissen.py
+++ /dev/null
@@ -1,151 +0,0 @@
-"""Wissens-Vault-Reader: die nächtlichen Traum-Notizen (~/wissens-vault) als klickbares
-Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom Traum-Cron
-(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
-
-import os
-import re
-import time
-from pathlib import Path
-
-from fastapi import APIRouter, HTTPException
-from pydantic import BaseModel
-
-router = APIRouter(prefix="/api")
-
-VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
-
-
-def _available() -> bool:
- return VAULT.is_dir()
-
-
-def _title_of(p: Path) -> str:
- """Erste nicht-leere Zeile (ohne Markdown-#) als Anzeigename."""
- try:
- with p.open(encoding="utf-8", errors="replace") as fh:
- for line in fh:
- s = line.strip()
- if s:
- return s.lstrip("# ").strip()[:160]
- except OSError:
- pass
- return p.stem
-
-
-class VaultFile(BaseModel):
- path: str
- name: str
- dir: str
- title: str
- mtime: float
- neu: bool
-
-
-class WissenListResponse(BaseModel):
- available: bool
- files: list[VaultFile]
-
-
-class WissenFileResponse(BaseModel):
- path: str
- content: str
- mtime: float
-
-
-@router.get("/wissen", response_model=WissenListResponse)
-def list_vault() -> dict:
- """Alle Markdown-Notizen des Vaults (relativer Pfad, Titel, Ordner, Alter)."""
- if not _available():
- return {"available": False, "files": []}
- files = []
- now = time.time()
- for p in sorted(VAULT.rglob("*.md")):
- if ".git" in p.parts:
- continue
- rel = p.relative_to(VAULT).as_posix()
- st = p.stat()
- files.append({
- "path": rel,
- "name": p.stem,
- "dir": p.parent.relative_to(VAULT).as_posix() if p.parent != VAULT else "",
- "title": _title_of(p),
- "mtime": st.st_mtime,
- "neu": (now - st.st_mtime) < 36 * 3600, # „neu seit gestern Nacht"
- })
- files.sort(key=lambda f: f["mtime"], reverse=True)
- return {"available": True, "files": files}
-
-
-@router.get("/wissen/datei", response_model=WissenFileResponse)
-def read_file(pfad: str) -> dict:
- """Inhalt einer Vault-Notiz — Traversal hart geblockt (resolve + is_relative_to)."""
- if not _available():
- raise HTTPException(404, "Wissens-Vault liegt auf der Box (hier nicht verfügbar).")
- try:
- target = (VAULT / pfad).resolve()
- if not target.is_relative_to(VAULT.resolve()) or target.suffix != ".md" or not target.is_file():
- raise HTTPException(404, "Notiz nicht gefunden.")
- return {"path": pfad, "content": target.read_text(encoding="utf-8", errors="replace")[:400_000],
- "mtime": target.stat().st_mtime}
- except HTTPException:
- raise
- except (ValueError, OSError):
- raise HTTPException(404, "Notiz nicht lesbar.")
-
-
-@router.get("/wissen/graph")
-def graph_vault() -> dict:
- """Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links)."""
- if not _available():
- return {"nodes": [], "edges": []}
-
- nodes = []
- edges = []
- # Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID)
- name_to_id = {}
-
- # 1. Alle Nodes sammeln
- for p in VAULT.rglob("*.md"):
- if ".git" in p.parts:
- continue
- rel = p.relative_to(VAULT).as_posix()
- name_to_id[p.stem.lower()] = rel
-
- # Kategorie aus dem Ordner ableiten
- cat = "knowledge"
- parent = p.parent.name if p.parent != VAULT else ""
- if parent == "traeume":
- cat = "events"
- elif parent == "muster":
- cat = "rules"
- elif parent == "skill-kandidaten":
- cat = "identity"
-
- nodes.append({
- "id": rel,
- "content": p.stem, # stem ist oft kürzer/prägnanter als der Titel
- "category": cat,
- "source": "wiki"
- })
-
- # 2. Edges extrahieren (Wiki-Links [[Name]])
- link_rx = re.compile(r"\[\[([^\]]+)\]\]")
- for n in nodes:
- try:
- target = VAULT / n["id"]
- content = target.read_text(encoding="utf-8", errors="replace")
- # Set, um mehrfache Links auf dieselbe Datei zu entduplizieren
- found_links = {m.group(1).strip().lower() for m in link_rx.finditer(content)}
-
- for link in found_links:
- target_id = name_to_id.get(link)
- if target_id and target_id != n["id"]:
- edges.append({
- "source": n["id"],
- "target": target_id,
- "weight": 1.0
- })
- except OSError:
- pass
-
- return {"nodes": nodes, "edges": edges}
diff --git a/frontend/src/views/SkillsView.tsx b/frontend/src/views/SkillsView.tsx
index 4885056..e363dfd 100644
--- a/frontend/src/views/SkillsView.tsx
+++ b/frontend/src/views/SkillsView.tsx
@@ -9,6 +9,7 @@ import { JobsBar } from "@/components/models/JobsBar"
interface Skill {
name: string
description: string
+ running?: boolean
}
interface SkillsResp {
@@ -98,16 +99,16 @@ export function SkillsView() {
diff --git a/frontend/src/views/WissenView.tsx b/frontend/src/views/WissenView.tsx
deleted file mode 100644
index fcbf697..0000000
--- a/frontend/src/views/WissenView.tsx
+++ /dev/null
@@ -1,283 +0,0 @@
-import { useEffect, useMemo, useState } from "react"
-import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
-import { api, type WissenDatei, type WissenFile } from "@/lib/api"
-import { useWissen, useMemoryGraph } from "@/lib/queries"
-import { GraphView } from "./GraphView"
-import { GraphErrorBoundary } from "./GraphErrorBoundary"
-import { cn } from "@/lib/utils"
-
-// Wissens-Vault: die nächtlichen Traum-Notizen der Box als klickbares Wiki.
-// [[wiki-links]] springen zur verlinkten Notiz; Markdown wird leichtgewichtig gerendert
-// (bewusst ohne externe Markdown-Lib — Überschriften, Listen, fett/kursiv, Code reichen hier).
-
-const DIR_LABEL: Record
= {
- "": "Allgemein",
- traeume: "Träume",
- muster: "Muster",
- "skill-kandidaten": "Skill-Kandidaten",
- "skill-kandidaten/beauftragt": "Skill-Kandidaten · beauftragt",
- "skill-kandidaten/verworfen": "Skill-Kandidaten · verworfen",
-}
-
-export function WissenView() {
- const { data, isLoading } = useWissen()
- const [active, setActive] = useState(null)
- const [doc, setDoc] = useState(null)
- const [docLoading, setDocLoading] = useState(false)
- const [q, setQ] = useState("")
- const [showGraph, setShowGraph] = useState(false)
-
- const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph)
-
- const files = useMemo(() => data?.files ?? [], [data])
-
- // Name → Pfad für [[wiki-links]] (Dateiname ohne .md, case-insensitive).
- const byName = useMemo(() => {
- const m = new Map()
- for (const f of files) m.set(f.name.toLowerCase(), f.path)
- return m
- }, [files])
-
- const filtered = useMemo(() => {
- if (!q.trim()) return files
- const ql = q.toLowerCase()
- return files.filter((f) => f.title.toLowerCase().includes(ql) || f.path.toLowerCase().includes(ql))
- }, [files, q])
-
- const groups = useMemo(() => {
- const g: { dir: string; files: WissenFile[] }[] = []
- for (const f of filtered) {
- const found = g.find((x) => x.dir === f.dir)
- if (found) found.files.push(f)
- else g.push({ dir: f.dir, files: [f] })
- }
- // INDEX zuerst, dann Träume, dann Rest alphabetisch
- return g.sort((a, b) => (a.dir === "" ? -1 : b.dir === "" ? 1 : a.dir.localeCompare(b.dir)))
- }, [filtered])
-
- // Beim ersten Laden: INDEX.md öffnen, falls vorhanden.
- useEffect(() => {
- if (!active && files.length) {
- const index = files.find((f) => f.path.toLowerCase() === "index.md")
- setActive(index?.path ?? files[0].path)
- }
- }, [files, active])
-
- useEffect(() => {
- if (!active) return
- setDocLoading(true)
- api(`/api/wissen/datei?pfad=${encodeURIComponent(active)}`)
- .then(setDoc)
- .catch(() => setDoc({ path: active, content: "*(Notiz nicht ladbar)*", mtime: 0 }))
- .finally(() => setDocLoading(false))
- }, [active])
-
- const openWikiLink = (name: string) => {
- const path = byName.get(name.toLowerCase())
- if (path) setActive(path)
- }
-
- return (
-
-
-
-
- Wissen
-
-
- Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki.
-
-
- {files.length > 0 && (
-
-
-
-
- )}
-
-
- {data && !data.available && (
-
- Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar.
-
- )}
-
- {isLoading && (
-
- Vault wird geladen …
-
- )}
-
- {data?.available && files.length === 0 && (
-
- Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste.
-
- )}
-
- {showGraph ? (
-
- {graphLoading && (
-
-
- Graph wird berechnet …
-
-
- )}
-
- {graphData && (
- {
- if (id) {
- setActive(id)
- setShowGraph(false)
- }
- }}
- />
- )}
-
-
- ) : files.length > 0 && (
-
- {/* Datei-Liste */}
-
-
- {/* Inhalt */}
-
- {docLoading ? (
-
Notiz wird geladen …
- ) : doc ? (
- <>
-
- {doc.path}
- {doc.mtime > 0 && (
-
- Stand {new Date(doc.mtime * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })}
-
- )}
-
-
- >
- ) : null}
-
-
- )}
-
- )
-}
-
-// ── Leichtgewichtiger Markdown-Renderer (Zeilen-basiert, XSS-frei da reines JSX) ──
-function Markdown({ text, onWikiLink, known }: {
- text: string
- onWikiLink: (name: string) => void
- known: Map
-}) {
- const lines = text.split("\n")
- const out: React.ReactNode[] = []
- let list: React.ReactNode[] = []
- let code: string[] | null = null
-
- const flushList = (key: string) => {
- if (list.length) {
- out.push()
- list = []
- }
- }
-
- lines.forEach((raw, i) => {
- const key = `l${i}`
- if (code !== null) {
- if (raw.trimEnd() === "```") {
- out.push({code.join("\n")})
- code = null
- } else code.push(raw)
- return
- }
- if (raw.trimStart().startsWith("```")) { flushList(key); code = []; return }
-
- const line = raw.trimEnd()
- if (!line.trim()) { flushList(key); return }
-
- const h = line.match(/^(#{1,4})\s+(.*)$/)
- if (h) {
- flushList(key)
- const level = h[1].length
- const cls = level === 1 ? "text-lg font-bold mt-1 mb-3" : level === 2 ? "text-base font-bold mt-4 mb-2" : "text-sm font-bold mt-3 mb-1.5"
- out.push({inline(h[2], onWikiLink, known, key)}
)
- return
- }
- const li = line.match(/^\s*[-*•]\s+(.*)$/)
- if (li) {
- list.push({inline(li[1], onWikiLink, known, key)})
- return
- }
- flushList(key)
- out.push({inline(line, onWikiLink, known, key)}
)
- })
- flushList("end")
- if (code !== null) out.push({(code as string[]).join("\n")})
-
- return {out}
-}
-
-// Inline: [[wiki-links]], **fett**, *kursiv*, `code` — per Regex-Split, reines JSX (kein HTML-Inject).
-function inline(text: string, onWikiLink: (n: string) => void, known: Map, keyBase: string): React.ReactNode[] {
- const parts = text.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g)
- return parts.map((p, i) => {
- const key = `${keyBase}-${i}`
- const wiki = p.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/)
- if (wiki) {
- const target = wiki[1].trim()
- const label = (wiki[2] ?? wiki[1]).trim()
- const exists = known.has(target.toLowerCase())
- return exists ? (
-
- ) : (
- {label}
- )
- }
- if (p.startsWith("**") && p.endsWith("**")) return {p.slice(2, -2)}
- if (p.startsWith("*") && p.endsWith("*") && p.length > 2) return {p.slice(1, -1)}
- if (p.startsWith("`") && p.endsWith("`")) return {p.slice(1, -1)}
- return {p}
- })
-}