package skill import ( "context" "sort" "sync" "time" ) // Memory is a zero-dependency in-process SkillStore — a light host or test gets // saved-skill persistence with no DB. Mort backs SkillStore with GORM/MySQL; // contrib/store adds durable SQLite. type Memory struct { mu sync.RWMutex skills map[string]*Skill // by ID versions map[string][]SkillVersion // by skill ID, append order byVerID map[string]SkillVersion // by version ID } // NewMemory returns an empty in-memory SkillStore. func NewMemory() *Memory { return &Memory{ skills: map[string]*Skill{}, versions: map[string][]SkillVersion{}, byVerID: map[string]SkillVersion{}, } } var _ SkillStore = (*Memory)(nil) func (m *Memory) Initialize(context.Context) error { return nil } func (m *Memory) Save(_ context.Context, s *Skill) error { m.mu.Lock() defer m.mu.Unlock() cp := *s m.skills[s.ID] = &cp return nil } func (m *Memory) Get(_ context.Context, id string) (*Skill, error) { m.mu.RLock() defer m.mu.RUnlock() s, ok := m.skills[id] if !ok { return nil, ErrNotFound } cp := *s return &cp, nil } func (m *Memory) GetByName(_ context.Context, ownerID, name string) (*Skill, error) { m.mu.RLock() defer m.mu.RUnlock() for _, s := range m.skills { if s.OwnerID == ownerID && s.Name == name { cp := *s return &cp, nil } } return nil, ErrNotFound } func (m *Memory) Delete(_ context.Context, id string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.skills, id) return nil } func (m *Memory) listWhere(keep func(*Skill) bool) []Skill { m.mu.RLock() defer m.mu.RUnlock() out := make([]Skill, 0, len(m.skills)) for _, s := range m.skills { if keep == nil || keep(s) { out = append(out, *s) } } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } func (m *Memory) ListByOwner(_ context.Context, ownerID string) ([]Skill, error) { return m.listWhere(func(s *Skill) bool { return s.OwnerID == ownerID }), nil } func (m *Memory) ListPublic(context.Context) ([]Skill, error) { return m.listWhere(func(s *Skill) bool { return s.Visibility == VisibilityPublic }), nil } func (m *Memory) ListSharedWith(_ context.Context, memberID string) ([]Skill, error) { return m.listWhere(func(s *Skill) bool { if s.Visibility != VisibilityShared { return false } for _, id := range s.SharedWith { if id == memberID { return true } } return false }), nil } func (m *Memory) ListBuiltinByName(_ context.Context, name string) (*Skill, error) { m.mu.RLock() defer m.mu.RUnlock() for _, s := range m.skills { if s.Source == SourceBuiltin && s.Name == name { cp := *s return &cp, nil } } return nil, ErrNotFound } func (m *Memory) ListChatbotExposed(context.Context) ([]Skill, error) { return m.listWhere(func(s *Skill) bool { return s.ExposeAsChatbotTool }), nil } func (m *Memory) ListDueScheduled(_ context.Context, now time.Time) ([]Skill, error) { return m.listWhere(func(s *Skill) bool { return s.DueAt(now) }), nil } func (m *Memory) MarkScheduledRun(_ context.Context, skillID string, ranAt, nextAt time.Time) error { m.mu.Lock() defer m.mu.Unlock() s, ok := m.skills[skillID] if !ok { return ErrNotFound } s.LastScheduledRunAt = ranAt s.NextRunAt = nextAt return nil } func (m *Memory) AppendVersion(_ context.Context, sv SkillVersion) error { m.mu.Lock() defer m.mu.Unlock() m.versions[sv.SkillID] = append(m.versions[sv.SkillID], sv) m.byVerID[sv.ID] = sv return nil } func (m *Memory) ListVersionsBySkill(_ context.Context, skillID string, limit int) ([]SkillVersion, error) { m.mu.RLock() defer m.mu.RUnlock() all := m.versions[skillID] // newest first out := make([]SkillVersion, 0, len(all)) for i := len(all) - 1; i >= 0; i-- { out = append(out, all[i]) if limit > 0 && len(out) >= limit { break } } return out, nil } func (m *Memory) GetVersionByID(_ context.Context, versionID string) (*SkillVersion, error) { m.mu.RLock() defer m.mu.RUnlock() sv, ok := m.byVerID[versionID] if !ok { return nil, ErrNotFound } return &sv, nil }