Go backend: - LLM client with DeepSeek/Kimi/Ollama/OpenAI support (OpenAI-compat) - Agent loader: AGENT.md frontmatter, SOUL.md, memory read/write - Skill system following agentskills.io standard - Room orchestration: master assign→execute→review loop with streaming - Hub: GitHub repo clone and team package install - Echo HTTP server with WebSocket and full REST API React frontend: - Discord-style 3-panel layout with Tailwind v4 - Zustand store with WebSocket streaming message handling - Chat view: streaming messages, role styles, right panel, drawer buttons - Agent MD editor with Monaco Editor (AGENT.md + SOUL.md) - Market page for GitHub team install/publish Docs: - plan.md with full progress tracking and next steps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
2.1 KiB
Go
96 lines
2.1 KiB
Go
package skill
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Meta struct {
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description"`
|
|
Path string `yaml:"-"`
|
|
}
|
|
|
|
type Skill struct {
|
|
Meta
|
|
Body string // full SKILL.md body (instructions)
|
|
}
|
|
|
|
// Discover scans skillsDir and returns metadata for all valid skills.
|
|
func Discover(skillsDir string) ([]Meta, error) {
|
|
entries, err := os.ReadDir(skillsDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var metas []Meta
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
path := filepath.Join(skillsDir, e.Name(), "SKILL.md")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
meta, err := parseMeta(data)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
meta.Path = filepath.Join(skillsDir, e.Name())
|
|
metas = append(metas, meta)
|
|
}
|
|
return metas, nil
|
|
}
|
|
|
|
// Load returns a fully loaded skill including body.
|
|
func Load(skillDir string) (*Skill, error) {
|
|
data, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
meta, err := parseMeta(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
meta.Path = skillDir
|
|
body := extractBody(data)
|
|
return &Skill{Meta: meta, Body: body}, nil
|
|
}
|
|
|
|
// ToXML generates <available_skills> XML for agent system prompts.
|
|
func ToXML(metas []Meta) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("<available_skills>\n")
|
|
for _, m := range metas {
|
|
fmt.Fprintf(&sb, " <skill>\n <name>%s</name>\n <description>%s</description>\n <location>%s/SKILL.md</location>\n </skill>\n",
|
|
m.Name, m.Description, m.Path)
|
|
}
|
|
sb.WriteString("</available_skills>")
|
|
return sb.String()
|
|
}
|
|
|
|
func parseMeta(data []byte) (Meta, error) {
|
|
var meta Meta
|
|
if !bytes.HasPrefix(data, []byte("---")) {
|
|
return meta, fmt.Errorf("missing frontmatter")
|
|
}
|
|
parts := bytes.SplitN(data, []byte("---"), 3)
|
|
if len(parts) < 3 {
|
|
return meta, fmt.Errorf("invalid frontmatter")
|
|
}
|
|
return meta, yaml.Unmarshal(parts[1], &meta)
|
|
}
|
|
|
|
func extractBody(data []byte) string {
|
|
parts := bytes.SplitN(data, []byte("---"), 3)
|
|
if len(parts) < 3 {
|
|
return string(data)
|
|
}
|
|
return strings.TrimSpace(string(parts[2]))
|
|
}
|