feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/enmanuel/agents/pkg/skills"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Loader descubre y carga skills desde un directorio base.
|
||||
type Loader struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
// NewLoader crea un nuevo Loader apuntando al directorio de skills.
|
||||
func NewLoader(basePath string) *Loader {
|
||||
return &Loader{basePath: basePath}
|
||||
}
|
||||
|
||||
// LoadMeta carga solo la metadata (nivel 1) de todas las skills.
|
||||
// Recorre el directorio base buscando SKILL.md y extrae el frontmatter YAML.
|
||||
func (l *Loader) LoadMeta() ([]skills.SkillMeta, error) {
|
||||
var metas []skills.SkillMeta
|
||||
|
||||
// Recorre categorias (devops/, analysis/, etc.)
|
||||
categories, err := os.ReadDir(l.basePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read skills dir: %w", err)
|
||||
}
|
||||
|
||||
for _, catEntry := range categories {
|
||||
if !catEntry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
category := catEntry.Name()
|
||||
catPath := filepath.Join(l.basePath, category)
|
||||
|
||||
// Recorre skills dentro de la categoria
|
||||
skillDirs, err := os.ReadDir(catPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, skillEntry := range skillDirs {
|
||||
if !skillEntry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
skillName := skillEntry.Name()
|
||||
skillPath := filepath.Join(catPath, skillName)
|
||||
skillMdPath := filepath.Join(skillPath, "SKILL.md")
|
||||
|
||||
// Verificar que existe SKILL.md
|
||||
if _, err := os.Stat(skillMdPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parsear metadata
|
||||
meta, _, err := parseSkillMD(skillMdPath)
|
||||
if err != nil {
|
||||
continue // skip invalid skills
|
||||
}
|
||||
|
||||
meta.Category = category
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
// LoadSkill carga una skill completa (nivel 2) por nombre.
|
||||
// Retorna el struct Skill con metadata, instrucciones y listado de recursos.
|
||||
func (l *Loader) LoadSkill(name string) (*skills.Skill, error) {
|
||||
// Buscar en todas las categorias
|
||||
categories, err := os.ReadDir(l.basePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read skills dir: %w", err)
|
||||
}
|
||||
|
||||
for _, catEntry := range categories {
|
||||
if !catEntry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
category := catEntry.Name()
|
||||
skillPath := filepath.Join(l.basePath, category, name)
|
||||
skillMdPath := filepath.Join(skillPath, "SKILL.md")
|
||||
|
||||
if _, err := os.Stat(skillMdPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parsear skill completa
|
||||
meta, instructions, err := parseSkillMD(skillMdPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", skillMdPath, err)
|
||||
}
|
||||
|
||||
meta.Category = category
|
||||
|
||||
skill := &skills.Skill{
|
||||
Meta: meta,
|
||||
Instructions: instructions,
|
||||
BasePath: skillPath,
|
||||
Scripts: listFiles(filepath.Join(skillPath, "scripts")),
|
||||
References: listFiles(filepath.Join(skillPath, "references")),
|
||||
Templates: listFiles(filepath.Join(skillPath, "templates")),
|
||||
Assets: listFiles(filepath.Join(skillPath, "assets")),
|
||||
}
|
||||
|
||||
return skill, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("skill not found: %s", name)
|
||||
}
|
||||
|
||||
// ReadResource lee un recurso especifico (nivel 3) de una skill.
|
||||
// path es relativo a la skill (ej: "scripts/deploy.sh", "references/api.md").
|
||||
func (l *Loader) ReadResource(skillName, resourcePath string) (string, error) {
|
||||
skill, err := l.LoadSkill(skillName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(skill.BasePath, resourcePath)
|
||||
|
||||
// Validar que el path esta dentro de la skill (evitar path traversal)
|
||||
absBasePath, err := filepath.Abs(skill.BasePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("abs base path: %w", err)
|
||||
}
|
||||
|
||||
absFullPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("abs resource path: %w", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(absFullPath, absBasePath) {
|
||||
return "", fmt.Errorf("path traversal detected: %s", resourcePath)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(absFullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read resource: %w", err)
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// parseSkillMD extrae el frontmatter YAML y el cuerpo markdown de un SKILL.md.
|
||||
func parseSkillMD(path string) (skills.SkillMeta, string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return skills.SkillMeta{}, "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
var yamlLines []string
|
||||
var bodyLines []string
|
||||
inYAML := false
|
||||
yamlClosed := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
if !inYAML {
|
||||
inYAML = true
|
||||
continue
|
||||
} else {
|
||||
inYAML = false
|
||||
yamlClosed = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if inYAML {
|
||||
yamlLines = append(yamlLines, line)
|
||||
} else if yamlClosed {
|
||||
bodyLines = append(bodyLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return skills.SkillMeta{}, "", err
|
||||
}
|
||||
|
||||
// Parse YAML frontmatter
|
||||
var meta skills.SkillMeta
|
||||
yamlStr := strings.Join(yamlLines, "\n")
|
||||
if err := yaml.Unmarshal([]byte(yamlStr), &meta); err != nil {
|
||||
return skills.SkillMeta{}, "", fmt.Errorf("parse yaml: %w", err)
|
||||
}
|
||||
|
||||
// Cuerpo markdown
|
||||
body := strings.Join(bodyLines, "\n")
|
||||
|
||||
return meta, body, nil
|
||||
}
|
||||
|
||||
// listFiles retorna una lista de archivos (rutas relativas) dentro de un directorio.
|
||||
// Si el directorio no existe, retorna una lista vacia.
|
||||
func listFiles(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
Reference in New Issue
Block a user