// Package paketordner packt Paketordner für den Server und legt neue an // (→ KONZEPT.md 6.1, 11.1: hugin new, validate, try, upload). package pkgdir import ( "archive/zip" "bytes" "errors" "fmt" "io/fs" "os" "path/filepath" "regexp" "strings" ) const Manifest = "hugin.yaml" // Zip packt den Ordner wie der Server ihn erwartet: hugin.yaml in der Wurzel, // versteckte Dateien weggelassen, Symlinks abgelehnt. func Zip(folder string) ([]byte, error) { if _, err := os.Stat(filepath.Join(folder, Manifest)); err != nil { return nil, fmt.Errorf("%s fehlt in %s", Manifest, folder) } var buffer bytes.Buffer z := zip.NewWriter(&buffer) err := filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(folder, path) if err != nil || rel == "." { return err } if strings.HasPrefix(d.Name(), ".") { if d.IsDir() { return filepath.SkipDir } return nil } if d.Type()&fs.ModeSymlink != 0 { return fmt.Errorf("Symlinks sind nicht erlaubt: %s", rel) } if d.IsDir() { return nil } data, err := os.ReadFile(path) if err != nil { return err } w, err := z.Create(filepath.ToSlash(rel)) if err != nil { return err } _, err = w.Write(data) return err }) if err != nil { return nil, err } if err := z.Close(); err != nil { return nil, err } return buffer.Bytes(), nil } var idPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) var contentFile = map[string]string{ "skill": "SKILL.md", "rules": "RULES.md", "command": "COMMAND.md", "subagent": "AGENT.md", "mcp": "", "plugin": "index.ts", } var scaffolds = map[string]string{ "skill": `# Titel Was der Agent tun soll, Schritt für Schritt. Werkzeuge neutral beschreiben („lies die Datei“, nicht „nutze Read“), damit es in Claude Code und OpenCode passt. `, "rules": `# Regeln - Eine Regel pro Zeile, kurz und prüfbar. `, "command": `Beschreibe die Aufgabe für {{input}}. Platzhalter: {{name}} für benannte Argumente (in hugin.yaml unter arguments), $ARGUMENTS für alles zusammen. Keine $1, $2 …: Claude Code und OpenCode zählen verschieden. `, "subagent": `Du bist … Beschreibe Rolle, Vorgehen und Form der Antwort. `, "plugin": `import { tool } from "@opencode-ai/plugin" // OpenCode ruft jede exportierte Funktion beim Start auf. Erlaubte Importe: relative // Dateien im Paket, node:…, bun und @opencode-ai/plugin; npm-Pakete noch nicht. export const MeinPlugin = async ({ worktree }) => { return { tool: { mein_werkzeug: tool({ description: "Was das Werkzeug tut", args: {}, async execute() { return "Ergebnis" }, }), }, } } `, } // New legt folder/ mit Manifest-Gerüst und Inhaltsdatei an. func New(folder, kind, id string) (string, error) { file, ok := contentFile[kind] if !ok { return "", fmt.Errorf("kind %q: erwartet skill, rules, command, subagent, mcp oder plugin", kind) } if !idPattern.MatchString(id) || len(id) > 64 { return "", fmt.Errorf("id %q: nur a–z, 0–9 und einzelne Bindestriche, 1–64 Zeichen", id) } target := filepath.Join(folder, id) if _, err := os.Stat(target); err == nil { return "", fmt.Errorf("%s gibt es schon", target) } else if !errors.Is(err, fs.ErrNotExist) { return "", err } if err := os.MkdirAll(target, 0o755); err != nil { return "", err } if err := os.WriteFile(filepath.Join(target, Manifest), []byte(manifest(kind, id)), 0o644); err != nil { return "", err } if file != "" { if err := os.WriteFile(filepath.Join(target, file), []byte(scaffolds[kind]), 0o644); err != nil { return "", err } } return target, nil } func manifest(kind, id string) string { scope := "tenant # public | tenant | project; das Präfix der id muss passen" if strings.HasPrefix(id, "pub-") { scope = "public # public | tenant | project" } var b strings.Builder fmt.Fprintf(&b, "id: %s\nkind: %s\nversion: 0.1.0\nowner: \"\" # Projekt-Slug\nscope: %s\n", id, kind, scope) b.WriteString("description: >\n Wann und wofür der Agent das nutzt. Die Werkzeuge wählen danach aus,\n also konkret schreiben (1–1.024 Zeichen).\n") switch kind { case "command": b.WriteString("arguments: [input] # Reihenfolge = Position beim Aufruf\n") case "subagent": b.WriteString("capability: read # read | write | shell | web, auch als Liste\n") b.WriteString("model: standard # fast | standard | thorough | inherit\n") case "plugin": b.WriteString("targets: [opencode] # Plugins gibt es nur für OpenCode\n") case "mcp": b.WriteString("server:\n name: name\n url: https://…/mcp\n headers:\n Authorization: \"Bearer {{env:NAME_KEY}}\"\n") b.WriteString(" # Lokaler Server statt url und headers: Code (und requirements.txt mit Hashes) ins Paket\n" + " # local:\n # runtime: python\n # entry: mein_server # Modul oder Skript.py\n" + " # python: \"3.10\"\n # env: [NAME_KEY]\n") } b.WriteString("# extra: # nur, was ein Werkzeug allein kann\n# claude: {}\n# opencode: {}\n") return b.String() }