Files
Augustin 4523bbd42c
All checks were successful
Stable Release / stable (push) Successful in 1m34s
feat: RAG, memory, plugins, lessons, file editor, split panes, Markdown rendering, PWA + UI overhaul
Major additions:
- RAG pipeline (indexing, chunking, search) with sidebar upload button
- Memory system with CRUD API
- Plugins and lessons modules
- MCP discovery and MCP server
- Advanced skills (auto-create, conditional, improver)
- Agent browser/image support, delegate, sessions
- File editor with CodeMirror in split panes
- Markdown rendering via react-markdown + KaTeX + highlight.js
- Raw markdown toggle
- PWA manifest + service worker
- Extension UI redesign with new design tokens and studio-style chat
- Pipeline API for chat streaming
- Mobile responsive layout

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-27 21:04:11 +02:00

277 lines
6.2 KiB
Go

package memory
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"sync"
"time"
_ "modernc.org/sqlite"
)
type MemoryType string
const (
TypePreference MemoryType = "preference"
TypeContext MemoryType = "context"
TypeSummary MemoryType = "summary"
TypeFact MemoryType = "fact"
TypePattern MemoryType = "pattern"
)
type Memory struct {
ID string `json:"id"`
Type MemoryType `json:"type"`
Key string `json:"key"`
Content string `json:"content"`
Tags string `json:"tags,omitempty"`
Source string `json:"source,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
AccessCount int `json:"access_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Store struct {
db *sql.DB
path string
mu sync.RWMutex
}
func NewStore() (*Store, error) {
dbPath, err := dbPath()
if err != nil {
return nil, fmt.Errorf("get db path: %w", err)
}
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
return nil, fmt.Errorf("create memory dir: %w", err)
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("open memory db: %w", err)
}
db.SetMaxOpenConns(1)
s := &Store{db: db, path: dbPath}
if err := s.migrate(); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return s, nil
}
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) Store(m *Memory) error {
s.mu.Lock()
defer s.mu.Unlock()
if m.ID == "" {
m.ID = generateID()
}
now := time.Now()
if m.CreatedAt.IsZero() {
m.CreatedAt = now
}
m.UpdatedAt = now
_, err := s.db.Exec(`
INSERT INTO memories (id, type, key, content, tags, source, confidence, access_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
type = excluded.type,
key = excluded.key,
content = excluded.content,
tags = excluded.tags,
source = excluded.source,
confidence = excluded.confidence,
access_count = excluded.access_count,
updated_at = excluded.updated_at
`, m.ID, string(m.Type), m.Key, m.Content, m.Tags, m.Source, m.Confidence, m.AccessCount, m.CreatedAt, m.UpdatedAt)
return err
}
func (s *Store) Get(id string) (*Memory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m := &Memory{}
err := s.db.QueryRow(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories WHERE id = ?
`, id).Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt)
if err == nil {
s.incrementAccess(id)
}
return m, err
}
func (s *Store) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`DELETE FROM memories WHERE id = ?`, id)
return err
}
func (s *Store) List(memType MemoryType, limit, offset int) ([]Memory, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 {
limit = 50
}
if limit > 200 {
limit = 200
}
var rows *sql.Rows
var err error
if memType != "" {
rows, err = s.db.Query(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories WHERE type = ?
ORDER BY updated_at DESC LIMIT ? OFFSET ?
`, string(memType), limit, offset)
} else {
rows, err = s.db.Query(`
SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at
FROM memories ORDER BY updated_at DESC LIMIT ? OFFSET ?
`, limit, offset)
}
if err != nil {
return nil, err
}
defer rows.Close()
return scanMemories(rows)
}
func (s *Store) Count() (int, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var count int
err := s.db.QueryRow(`SELECT COUNT(*) FROM memories`).Scan(&count)
return count, err
}
func (s *Store) incrementAccess(id string) {
go func() {
s.db.Exec(`UPDATE memories SET access_count = access_count + 1 WHERE id = ?`, id)
}()
}
func (s *Store) migrate() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
key TEXT NOT NULL,
content TEXT NOT NULL,
tags TEXT DEFAULT '',
source TEXT DEFAULT '',
confidence REAL DEFAULT 0.5,
access_count INTEGER DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
)
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE INDEX IF NOT EXISTS idx_memories_key ON memories(key)
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
key, content, tags,
content=memories,
content_rowid=rowid
)
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
INSERT INTO memories_fts(rowid, key, content, tags)
VALUES (new.rowid, new.key, new.content, new.tags);
END
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, key, content, tags)
VALUES ('delete', old.rowid, old.key, old.content, old.tags);
END
`)
if err != nil {
return err
}
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, key, content, tags)
VALUES ('delete', old.rowid, old.key, old.content, old.tags);
INSERT INTO memories_fts(rowid, key, content, tags)
VALUES (new.rowid, new.key, new.content, new.tags);
END
`)
return err
}
func scanMemories(rows *sql.Rows) ([]Memory, error) {
var memories []Memory
for rows.Next() {
var m Memory
err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt)
if err != nil {
return memories, err
}
memories = append(memories, m)
}
return memories, nil
}
func dbPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".muyue", "memory", "memories.db"), nil
}
func generateID() string {
return fmt.Sprintf("mem_%d", time.Now().UnixNano())
}