diff --git a/brain_direct.go b/brain_direct.go new file mode 100644 index 0000000..086185b --- /dev/null +++ b/brain_direct.go @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + coreerr "dappco.re/go/core/log" + coremcp "forge.lthn.ai/core/mcp/pkg/mcp" + brainpkg "forge.lthn.ai/core/mcp/pkg/mcp/brain" + _ "github.com/marcboeker/go-duckdb" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +const ( + defaultBrainAPIURL = "https://api.lthn.sh" + defaultBrainCacheTTL = 5 * time.Minute + defaultBrainCacheDriver = "duckdb" +) + +type ( + RememberInput = brainpkg.RememberInput + RememberOutput = brainpkg.RememberOutput + RecallInput = brainpkg.RecallInput + RecallFilter = brainpkg.RecallFilter + RecallOutput = brainpkg.RecallOutput + Memory = brainpkg.Memory + ForgetInput = brainpkg.ForgetInput + ForgetOutput = brainpkg.ForgetOutput +) + +// BrainDirectSubsystem implements the OpenBrain MCP tools over the direct HTTP API. +// brain_recall is cached locally in DuckDB to avoid repeated semantic searches. +type BrainDirectSubsystem struct { + workspaceRoot string + apiURL string + apiKey string + client *http.Client + + cache *brainRecallCache +} + +var _ coremcp.Subsystem = (*BrainDirectSubsystem)(nil) +var _ coremcp.SubsystemWithShutdown = (*BrainDirectSubsystem)(nil) + +// NewCachedBrainDirect creates the direct OpenBrain subsystem with a DuckDB-backed +// cache for brain_recall responses. +func NewCachedBrainDirect(workspaceRoot string) (*BrainDirectSubsystem, error) { + apiURL := strings.TrimSpace(os.Getenv("CORE_BRAIN_URL")) + if apiURL == "" { + apiURL = defaultBrainAPIURL + } + + apiKey := strings.TrimSpace(os.Getenv("CORE_BRAIN_KEY")) + if apiKey == "" { + if home, err := os.UserHomeDir(); err == nil { + keyPath := filepath.Join(home, ".claude", "brain.key") + if data, readErr := os.ReadFile(keyPath); readErr == nil { + apiKey = strings.TrimSpace(string(data)) + } + } + } + + cacheTTL := defaultBrainCacheTTL + if raw := strings.TrimSpace(os.Getenv("CORE_BRAIN_RECALL_CACHE_TTL")); raw != "" { + if parsed, err := time.ParseDuration(raw); err == nil && parsed > 0 { + cacheTTL = parsed + } + } + + cache, err := newBrainRecallCache(workspaceRoot, apiURL, apiKey, cacheTTL) + if err != nil { + return nil, err + } + + return &BrainDirectSubsystem{ + workspaceRoot: workspaceRoot, + apiURL: apiURL, + apiKey: apiKey, + client: &http.Client{Timeout: 30 * time.Second}, + cache: cache, + }, nil +} + +// Name implements mcp.Subsystem. +func (s *BrainDirectSubsystem) Name() string { return "brain" } + +// RegisterTools implements mcp.Subsystem. +func (s *BrainDirectSubsystem) RegisterTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{ + Name: "brain_remember", + Description: "Store a memory in OpenBrain. Types: fact, decision, observation, plan, convention, architecture, research, documentation, service, bug, pattern, context, procedure.", + }, s.brainRemember) + + mcp.AddTool(server, &mcp.Tool{ + Name: "brain_recall", + Description: "Semantic search across OpenBrain memories. Returns memories ranked by similarity. Use agent_id 'cladius' for Cladius's memories.", + }, s.brainRecall) + + mcp.AddTool(server, &mcp.Tool{ + Name: "brain_forget", + Description: "Remove a memory from OpenBrain by ID.", + }, s.brainForget) +} + +// Shutdown implements mcp.SubsystemWithShutdown. +func (s *BrainDirectSubsystem) Shutdown(ctx context.Context) error { + if s.cache != nil { + return s.cache.Close() + } + return nil +} + +func (s *BrainDirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) { + if s.apiKey == "" { + return nil, coreerr.E("brain.apiCall", "no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)", nil) + } + + var reqBody io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return nil, coreerr.E("brain.apiCall", "marshal request", err) + } + reqBody = bytes.NewReader(data) + } + + req, err := http.NewRequestWithContext(ctx, method, s.apiURL+path, reqBody) + if err != nil { + return nil, coreerr.E("brain.apiCall", "create request", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+s.apiKey) + + resp, err := s.client.Do(req) + if err != nil { + return nil, coreerr.E("brain.apiCall", "API call failed", err) + } + defer resp.Body.Close() + + respData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, coreerr.E("brain.apiCall", "read response", err) + } + + if resp.StatusCode >= 400 { + return nil, coreerr.E("brain.apiCall", "API returned "+string(respData), nil) + } + + var result map[string]any + if err := json.Unmarshal(respData, &result); err != nil { + return nil, coreerr.E("brain.apiCall", "parse response", err) + } + + return result, nil +} + +func (s *BrainDirectSubsystem) brainRemember(ctx context.Context, _ *mcp.CallToolRequest, input RememberInput) (*mcp.CallToolResult, RememberOutput, error) { + result, err := s.apiCall(ctx, "POST", "/v1/brain/remember", map[string]any{ + "content": input.Content, + "type": input.Type, + "tags": input.Tags, + "project": input.Project, + "agent_id": "cladius", + }) + if err != nil { + return nil, RememberOutput{}, err + } + + if s.cache != nil { + _ = s.cache.clear(ctx) + } + + id, _ := result["id"].(string) + return nil, RememberOutput{ + Success: true, + MemoryID: id, + Timestamp: time.Now(), + }, nil +} + +func (s *BrainDirectSubsystem) brainRecall(ctx context.Context, _ *mcp.CallToolRequest, input RecallInput) (*mcp.CallToolResult, RecallOutput, error) { + request := s.normalisedRecallRequest(input) + cacheKey, err := s.cacheKey(request) + if err != nil { + return nil, RecallOutput{}, err + } + + if s.cache != nil { + if cached, ok, err := s.cache.get(ctx, cacheKey); err != nil { + return nil, RecallOutput{}, err + } else if ok { + return nil, cached, nil + } + } + + body := map[string]any{ + "query": request.Query, + "top_k": request.TopK, + "agent_id": request.AgentID, + } + if request.Project != "" { + body["project"] = request.Project + } + if request.Type != nil { + body["type"] = request.Type + } + + result, err := s.apiCall(ctx, "POST", "/v1/brain/recall", body) + if err != nil { + return nil, RecallOutput{}, err + } + + output := brainRecallResult(result) + if s.cache != nil { + _ = s.cache.set(ctx, cacheKey, output) + } + + return nil, output, nil +} + +func (s *BrainDirectSubsystem) brainForget(ctx context.Context, _ *mcp.CallToolRequest, input ForgetInput) (*mcp.CallToolResult, ForgetOutput, error) { + _, err := s.apiCall(ctx, "DELETE", "/v1/brain/forget/"+input.ID, nil) + if err != nil { + return nil, ForgetOutput{}, err + } + + if s.cache != nil { + _ = s.cache.clear(ctx) + } + + return nil, ForgetOutput{ + Success: true, + Forgotten: input.ID, + Timestamp: time.Now(), + }, nil +} + +type brainRecallRequest struct { + WorkspaceRoot string `json:"workspace_root"` + APIURL string `json:"api_url"` + APIKeyHash string `json:"api_key_hash"` + Query string `json:"query"` + TopK int `json:"top_k"` + AgentID string `json:"agent_id"` + Project string `json:"project,omitempty"` + Type any `json:"type,omitempty"` +} + +func (s *BrainDirectSubsystem) normalisedRecallRequest(input RecallInput) brainRecallRequest { + topK := input.TopK + if topK <= 0 { + topK = 10 + } + + return brainRecallRequest{ + WorkspaceRoot: strings.TrimSpace(s.workspaceRoot), + APIURL: strings.TrimSpace(s.apiURL), + APIKeyHash: hashString(s.apiKey), + Query: strings.TrimSpace(input.Query), + TopK: topK, + AgentID: "cladius", + Project: strings.TrimSpace(input.Filter.Project), + Type: input.Filter.Type, + } +} + +func (s *BrainDirectSubsystem) cacheKey(request brainRecallRequest) (string, error) { + data, err := json.Marshal(request) + if err != nil { + return "", coreerr.E("brain.cacheKey", "marshal recall request", err) + } + + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]), nil +} + +func brainRecallResult(result map[string]any) RecallOutput { + var memories []Memory + if mems, ok := result["memories"].([]any); ok { + for _, m := range mems { + mm, ok := m.(map[string]any) + if !ok { + continue + } + mem := Memory{ + Content: fmt.Sprintf("%v", mm["content"]), + Type: fmt.Sprintf("%v", mm["type"]), + Project: fmt.Sprintf("%v", mm["project"]), + AgentID: fmt.Sprintf("%v", mm["agent_id"]), + CreatedAt: fmt.Sprintf("%v", mm["created_at"]), + } + if id, ok := mm["id"].(string); ok { + mem.ID = id + } + if score, ok := mm["score"].(float64); ok { + mem.Confidence = score + } + if source, ok := mm["source"].(string); ok { + mem.Tags = append(mem.Tags, "source:"+source) + } + memories = append(memories, mem) + } + } + + return RecallOutput{ + Success: true, + Count: len(memories), + Memories: memories, + } +} + +func hashString(value string) string { + sum := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sum[:]) +} + +type brainRecallCache struct { + db *sql.DB + ttl time.Duration + path string +} + +func newBrainRecallCache(workspaceRoot, apiURL, apiKey string, ttl time.Duration) (*brainRecallCache, error) { + cachePath, err := brainRecallCachePath(workspaceRoot, apiURL, apiKey) + if err != nil { + return nil, err + } + + db, err := sql.Open(defaultBrainCacheDriver, cachePath) + if err != nil { + return nil, coreerr.E("brain.cache.open", "open duckdb cache", err) + } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + schema := ` +CREATE TABLE IF NOT EXISTS brain_recall_cache ( + cache_key TEXT PRIMARY KEY, + payload TEXT NOT NULL, + created_at_unix_ms BIGINT NOT NULL, + expires_at_unix_ms BIGINT NOT NULL +); +` + if _, err := db.Exec(schema); err != nil { + _ = db.Close() + return nil, coreerr.E("brain.cache.schema", "initialise duckdb cache", err) + } + + return &brainRecallCache{ + db: db, + ttl: ttl, + path: cachePath, + }, nil +} + +func brainRecallCachePath(workspaceRoot, apiURL, apiKey string) (string, error) { + baseDir, err := os.UserCacheDir() + if err != nil || baseDir == "" { + baseDir = os.TempDir() + } + + identity := strings.Join([]string{ + strings.TrimSpace(workspaceRoot), + strings.TrimSpace(apiURL), + hashString(apiKey), + }, "\x00") + + sum := sha256.Sum256([]byte(identity)) + fileName := "brain-recall-" + hex.EncodeToString(sum[:]) + ".duckdb" + cacheDir := filepath.Join(baseDir, "core-ide", "brain") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return "", coreerr.E("brain.cache.path", "create cache directory", err) + } + + return filepath.Join(cacheDir, fileName), nil +} + +func (c *brainRecallCache) Close() error { + if c == nil || c.db == nil { + return nil + } + return c.db.Close() +} + +func (c *brainRecallCache) get(ctx context.Context, key string) (RecallOutput, bool, error) { + if c == nil || c.db == nil { + return RecallOutput{}, false, nil + } + + var payload string + var expires int64 + err := c.db.QueryRowContext(ctx, `SELECT payload, expires_at_unix_ms FROM brain_recall_cache WHERE cache_key = ?`, key).Scan(&payload, &expires) + if err == sql.ErrNoRows { + return RecallOutput{}, false, nil + } + if err != nil { + return RecallOutput{}, false, coreerr.E("brain.cache.get", "read recall cache", err) + } + + if time.Now().UTC().UnixMilli() >= expires { + _, _ = c.db.ExecContext(ctx, `DELETE FROM brain_recall_cache WHERE cache_key = ?`, key) + return RecallOutput{}, false, nil + } + + var out RecallOutput + if err := json.Unmarshal([]byte(payload), &out); err != nil { + return RecallOutput{}, false, coreerr.E("brain.cache.get", "decode cached recall output", err) + } + + return out, true, nil +} + +func (c *brainRecallCache) set(ctx context.Context, key string, value RecallOutput) error { + if c == nil || c.db == nil { + return nil + } + + payload, err := json.Marshal(value) + if err != nil { + return coreerr.E("brain.cache.set", "encode recall output", err) + } + + now := time.Now().UTC().UnixMilli() + expires := now + int64(c.ttl/time.Millisecond) + if expires <= now { + expires = now + 1 + } + + _, err = c.db.ExecContext(ctx, ` +INSERT INTO brain_recall_cache (cache_key, payload, created_at_unix_ms, expires_at_unix_ms) +VALUES (?, ?, ?, ?) +ON CONFLICT(cache_key) DO UPDATE SET + payload = excluded.payload, + created_at_unix_ms = excluded.created_at_unix_ms, + expires_at_unix_ms = excluded.expires_at_unix_ms +`, key, string(payload), now, expires) + if err != nil { + return coreerr.E("brain.cache.set", "store recall cache", err) + } + + return nil +} + +func (c *brainRecallCache) clear(ctx context.Context) error { + if c == nil || c.db == nil { + return nil + } + + if _, err := c.db.ExecContext(ctx, `DELETE FROM brain_recall_cache`); err != nil { + return coreerr.E("brain.cache.clear", "clear recall cache", err) + } + return nil +} diff --git a/brain_direct_test.go b/brain_direct_test.go new file mode 100644 index 0000000..f5c6bff --- /dev/null +++ b/brain_direct_test.go @@ -0,0 +1,148 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBrainRecallCache_Good_StoresAndExpires(t *testing.T) { + cache, err := newBrainRecallCache(t.TempDir(), "https://example.com", "secret", 25*time.Millisecond) + require.NoError(t, err) + defer func() { + require.NoError(t, cache.Close()) + }() + + want := RecallOutput{ + Success: true, + Count: 1, + Memories: []Memory{ + {ID: "m-1", Content: "remember this", CreatedAt: "2026-03-31T00:00:00Z"}, + }, + } + + require.NoError(t, cache.set(context.Background(), "cache-key", want)) + + got, ok, err := cache.get(context.Background(), "cache-key") + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, want, got) + + time.Sleep(50 * time.Millisecond) + + got, ok, err = cache.get(context.Background(), "cache-key") + require.NoError(t, err) + assert.False(t, ok) + assert.Empty(t, got) +} + +func TestBrainDirectSubsystem_RecallCachesResults(t *testing.T) { + var calls atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/v1/brain/recall", r.URL.Path) + + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, "alpha", body["query"]) + assert.Equal(t, float64(5), body["top_k"]) + assert.Equal(t, "cladius", body["agent_id"]) + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "memories": []map[string]any{ + { + "id": "m-1", + "content": "alpha memory", + "type": "decision", + "project": "ide", + "agent_id": "cladius", + "created_at": "2026-03-31T00:00:00Z", + "score": 0.91, + "source": "laravel", + }, + }, + }) + })) + defer upstream.Close() + + cache, err := newBrainRecallCache(t.TempDir(), upstream.URL, "secret", time.Hour) + require.NoError(t, err) + defer func() { + require.NoError(t, cache.Close()) + }() + + sub := &BrainDirectSubsystem{ + workspaceRoot: "/workspace", + apiURL: upstream.URL, + apiKey: "secret", + client: upstream.Client(), + cache: cache, + } + + _, first, err := sub.brainRecall(context.Background(), nil, RecallInput{Query: "alpha", TopK: 5}) + require.NoError(t, err) + assert.True(t, first.Success) + require.Equal(t, int32(1), calls.Load()) + + _, second, err := sub.brainRecall(context.Background(), nil, RecallInput{Query: "alpha", TopK: 5}) + require.NoError(t, err) + assert.True(t, second.Success) + require.Equal(t, int32(1), calls.Load()) +} + +func TestBrainDirectSubsystem_ForgetClearsCache(t *testing.T) { + var recallCalls atomic.Int32 + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPost && r.URL.Path == "/v1/brain/recall": + recallCalls.Add(1) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "memories": []map[string]any{ + {"id": "m-1", "content": "alpha", "type": "decision", "created_at": "2026-03-31T00:00:00Z"}, + }, + }) + case r.Method == http.MethodDelete && r.URL.Path == "/v1/brain/forget/m-1": + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"deleted": true}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer upstream.Close() + + cache, err := newBrainRecallCache(t.TempDir(), upstream.URL, "secret", time.Hour) + require.NoError(t, err) + defer func() { + require.NoError(t, cache.Close()) + }() + + sub := &BrainDirectSubsystem{ + workspaceRoot: "/workspace", + apiURL: upstream.URL, + apiKey: "secret", + client: upstream.Client(), + cache: cache, + } + + _, _, err = sub.brainRecall(context.Background(), nil, RecallInput{Query: "alpha", TopK: 5}) + require.NoError(t, err) + require.Equal(t, int32(1), recallCalls.Load()) + + _, _, err = sub.brainForget(context.Background(), nil, ForgetInput{ID: "m-1"}) + require.NoError(t, err) + + _, _, err = sub.brainRecall(context.Background(), nil, RecallInput{Query: "alpha", TopK: 5}) + require.NoError(t, err) + require.Equal(t, int32(2), recallCalls.Load()) +} diff --git a/go.mod b/go.mod index 40f953e..af95ade 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( forge.lthn.ai/core/go-ws v0.2.3 forge.lthn.ai/core/gui v0.1.3 forge.lthn.ai/core/mcp v0.3.2 + github.com/marcboeker/go-duckdb v1.8.5 github.com/stretchr/testify v1.11.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.74 ) @@ -21,6 +22,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/adrg/xdg v0.5.3 // indirect + github.com/apache/arrow-go/v18 v18.5.2 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/coder/websocket v1.8.14 // indirect @@ -35,14 +37,17 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/u v1.1.1 // indirect github.com/lmittmann/tint v1.1.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -53,6 +58,10 @@ require ( github.com/skeema/knownhosts v1.3.2 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gopkg.in/warnings.v0 v0.1.2 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect @@ -130,7 +139,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modelcontextprotocol/go-sdk v1.4.1 // indirect + github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ollama/ollama v0.18.1 // indirect diff --git a/go.sum b/go.sum index 29ea80f..63691a2 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,10 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apache/arrow-go/v18 v18.5.2 h1:3uoHjoaEie5eVsxx/Bt64hKwZx4STb+beAkqKOlq/lY= +github.com/apache/arrow-go/v18 v18.5.2/go.mod h1:yNoizNTT4peTciJ7V01d2EgOkE1d0fQ1vZcFOsVtFsw= +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -215,6 +219,10 @@ github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -244,6 +252,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -263,6 +275,8 @@ github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -270,6 +284,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -285,6 +303,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -372,6 +392,8 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= @@ -443,6 +465,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe h1:MaXjBsxue6l0hflXDwJ/XBfUJRjiyX1PwLd7F3lYDXA= +golang.org/x/telemetry v0.0.0-20260312161427-1546bf4b83fe/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -462,6 +486,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= diff --git a/main.go b/main.go index 94b5774..69f7c91 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,11 @@ func main() { } bridge := ide.NewBridge(hub, bridgeCfg) + brainDirect, err := NewCachedBrainDirect(cwd) + if err != nil { + log.Fatalf("failed to initialise brain cache: %v", err) + } + // ── Service Provider Registry ────────────────────────────── reg := provider.NewRegistry() reg.Add(processapi.NewProvider(process.DefaultRegistry(), hub)) @@ -100,7 +105,7 @@ func main() { return mcp.New( mcp.WithWorkspaceRoot(cwd), mcp.WithWSHub(hub), - mcp.WithSubsystem(brain.NewDirect()), + mcp.WithSubsystem(brainDirect), mcp.WithSubsystem(agentic.NewPrep()), mcp.WithSubsystem(guiMCP.New(c)), ) diff --git a/main_linux.go b/main_linux.go index 2681ef7..d852913 100644 --- a/main_linux.go +++ b/main_linux.go @@ -50,6 +50,11 @@ func main() { } bridge := ide.NewBridge(hub, bridgeCfg) + brainDirect, err := NewCachedBrainDirect(cwd) + if err != nil { + log.Fatalf("failed to initialise brain cache: %v", err) + } + reg := provider.NewRegistry() reg.Add(processapi.NewProvider(process.DefaultRegistry(), hub)) reg.Add(brain.NewProvider(bridge, hub)) @@ -79,7 +84,7 @@ func main() { return mcp.New( mcp.WithWorkspaceRoot(cwd), mcp.WithWSHub(hub), - mcp.WithSubsystem(brain.NewDirect()), + mcp.WithSubsystem(brainDirect), mcp.WithSubsystem(agentic.NewPrep()), ) }),