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:
agent
2026-06-07 11:50:13 +02:00
parent bb5b0e09b1
commit fc644ecd6e
308 changed files with 38829 additions and 474 deletions
+223
View File
@@ -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
}