mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-20 02:17:24 +00:00
- Add skill edit dialog (name, description, visibility, tags) - Add drag-and-drop support to upload modal with visual feedback - Fix frontmatter not stripped in skill detail view (CRLF normalization) - Include visibility, tags, version in skill list/detail API responses - Change default skill upload visibility from private to internal - Add visibility column and version display to skills table
164 lines
4.7 KiB
Go
164 lines
4.7 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// SkillsMethods handles skills.list, skills.get, skills.update.
|
|
type SkillsMethods struct {
|
|
store store.SkillStore
|
|
}
|
|
|
|
func NewSkillsMethods(s store.SkillStore) *SkillsMethods {
|
|
return &SkillsMethods{store: s}
|
|
}
|
|
|
|
func (m *SkillsMethods) Register(router *gateway.MethodRouter) {
|
|
router.Register(protocol.MethodSkillsList, m.handleList)
|
|
router.Register(protocol.MethodSkillsGet, m.handleGet)
|
|
router.Register(protocol.MethodSkillsUpdate, m.handleUpdate)
|
|
}
|
|
|
|
func (m *SkillsMethods) handleList(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
allSkills := m.store.ListSkills()
|
|
|
|
result := make([]map[string]interface{}, 0, len(allSkills))
|
|
for _, s := range allSkills {
|
|
entry := map[string]interface{}{
|
|
"name": s.Name,
|
|
"slug": s.Slug,
|
|
"description": s.Description,
|
|
"source": s.Source,
|
|
"version": s.Version,
|
|
}
|
|
if s.ID != "" {
|
|
entry["id"] = s.ID
|
|
}
|
|
if s.Visibility != "" {
|
|
entry["visibility"] = s.Visibility
|
|
}
|
|
if len(s.Tags) > 0 {
|
|
entry["tags"] = s.Tags
|
|
}
|
|
result = append(result, entry)
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]interface{}{
|
|
"skills": result,
|
|
}))
|
|
}
|
|
|
|
func (m *SkillsMethods) handleGet(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
var params struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
if params.Name == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name is required"))
|
|
return
|
|
}
|
|
|
|
info, ok := m.store.GetSkill(params.Name)
|
|
if !ok {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skill not found: "+params.Name))
|
|
return
|
|
}
|
|
|
|
content, _ := m.store.LoadSkill(params.Name)
|
|
|
|
resp := map[string]interface{}{
|
|
"name": info.Name,
|
|
"slug": info.Slug,
|
|
"description": info.Description,
|
|
"source": info.Source,
|
|
"content": content,
|
|
"version": info.Version,
|
|
}
|
|
if info.ID != "" {
|
|
resp["id"] = info.ID
|
|
}
|
|
if info.Visibility != "" {
|
|
resp["visibility"] = info.Visibility
|
|
}
|
|
if len(info.Tags) > 0 {
|
|
resp["tags"] = info.Tags
|
|
}
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, resp))
|
|
}
|
|
|
|
// skillUpdater is an optional interface for stores that support skill updates (e.g. PGSkillStore).
|
|
type skillUpdater interface {
|
|
UpdateSkill(id uuid.UUID, updates map[string]interface{}) error
|
|
}
|
|
|
|
func (m *SkillsMethods) handleUpdate(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
var params struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
Updates map[string]interface{} `json:"updates"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
if params.Name == "" && params.ID == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "name or id is required"))
|
|
return
|
|
}
|
|
|
|
// Check if the store supports updates (PGSkillStore does, FileSkillStore doesn't)
|
|
updater, ok := m.store.(skillUpdater)
|
|
if !ok {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skills.update not supported for file-based skills"))
|
|
return
|
|
}
|
|
|
|
// Resolve skill ID
|
|
var skillID uuid.UUID
|
|
if params.ID != "" {
|
|
parsed, err := uuid.Parse(params.ID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid skill ID"))
|
|
return
|
|
}
|
|
skillID = parsed
|
|
} else {
|
|
// Look up by name — use GetSkill which returns path info, but we need DB ID
|
|
// For PGSkillStore, the name is the slug
|
|
info, exists := m.store.GetSkill(params.Name)
|
|
if !exists {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, "skill not found: "+params.Name))
|
|
return
|
|
}
|
|
// Try to parse Path as UUID (PGSkillStore stores DB ID in Path field for managed skills)
|
|
parsed, err := uuid.Parse(info.Path)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "cannot resolve skill ID for file-based skill"))
|
|
return
|
|
}
|
|
skillID = parsed
|
|
}
|
|
|
|
if params.Updates == nil || len(params.Updates) == 0 {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "updates is required"))
|
|
return
|
|
}
|
|
|
|
if err := updater.UpdateSkill(skillID, params.Updates); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
|
|
return
|
|
}
|
|
|
|
m.store.BumpVersion()
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]string{"ok": "true"}))
|
|
}
|