Files
ely-eye/ely-eye-mcp/internal/config/config.go
T
ZacharyZhang-NY 3d792417cf Give the write path a CLI and an MCP face
remember / supersede / forget now reach the one Python writer from a
terminal, from MCP, and from HTTP. The server, not the caller, decides
who owns a memory.

- cli: remember, supersede, forget. --holder falls back to
  settings.default_holder until the holder registry lands. Timestamps are
  parsed once with one clear message.
- ingestion: import pymupdf as fitz. The deprecated fitz import printed a
  warning to stdout, so every CLI command's JSON answer was unparseable.
- mcp: remember, supersede_memory and forget_memory forward to the Python
  writer over HTTP (ELY_EYE_API, default http://127.0.0.1:8765). SQLite
  stays read-only to the Go side, so the write rules live in one place.
- mcp reads follow schema v2: atoms.time became ingested_at, and reads go
  through v_atoms_current, so a superseded memory stops being findable.
  holder, origin, scenario and event_time now show up in fetch_atom and
  search_atoms.
- scripts/mcp_write_smoke.sh proves the loop end to end: remember, search,
  supersede, forget, then not found.

Evidence: 92 pytest passed; go build, go vet and go test clean; mypy clean
on the 10 touched source files with a fresh cache; ruff clean except a
pre-existing B008 at cli.py:292, identical at HEAD.
2026-10-07 15:09:17 -04:00

99 lines
2.4 KiB
Go

package config
import (
"errors"
"os"
"path/filepath"
)
type Config struct {
ProjectRoot string `json:"project_root"`
ElyEyeHome string `json:"ely_eye_home"`
Database string `json:"database"`
Binary string `json:"binary"`
APIBaseURL string `json:"api_base_url"`
}
// Resolve locates the Ely-Eye data home and its SQLite database. Reads mirror
// any local .ely_eye store directly, so identity is anchored on the database
// itself rather than on Ely-Eye repository layout. This lets the MCP server be
// deployed for any project that owns an .ely_eye/data/ely_eye.sqlite. Writes go
// through the Ely-Eye API, which owns the memory rules.
func Resolve(projectRoot string, elyEyeHome string, binary string) (Config, error) {
home, err := resolveHome(projectRoot, elyEyeHome)
if err != nil {
return Config{}, err
}
db := filepath.Join(home, "data", "ely_eye.sqlite")
if _, err := os.Stat(db); err != nil {
return Config{}, errors.New("Ely-Eye database is required at " + db)
}
root := projectRoot
if root == "" {
root = filepath.Dir(home)
}
root, err = filepath.Abs(root)
if err != nil {
return Config{}, err
}
if binary == "" {
binary, _ = os.Executable()
}
if binary != "" {
binary, err = filepath.Abs(binary)
if err != nil {
return Config{}, err
}
}
return Config{
ProjectRoot: root,
ElyEyeHome: home,
Database: db,
Binary: binary,
APIBaseURL: apiBaseURL(),
}, nil
}
// apiBaseURL points at the Ely-Eye API that owns every write rule.
func apiBaseURL() string {
if fromEnv := os.Getenv("ELY_EYE_API"); fromEnv != "" {
return fromEnv
}
return "http://127.0.0.1:8765"
}
func resolveHome(projectRoot string, elyEyeHome string) (string, error) {
if elyEyeHome == "" {
elyEyeHome = os.Getenv("ELY_EYE_HOME")
}
if elyEyeHome != "" {
return filepath.Abs(elyEyeHome)
}
if projectRoot != "" {
abs, err := filepath.Abs(projectRoot)
if err != nil {
return "", err
}
return filepath.Join(abs, ".ely_eye"), nil
}
return discoverHome()
}
func discoverHome() (string, error) {
current, err := os.Getwd()
if err != nil {
return "", err
}
for {
home := filepath.Join(current, ".ely_eye")
if _, err := os.Stat(filepath.Join(home, "data", "ely_eye.sqlite")); err == nil {
return home, nil
}
next := filepath.Dir(current)
if next == current {
return "", errors.New("cannot find an Ely-Eye data home; pass --ely-eye-home or set ELY_EYE_HOME")
}
current = next
}
}