Files
ely-eye/ely-eye-mcp/internal/store/sqlite.go
T
ZacharyZhang-NY 4cd29f91e5 Add Ely-Eye MCP server
Local read-only Model Context Protocol server (Go) exposing the .ely_eye memory
store to coding agents such as Codex and Claude Code: nine tools over status,
cartridges, Evidence Atoms, compiled evidence packs, proof suites, and HashHop
proofs. One-line cross-platform install (no Docker), with a prebuilt-binary
release workflow.
2026-05-30 23:51:45 -04:00

137 lines
3.8 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"net/url"
"path/filepath"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/config"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
_ "modernc.org/sqlite"
)
type Store struct {
cfg config.Config
db *sql.DB
}
func Open(ctx context.Context, cfg config.Config) (*Store, error) {
dsn := sqliteReadOnlyDSN(cfg.Database)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
if err := db.PingContext(ctx); err != nil {
db.Close()
return nil, err
}
return &Store{cfg: cfg, db: db}, nil
}
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) Status(ctx context.Context) (domain.Status, error) {
atomCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM atoms")
if err != nil {
return domain.Status{}, err
}
sourceCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM sources")
if err != nil {
return domain.Status{}, err
}
cartridgeCount, err := s.scalarInt(ctx, "SELECT COUNT(*) FROM cartridges")
if err != nil {
return domain.Status{}, err
}
activeTokens, err := s.scalarInt64(ctx, "SELECT COALESCE(SUM(token_equivalent), 0) FROM atoms")
if err != nil {
return domain.Status{}, err
}
cartridgeTokens, err := s.scalarInt64(ctx, "SELECT COALESCE(SUM(token_equivalent), 0) FROM cartridges")
if err != nil {
return domain.Status{}, err
}
layers, err := s.CacheLayers(ctx)
if err != nil {
return domain.Status{}, err
}
libraryTokens := activeTokens
if cartridgeTokens > libraryTokens {
libraryTokens = cartridgeTokens
}
return domain.Status{
App: "Ely-Eye MCP",
ProjectRoot: s.cfg.ProjectRoot,
DataHome: s.cfg.ElyEyeHome,
Database: s.cfg.Database,
AtomCount: atomCount,
SourceCount: sourceCount,
CartridgeCount: cartridgeCount,
ActiveTokenEquivalent: activeTokens,
LibraryTokenEquivalent: libraryTokens,
CacheLayers: layers,
}, nil
}
func (s *Store) CacheLayers(ctx context.Context) ([]domain.CacheLayer, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT
layer,
COUNT(DISTINCT cache_key) AS entries,
COALESCE(SUM(bytes_estimate), 0) AS bytes_estimate,
SUM(CASE WHEN event = 'hit' THEN 1 ELSE 0 END) AS hit_count,
SUM(CASE WHEN event = 'miss' THEN 1 ELSE 0 END) AS miss_count,
COALESCE(MAX(ts), '') AS last_event_at
FROM cache_events
GROUP BY layer
ORDER BY layer`)
if err != nil {
return nil, err
}
defer rows.Close()
layers := []domain.CacheLayer{}
for rows.Next() {
var layer domain.CacheLayer
if err := rows.Scan(&layer.Layer, &layer.Entries, &layer.BytesEstimate, &layer.HitCount, &layer.MissCount, &layer.LastEventAt); err != nil {
return nil, err
}
layers = append(layers, layer)
}
return layers, rows.Err()
}
func (s *Store) scalarInt(ctx context.Context, query string) (int, error) {
var value int
if err := s.db.QueryRowContext(ctx, query).Scan(&value); err != nil {
return 0, err
}
return value, nil
}
func (s *Store) scalarInt64(ctx context.Context, query string) (int64, error) {
var value int64
if err := s.db.QueryRowContext(ctx, query).Scan(&value); err != nil {
return 0, err
}
return value, nil
}
func sqliteReadOnlyDSN(path string) string {
absolute, _ := filepath.Abs(path)
uri := "file:" + filepath.ToSlash(absolute)
query := url.Values{}
query.Set("mode", "ro")
query.Add("_pragma", "query_only(1)")
// The production database runs in WAL mode and is written by the Ely-Eye
// app concurrently. busy_timeout lets a reader wait through a checkpoint or
// a momentary write lock instead of failing with SQLITE_BUSY. immutable is
// deliberately not set because the database does change underneath us.
query.Add("_pragma", "busy_timeout(5000)")
return fmt.Sprintf("%s?%s", uri, query.Encode())
}