feat(ide): add DuckDB brain recall cache
Some checks failed
Test / test (push) Successful in 4m43s
Security Scan / security (push) Failing after 13m58s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 19:11:01 +00:00
parent efed2fc3ec
commit 7ab16075c4
6 changed files with 663 additions and 3 deletions

467
brain_direct.go Normal file
View file

@ -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
}

148
brain_direct_test.go Normal file
View file

@ -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())
}

11
go.mod
View file

@ -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

26
go.sum
View file

@ -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=

View file

@ -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)),
)

View file

@ -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()),
)
}),