diff --git a/cmd/lem-desktop/agent_runner.go b/cmd/lem-desktop/agent_runner.go new file mode 100644 index 0000000..6b03e68 --- /dev/null +++ b/cmd/lem-desktop/agent_runner.go @@ -0,0 +1,122 @@ +package main + +import ( + "context" + "log" + "sync" + + "forge.lthn.ai/lthn/lem/pkg/lem" + "github.com/wailsapp/wails/v3/pkg/application" +) + +// AgentRunner wraps the scoring agent for desktop use. +// Provides start/stop/status for the tray and dashboard. +type AgentRunner struct { + apiURL string + influxURL string + influxDB string + m3Host string + baseModel string + workDir string + + mu sync.RWMutex + running bool + task string + cancel context.CancelFunc +} + +// NewAgentRunner creates an AgentRunner. +func NewAgentRunner(apiURL, influxURL, influxDB, m3Host, baseModel, workDir string) *AgentRunner { + return &AgentRunner{ + apiURL: apiURL, + influxURL: influxURL, + influxDB: influxDB, + m3Host: m3Host, + baseModel: baseModel, + workDir: workDir, + } +} + +// ServiceName returns the Wails service name. +func (a *AgentRunner) ServiceName() string { + return "AgentRunner" +} + +// ServiceStartup is called when the Wails app starts. +func (a *AgentRunner) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + log.Println("AgentRunner started") + return nil +} + +// IsRunning returns whether the agent is currently running. +func (a *AgentRunner) IsRunning() bool { + a.mu.RLock() + defer a.mu.RUnlock() + return a.running +} + +// CurrentTask returns the current task description. +func (a *AgentRunner) CurrentTask() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.task +} + +// Start begins the scoring agent in a background goroutine. +func (a *AgentRunner) Start() error { + a.mu.Lock() + if a.running { + a.mu.Unlock() + return nil + } + + ctx, cancel := context.WithCancel(context.Background()) + a.cancel = cancel + a.running = true + a.task = "Starting..." + a.mu.Unlock() + + go func() { + defer func() { + a.mu.Lock() + a.running = false + a.task = "" + a.cancel = nil + a.mu.Unlock() + }() + + log.Println("Scoring agent started via desktop") + + // Use the same RunAgent function from pkg/lem. + // Build args matching the CLI flags. + args := []string{ + "--api-url", a.apiURL, + "--influx", a.influxURL, + "--influx-db", a.influxDB, + "--m3-host", a.m3Host, + "--base-model", a.baseModel, + "--work-dir", a.workDir, + } + + // Run in the background — RunAgent blocks until cancelled. + // We use a goroutine-safe wrapper here. + _ = ctx // Agent doesn't support context cancellation yet. + _ = args + lem.RunAgent(args) + }() + + return nil +} + +// Stop stops the scoring agent. +func (a *AgentRunner) Stop() { + a.mu.Lock() + defer a.mu.Unlock() + + if a.cancel != nil { + a.cancel() + } + a.running = false + a.task = "" + log.Println("Scoring agent stopped via desktop") +} diff --git a/cmd/lem-desktop/dashboard.go b/cmd/lem-desktop/dashboard.go new file mode 100644 index 0000000..f996efd --- /dev/null +++ b/cmd/lem-desktop/dashboard.go @@ -0,0 +1,299 @@ +package main + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + "forge.lthn.ai/lthn/lem/pkg/lem" + "github.com/wailsapp/wails/v3/pkg/application" +) + +// DashboardService bridges pkg/lem CLI functions for the desktop UI. +// Provides real-time status, model inventory, and scoring progress +// to the frontend via Wails bindings. +type DashboardService struct { + influx *lem.InfluxClient + dbPath string + mu sync.RWMutex + + // Cached state (refreshed periodically). + trainingStatus []TrainingRow + generationStats GenerationStats + modelInventory []ModelInfo + lastRefresh time.Time +} + +// TrainingRow represents a single model's training progress. +type TrainingRow struct { + Model string `json:"model"` + RunID string `json:"runId"` + Status string `json:"status"` + Iteration int `json:"iteration"` + TotalIters int `json:"totalIters"` + Pct float64 `json:"pct"` + Loss float64 `json:"loss"` +} + +// GenerationStats shows golden set and expansion progress. +type GenerationStats struct { + GoldenCompleted int `json:"goldenCompleted"` + GoldenTarget int `json:"goldenTarget"` + GoldenPct float64 `json:"goldenPct"` + ExpansionCompleted int `json:"expansionCompleted"` + ExpansionTarget int `json:"expansionTarget"` + ExpansionPct float64 `json:"expansionPct"` +} + +// ModelInfo represents a model in the inventory. +type ModelInfo struct { + Name string `json:"name"` + Tag string `json:"tag"` + Accuracy float64 `json:"accuracy"` + Iterations int `json:"iterations"` + Status string `json:"status"` +} + +// AgentStatus represents the scoring agent's current state. +type AgentStatus struct { + Running bool `json:"running"` + CurrentTask string `json:"currentTask"` + Scored int `json:"scored"` + Remaining int `json:"remaining"` + LastScore string `json:"lastScore"` +} + +// DashboardSnapshot is the complete UI state sent to the frontend. +type DashboardSnapshot struct { + Training []TrainingRow `json:"training"` + Generation GenerationStats `json:"generation"` + Models []ModelInfo `json:"models"` + Agent AgentStatus `json:"agent"` + DBPath string `json:"dbPath"` + UpdatedAt string `json:"updatedAt"` +} + +// NewDashboardService creates a DashboardService. +func NewDashboardService(influxURL, influxDB, dbPath string) *DashboardService { + return &DashboardService{ + influx: lem.NewInfluxClient(influxURL, influxDB), + dbPath: dbPath, + } +} + +// ServiceName returns the Wails service name. +func (d *DashboardService) ServiceName() string { + return "DashboardService" +} + +// ServiceStartup is called when the Wails app starts. +func (d *DashboardService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + log.Println("DashboardService started") + go d.refreshLoop(ctx) + return nil +} + +// GetSnapshot returns the complete dashboard state. +func (d *DashboardService) GetSnapshot() DashboardSnapshot { + d.mu.RLock() + defer d.mu.RUnlock() + + return DashboardSnapshot{ + Training: d.trainingStatus, + Generation: d.generationStats, + Models: d.modelInventory, + DBPath: d.dbPath, + UpdatedAt: d.lastRefresh.Format(time.RFC3339), + } +} + +// GetTraining returns current training status. +func (d *DashboardService) GetTraining() []TrainingRow { + d.mu.RLock() + defer d.mu.RUnlock() + return d.trainingStatus +} + +// GetGeneration returns generation progress. +func (d *DashboardService) GetGeneration() GenerationStats { + d.mu.RLock() + defer d.mu.RUnlock() + return d.generationStats +} + +// GetModels returns the model inventory. +func (d *DashboardService) GetModels() []ModelInfo { + d.mu.RLock() + defer d.mu.RUnlock() + return d.modelInventory +} + +// Refresh forces an immediate data refresh. +func (d *DashboardService) Refresh() error { + return d.refresh() +} + +// RunQuery executes an ad-hoc SQL query against DuckDB. +func (d *DashboardService) RunQuery(sql string) ([]map[string]interface{}, error) { + if d.dbPath == "" { + return nil, fmt.Errorf("no database configured") + } + db, err := lem.OpenDB(d.dbPath) + if err != nil { + return nil, fmt.Errorf("open db: %w", err) + } + defer db.Close() + + rows, err := db.QueryRows(sql) + if err != nil { + return nil, fmt.Errorf("query: %w", err) + } + return rows, nil +} + +func (d *DashboardService) refreshLoop(ctx context.Context) { + // Initial refresh. + if err := d.refresh(); err != nil { + log.Printf("Dashboard refresh error: %v", err) + } + + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := d.refresh(); err != nil { + log.Printf("Dashboard refresh error: %v", err) + } + } + } +} + +func (d *DashboardService) refresh() error { + d.mu.Lock() + defer d.mu.Unlock() + + // Query training status from InfluxDB. + rows, err := d.influx.QuerySQL(` + SELECT model, run_id, status, iteration, total_iters, pct + FROM training_status + ORDER BY time DESC LIMIT 10 + `) + if err == nil { + d.trainingStatus = nil + for _, row := range rows { + d.trainingStatus = append(d.trainingStatus, TrainingRow{ + Model: strVal(row, "model"), + RunID: strVal(row, "run_id"), + Status: strVal(row, "status"), + Iteration: intVal(row, "iteration"), + TotalIters: intVal(row, "total_iters"), + Pct: floatVal(row, "pct"), + }) + } + } + + // Query latest loss per model. + lossRows, err := d.influx.QuerySQL(` + SELECT model, loss FROM training_loss + WHERE loss_type = 'train' + ORDER BY time DESC LIMIT 10 + `) + if err == nil { + lossMap := make(map[string]float64) + for _, row := range lossRows { + model := strVal(row, "model") + if _, exists := lossMap[model]; !exists { + lossMap[model] = floatVal(row, "loss") + } + } + for i, t := range d.trainingStatus { + if loss, ok := lossMap[t.Model]; ok { + d.trainingStatus[i].Loss = loss + } + } + } + + // Query golden set progress. + goldenRows, err := d.influx.QuerySQL(` + SELECT completed, target, pct FROM golden_gen_progress + ORDER BY time DESC LIMIT 1 + `) + if err == nil && len(goldenRows) > 0 { + d.generationStats.GoldenCompleted = intVal(goldenRows[0], "completed") + d.generationStats.GoldenTarget = intVal(goldenRows[0], "target") + d.generationStats.GoldenPct = floatVal(goldenRows[0], "pct") + } + + // Query expansion progress. + expRows, err := d.influx.QuerySQL(` + SELECT completed, target, pct FROM expansion_progress + ORDER BY time DESC LIMIT 1 + `) + if err == nil && len(expRows) > 0 { + d.generationStats.ExpansionCompleted = intVal(expRows[0], "completed") + d.generationStats.ExpansionTarget = intVal(expRows[0], "target") + d.generationStats.ExpansionPct = floatVal(expRows[0], "pct") + } + + // Query model capability scores. + capRows, err := d.influx.QuerySQL(` + SELECT model, label, accuracy, iteration FROM capability_score + WHERE category = 'overall' + ORDER BY time DESC LIMIT 20 + `) + if err == nil { + d.modelInventory = nil + seen := make(map[string]bool) + for _, row := range capRows { + label := strVal(row, "label") + if seen[label] { + continue + } + seen[label] = true + d.modelInventory = append(d.modelInventory, ModelInfo{ + Name: strVal(row, "model"), + Tag: label, + Accuracy: floatVal(row, "accuracy"), + Iterations: intVal(row, "iteration"), + Status: "scored", + }) + } + } + + d.lastRefresh = time.Now() + return nil +} + +func strVal(m map[string]interface{}, key string) string { + if v, ok := m[key]; ok { + return fmt.Sprintf("%v", v) + } + return "" +} + +func intVal(m map[string]interface{}, key string) int { + if v, ok := m[key]; ok { + switch n := v.(type) { + case float64: + return int(n) + case int: + return n + } + } + return 0 +} + +func floatVal(m map[string]interface{}, key string) float64 { + if v, ok := m[key]; ok { + if f, ok := v.(float64); ok { + return f + } + } + return 0 +} diff --git a/cmd/lem-desktop/docker.go b/cmd/lem-desktop/docker.go new file mode 100644 index 0000000..fea3a4c --- /dev/null +++ b/cmd/lem-desktop/docker.go @@ -0,0 +1,226 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +// DockerService manages the LEM Docker compose stack. +// Provides start/stop/status for Forgejo, InfluxDB, and inference services. +type DockerService struct { + composeFile string + mu sync.RWMutex + services map[string]ContainerStatus +} + +// ContainerStatus represents a Docker container's state. +type ContainerStatus struct { + Name string `json:"name"` + Image string `json:"image"` + Status string `json:"status"` + Health string `json:"health"` + Ports string `json:"ports"` + Running bool `json:"running"` +} + +// StackStatus represents the overall stack state. +type StackStatus struct { + Running bool `json:"running"` + Services map[string]ContainerStatus `json:"services"` + ComposeDir string `json:"composeDir"` +} + +// NewDockerService creates a DockerService. +// composeDir should point to the deploy/ directory containing docker-compose.yml. +func NewDockerService(composeDir string) *DockerService { + return &DockerService{ + composeFile: filepath.Join(composeDir, "docker-compose.yml"), + services: make(map[string]ContainerStatus), + } +} + +// ServiceName returns the Wails service name. +func (d *DockerService) ServiceName() string { + return "DockerService" +} + +// ServiceStartup is called when the Wails app starts. +func (d *DockerService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + log.Println("DockerService started") + go d.statusLoop(ctx) + return nil +} + +// Start brings up the full Docker compose stack. +func (d *DockerService) Start() error { + log.Println("Starting LEM stack...") + return d.compose("up", "-d") +} + +// Stop takes down the Docker compose stack. +func (d *DockerService) Stop() error { + log.Println("Stopping LEM stack...") + return d.compose("down") +} + +// Restart restarts the full stack. +func (d *DockerService) Restart() error { + if err := d.Stop(); err != nil { + return err + } + return d.Start() +} + +// StartService starts a single service. +func (d *DockerService) StartService(name string) error { + return d.compose("up", "-d", name) +} + +// StopService stops a single service. +func (d *DockerService) StopService(name string) error { + return d.compose("stop", name) +} + +// RestartService restarts a single service. +func (d *DockerService) RestartService(name string) error { + return d.compose("restart", name) +} + +// Logs returns recent logs for a service. +func (d *DockerService) Logs(name string, lines int) (string, error) { + if lines <= 0 { + lines = 50 + } + out, err := d.composeOutput("logs", "--tail", fmt.Sprintf("%d", lines), "--no-color", name) + if err != nil { + return "", err + } + return out, nil +} + +// GetStatus returns the current stack status. +func (d *DockerService) GetStatus() StackStatus { + d.mu.RLock() + defer d.mu.RUnlock() + + running := false + for _, s := range d.services { + if s.Running { + running = true + break + } + } + + return StackStatus{ + Running: running, + Services: d.services, + ComposeDir: filepath.Dir(d.composeFile), + } +} + +// IsRunning returns whether any services are running. +func (d *DockerService) IsRunning() bool { + d.mu.RLock() + defer d.mu.RUnlock() + for _, s := range d.services { + if s.Running { + return true + } + } + return false +} + +// Pull pulls latest images for all services. +func (d *DockerService) Pull() error { + return d.compose("pull") +} + +func (d *DockerService) compose(args ...string) error { + fullArgs := append([]string{"compose", "-f", d.composeFile}, args...) + cmd := exec.Command("docker", fullArgs...) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker compose %s: %w: %s", strings.Join(args, " "), err, string(out)) + } + return nil +} + +func (d *DockerService) composeOutput(args ...string) (string, error) { + fullArgs := append([]string{"compose", "-f", d.composeFile}, args...) + cmd := exec.Command("docker", fullArgs...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("docker compose %s: %w: %s", strings.Join(args, " "), err, string(out)) + } + return string(out), nil +} + +func (d *DockerService) refreshStatus() { + out, err := d.composeOutput("ps", "--format", "json") + if err != nil { + return + } + + d.mu.Lock() + defer d.mu.Unlock() + + d.services = make(map[string]ContainerStatus) + + // docker compose ps --format json outputs one JSON object per line. + for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + if line == "" { + continue + } + var container struct { + Name string `json:"Name"` + Image string `json:"Image"` + Service string `json:"Service"` + Status string `json:"Status"` + Health string `json:"Health"` + State string `json:"State"` + Ports string `json:"Ports"` + } + if err := json.Unmarshal([]byte(line), &container); err != nil { + continue + } + + name := container.Service + if name == "" { + name = container.Name + } + + d.services[name] = ContainerStatus{ + Name: container.Name, + Image: container.Image, + Status: container.Status, + Health: container.Health, + Ports: container.Ports, + Running: container.State == "running", + } + } +} + +func (d *DockerService) statusLoop(ctx context.Context) { + d.refreshStatus() + + ticker := time.NewTicker(15 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + d.refreshStatus() + } + } +} diff --git a/cmd/lem-desktop/frontend/index.html b/cmd/lem-desktop/frontend/index.html new file mode 100644 index 0000000..222cd9d --- /dev/null +++ b/cmd/lem-desktop/frontend/index.html @@ -0,0 +1,482 @@ + + + + + + LEM Dashboard + + + +
+

LEM Dashboard

+ Connecting... +
+ +
+ +
+

Training Progress

+
+
+ + +
+

Generation

+
+
+ + +
+

Model Scoreboard

+
+
+ + +
+

Services

+
+
+ + +
+
+ + +
+

Scoring Agent

+
+
+ +
+
+
+ + + + + + diff --git a/cmd/lem-desktop/go.mod b/cmd/lem-desktop/go.mod new file mode 100644 index 0000000..1a85ba0 --- /dev/null +++ b/cmd/lem-desktop/go.mod @@ -0,0 +1,72 @@ +module forge.lthn.ai/lthn/lem/cmd/lem-desktop + +go 1.25.6 + +require ( + forge.lthn.ai/lthn/lem v0.0.0 + github.com/wailsapp/wails/v3 v3.0.0-alpha.71 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/apache/arrow-go/v18 v18.1.0 // 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 + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // 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.1.24+incompatible // indirect + github.com/google/uuid v1.6.0 // 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.4.0 // indirect + github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // 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.2 // indirect + github.com/marcboeker/go-duckdb v1.8.5 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/parquet-go/bitpack v1.0.0 // indirect + github.com/parquet-go/jsonlite v1.0.0 // indirect + github.com/parquet-go/parquet-go v0.27.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.2 // indirect + github.com/twpayne/go-geom v1.6.1 // 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/crypto v0.47.0 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.41.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) + +replace forge.lthn.ai/lthn/lem => ../../ diff --git a/cmd/lem-desktop/go.sum b/cmd/lem-desktop/go.sum new file mode 100644 index 0000000..a2f88bb --- /dev/null +++ b/cmd/lem-desktop/go.sum @@ -0,0 +1,211 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +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.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= +github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/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= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +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= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA= +github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= +github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU= +github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0= +github.com/parquet-go/parquet-go v0.27.0 h1:vHWK2xaHbj+v1DYps03yDRpEsdtOeKbhiXUaixoPb3g= +github.com/parquet-go/parquet-go v0.27.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4= +github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028= +github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= +github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.71 h1:6ERh+1SJJ+tl5E4W49q8pDyQ4yeyi1yj9IdSppKtMx4= +github.com/wailsapp/wails/v3 v3.0.0-alpha.71/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +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= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +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.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/lem-desktop/icons/icons.go b/cmd/lem-desktop/icons/icons.go new file mode 100644 index 0000000..703bbd0 --- /dev/null +++ b/cmd/lem-desktop/icons/icons.go @@ -0,0 +1,23 @@ +package icons + +// Placeholder tray icons — replace with actual PNG data. +// Generate with: task lem-desktop:generate:icons +// +// macOS template icons should be black-on-transparent, 22x22 or 44x44. +// Windows/Linux icons should be full-color, 32x32 or 64x64. + +// Placeholder returns a minimal 1x1 transparent PNG for development. +// Replace with the real LEM logo (brain + scales motif). +func Placeholder() []byte { + return []byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, // RGB + 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT + 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, // data + 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, // data + 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND + 0x44, 0xae, 0x42, 0x60, 0x82, + } +} diff --git a/cmd/lem-desktop/main.go b/cmd/lem-desktop/main.go new file mode 100644 index 0000000..bb03911 --- /dev/null +++ b/cmd/lem-desktop/main.go @@ -0,0 +1,146 @@ +// Package main provides the LEM Desktop application. +// A system tray app inspired by BugSETI that bundles: +// - Local Forgejo for agentic git workflows +// - InfluxDB for metrics and coordination +// - Inference proxy to M3 MLX or local vLLM +// - Scoring agent for automated checkpoint evaluation +// - Lab dashboard for training and generation monitoring +// +// Built on Wails v3 — ships as a signed native binary on macOS (Lethean CIC), +// Linux AppImage, and Windows installer. +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "forge.lthn.ai/lthn/lem/cmd/lem-desktop/icons" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" +) + +//go:embed all:frontend +var assets embed.FS + +// Tray icon data — placeholders until real icons are generated. +var ( + trayIconTemplate = icons.Placeholder() + trayIconLight = icons.Placeholder() + trayIconDark = icons.Placeholder() +) + +func main() { + // Strip embed prefix so files serve from root. + staticAssets, err := fs.Sub(assets, "frontend") + if err != nil { + log.Fatal(err) + } + + // ── Configuration ── + influxURL := envOr("INFLUX_URL", "http://localhost:8181") + influxDB := envOr("INFLUX_DB", "training") + apiURL := envOr("LEM_API_URL", "http://localhost:8080") + m3Host := envOr("M3_HOST", "10.69.69.108") + baseModel := envOr("BASE_MODEL", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B") + dbPath := envOr("LEM_DB", "") + workDir := envOr("WORK_DIR", filepath.Join(os.TempDir(), "scoring-agent")) + deployDir := envOr("LEM_DEPLOY_DIR", findDeployDir()) + + // ── Services ── + dashboardService := NewDashboardService(influxURL, influxDB, dbPath) + dockerService := NewDockerService(deployDir) + agentRunner := NewAgentRunner(apiURL, influxURL, influxDB, m3Host, baseModel, workDir) + trayService := NewTrayService(nil) + + services := []application.Service{ + application.NewService(dashboardService), + application.NewService(dockerService), + application.NewService(agentRunner), + application.NewService(trayService), + } + + // ── Application ── + app := application.New(application.Options{ + Name: "LEM", + Description: "Lethean Ethics Model — Training, Scoring & Inference", + Services: services, + Assets: application.AssetOptions{ + Handler: spaHandler(staticAssets), + }, + Mac: application.MacOptions{ + ActivationPolicy: application.ActivationPolicyAccessory, + }, + }) + + // Wire up references. + trayService.app = app + trayService.SetServices(dashboardService, dockerService, agentRunner) + + // Set up system tray. + setupSystemTray(app, trayService, dashboardService, dockerService) + + // Show dashboard on first launch. + app.Event.RegisterApplicationEventHook(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { + if w, ok := app.Window.Get("dashboard"); ok { + w.Show() + w.Focus() + } + }) + + log.Println("Starting LEM Desktop...") + log.Println(" - System tray active") + log.Println(" - Dashboard ready") + log.Printf(" - InfluxDB: %s/%s", influxURL, influxDB) + log.Printf(" - Inference: %s", apiURL) + + if err := app.Run(); err != nil { + log.Fatal(err) + } +} + +// spaHandler serves static files with SPA fallback for client-side routing. +func spaHandler(fsys fs.FS) http.Handler { + fileServer := http.FileServer(http.FS(fsys)) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/") + if path == "" { + path = "index.html" + } + if _, err := fs.Stat(fsys, path); err != nil { + r.URL.Path = "/" + } + fileServer.ServeHTTP(w, r) + }) +} + +// findDeployDir locates the deploy/ directory relative to the binary. +func findDeployDir() string { + // Check relative to executable. + exe, err := os.Executable() + if err == nil { + dir := filepath.Join(filepath.Dir(exe), "deploy") + if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err == nil { + return dir + } + } + // Check relative to working directory. + if cwd, err := os.Getwd(); err == nil { + dir := filepath.Join(cwd, "deploy") + if _, err := os.Stat(filepath.Join(dir, "docker-compose.yml")); err == nil { + return dir + } + } + return "deploy" +} + +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/cmd/lem-desktop/tray.go b/cmd/lem-desktop/tray.go new file mode 100644 index 0000000..76c82cf --- /dev/null +++ b/cmd/lem-desktop/tray.go @@ -0,0 +1,277 @@ +package main + +import ( + "context" + "fmt" + "log" + "os/exec" + "runtime" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +// TrayService provides system tray bindings for the LEM desktop. +// Exposes status to the frontend and controls the tray menu. +type TrayService struct { + app *application.App + dashboard *DashboardService + docker *DockerService + agent *AgentRunner +} + +// NewTrayService creates a new TrayService. +func NewTrayService(app *application.App) *TrayService { + return &TrayService{app: app} +} + +// SetServices wires up service references after app creation. +func (t *TrayService) SetServices(dashboard *DashboardService, docker *DockerService, agent *AgentRunner) { + t.dashboard = dashboard + t.docker = docker + t.agent = agent +} + +// ServiceName returns the Wails service name. +func (t *TrayService) ServiceName() string { + return "TrayService" +} + +// ServiceStartup is called when the Wails app starts. +func (t *TrayService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + log.Println("TrayService started") + return nil +} + +// ServiceShutdown is called on app exit. +func (t *TrayService) ServiceShutdown() error { + log.Println("TrayService shutdown") + return nil +} + +// TraySnapshot is the complete tray state for the frontend. +type TraySnapshot struct { + StackRunning bool `json:"stackRunning"` + AgentRunning bool `json:"agentRunning"` + AgentTask string `json:"agentTask"` + Training []TrainingRow `json:"training"` + Generation GenerationStats `json:"generation"` + Models []ModelInfo `json:"models"` + DockerServices int `json:"dockerServices"` +} + +// GetSnapshot returns the full tray state. +func (t *TrayService) GetSnapshot() TraySnapshot { + snap := TraySnapshot{} + + if t.dashboard != nil { + ds := t.dashboard.GetSnapshot() + snap.Training = ds.Training + snap.Generation = ds.Generation + snap.Models = ds.Models + } + + if t.docker != nil { + status := t.docker.GetStatus() + snap.StackRunning = status.Running + snap.DockerServices = len(status.Services) + } + + if t.agent != nil { + snap.AgentRunning = t.agent.IsRunning() + snap.AgentTask = t.agent.CurrentTask() + } + + return snap +} + +// StartStack starts the Docker compose stack. +func (t *TrayService) StartStack() error { + if t.docker == nil { + return fmt.Errorf("docker service not available") + } + return t.docker.Start() +} + +// StopStack stops the Docker compose stack. +func (t *TrayService) StopStack() error { + if t.docker == nil { + return fmt.Errorf("docker service not available") + } + return t.docker.Stop() +} + +// StartAgent starts the scoring agent. +func (t *TrayService) StartAgent() error { + if t.agent == nil { + return fmt.Errorf("agent service not available") + } + return t.agent.Start() +} + +// StopAgent stops the scoring agent. +func (t *TrayService) StopAgent() { + if t.agent != nil { + t.agent.Stop() + } +} + +// setupSystemTray configures the system tray icon and menu. +func setupSystemTray(app *application.App, tray *TrayService, dashboard *DashboardService, docker *DockerService) { + systray := app.SystemTray.New() + systray.SetTooltip("LEM — Lethean Ethics Model") + + // Platform-specific icon. + if runtime.GOOS == "darwin" { + systray.SetTemplateIcon(trayIconTemplate) + } else { + systray.SetDarkModeIcon(trayIconDark) + systray.SetIcon(trayIconLight) + } + + // ── Tray Panel (frameless dropdown) ── + trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "tray-panel", + Title: "LEM", + Width: 420, + Height: 520, + URL: "/tray", + Hidden: true, + Frameless: true, + BackgroundColour: application.NewRGB(15, 23, 42), + }) + systray.AttachWindow(trayWindow).WindowOffset(5) + + // ── Dashboard Window ── + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "dashboard", + Title: "LEM Dashboard", + Width: 1400, + Height: 900, + URL: "/dashboard", + Hidden: true, + BackgroundColour: application.NewRGB(15, 23, 42), + }) + + // ── Workbench Window (model scoring, probes) ── + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "workbench", + Title: "LEM Workbench", + Width: 1200, + Height: 800, + URL: "/workbench", + Hidden: true, + BackgroundColour: application.NewRGB(15, 23, 42), + }) + + // ── Settings Window ── + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Name: "settings", + Title: "LEM Settings", + Width: 600, + Height: 500, + URL: "/settings", + Hidden: true, + BackgroundColour: application.NewRGB(15, 23, 42), + }) + + // ── Build Tray Menu ── + trayMenu := app.Menu.New() + + // Status (dynamic). + statusItem := trayMenu.Add("LEM: Idle") + statusItem.SetEnabled(false) + + trayMenu.AddSeparator() + + // Stack control. + stackItem := trayMenu.Add("Start Services") + stackItem.OnClick(func(ctx *application.Context) { + if docker.IsRunning() { + docker.Stop() + stackItem.SetLabel("Start Services") + statusItem.SetLabel("LEM: Stopped") + } else { + docker.Start() + stackItem.SetLabel("Stop Services") + statusItem.SetLabel("LEM: Running") + } + }) + + // Agent control. + agentItem := trayMenu.Add("Start Scoring Agent") + agentItem.OnClick(func(ctx *application.Context) { + if tray.agent != nil && tray.agent.IsRunning() { + tray.agent.Stop() + agentItem.SetLabel("Start Scoring Agent") + } else if tray.agent != nil { + tray.agent.Start() + agentItem.SetLabel("Stop Scoring Agent") + } + }) + + trayMenu.AddSeparator() + + // Windows. + trayMenu.Add("Open Dashboard").OnClick(func(ctx *application.Context) { + if w, ok := app.Window.Get("dashboard"); ok { + w.Show() + w.Focus() + } + }) + + trayMenu.Add("Open Workbench").OnClick(func(ctx *application.Context) { + if w, ok := app.Window.Get("workbench"); ok { + w.Show() + w.Focus() + } + }) + + trayMenu.Add("Open Forge").OnClick(func(ctx *application.Context) { + // Open the local Forgejo in the default browser. + openBrowser("http://localhost:3000") + }) + + trayMenu.AddSeparator() + + // Stats submenu. + statsMenu := trayMenu.AddSubmenu("Training") + statsMenu.Add("Golden Set: loading...").SetEnabled(false) + statsMenu.Add("Expansion: loading...").SetEnabled(false) + statsMenu.Add("Models Scored: loading...").SetEnabled(false) + + trayMenu.AddSeparator() + + // Settings. + trayMenu.Add("Settings...").OnClick(func(ctx *application.Context) { + if w, ok := app.Window.Get("settings"); ok { + w.Show() + w.Focus() + } + }) + + trayMenu.AddSeparator() + + // Quit. + trayMenu.Add("Quit LEM").OnClick(func(ctx *application.Context) { + app.Quit() + }) + + systray.SetMenu(trayMenu) +} + +// openBrowser launches the default browser. +func openBrowser(url string) { + var cmd string + var args []string + switch runtime.GOOS { + case "darwin": + cmd = "open" + case "linux": + cmd = "xdg-open" + case "windows": + cmd = "rundll32" + args = []string{"url.dll,FileProtocolHandler"} + } + args = append(args, url) + go exec.Command(cmd, args...).Start() +} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..d376c0d --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,88 @@ +# LEM Desktop — Docker Compose Stack +# Provides local Forgejo (agentic git), InfluxDB (metrics), and inference proxy. +# +# Usage: +# lem desktop start # starts all services +# docker compose -f deploy/docker-compose.yml up -d +# +# Services: +# forgejo — Local git forge for agentic workflows (port 3000, SSH 2222) +# influxdb — Metrics and coordination (port 8181) +# inference — OpenAI-compatible proxy to M3 MLX or local vLLM (port 8080) + +services: + # ── Forgejo — Local Agentic Git Forge ── + forgejo: + image: codeberg.org/forgejo/forgejo:10 + container_name: lem-forgejo + restart: unless-stopped + ports: + - "3000:3000" # Web UI + - "2222:22" # SSH + volumes: + - forgejo-data:/data + - forgejo-config:/etc/gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + - FORGEJO__server__ROOT_URL=http://localhost:3000/ + - FORGEJO__server__SSH_PORT=2222 + - FORGEJO__server__SSH_LISTEN_PORT=22 + - FORGEJO__service__DISABLE_REGISTRATION=false + - FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION=true + - FORGEJO__federation__ENABLED=true + - FORGEJO__actions__ENABLED=true + - FORGEJO__database__DB_TYPE=sqlite3 + - FORGEJO__database__PATH=/data/gitea/gitea.db + healthcheck: + test: ["CMD", "curl", "-fsSL", "http://localhost:3000/api/v1/version"] + interval: 30s + timeout: 5s + retries: 3 + + # ── InfluxDB v3 — Metrics & Coordination ── + influxdb: + image: quay.io/influxdb/influxdb3-core:latest + container_name: lem-influxdb + restart: unless-stopped + ports: + - "8181:8181" + volumes: + - influxdb-data:/var/lib/influxdb3 + environment: + - INFLUXDB3_NODE_ID=lem-local + command: ["serve", "--host-id", "lem-local", "--object-store", "file", "--data-dir", "/var/lib/influxdb3"] + healthcheck: + test: ["CMD", "curl", "-fsSL", "http://localhost:8181/health"] + interval: 15s + timeout: 5s + retries: 5 + + # ── Inference Proxy — OpenAI-Compatible API ── + # Routes to M3 MLX server or local vLLM/llama.cpp. + # Override LEM_INFERENCE_BACKEND to point elsewhere. + inference: + image: nginx:alpine + container_name: lem-inference + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - ./inference-proxy.conf:/etc/nginx/conf.d/default.conf:ro + environment: + - UPSTREAM_URL=${LEM_INFERENCE_BACKEND:-http://10.69.69.108:8090} + depends_on: + - influxdb + healthcheck: + test: ["CMD", "curl", "-fsSL", "http://localhost:8080/health"] + interval: 15s + timeout: 5s + retries: 3 + +volumes: + forgejo-data: + driver: local + forgejo-config: + driver: local + influxdb-data: + driver: local diff --git a/deploy/inference-proxy.conf b/deploy/inference-proxy.conf new file mode 100644 index 0000000..b652e1e --- /dev/null +++ b/deploy/inference-proxy.conf @@ -0,0 +1,30 @@ +# Nginx reverse proxy for OpenAI-compatible inference API. +# Routes /v1/* to the configured upstream (M3 MLX, vLLM, llama.cpp, etc.) +# Set UPSTREAM_URL env var or LEM_INFERENCE_BACKEND in docker-compose. + +server { + listen 8080; + server_name localhost; + + # Health check endpoint. + location /health { + return 200 '{"status": "ok"}'; + add_header Content-Type application/json; + } + + # Proxy all /v1/* requests to the inference backend. + location /v1/ { + proxy_pass ${UPSTREAM_URL}/v1/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_buffering off; + } + + # Model listing passthrough. + location /v1/models { + proxy_pass ${UPSTREAM_URL}/v1/models; + proxy_set_header Host $host; + } +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..91f4e66 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.25.6 + +use ( + . + ./cmd/lem-desktop +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..3c74b79 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,7 @@ +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= diff --git a/pkg/lem/db.go b/pkg/lem/db.go index 52107c7..ec1364c 100644 --- a/pkg/lem/db.go +++ b/pkg/lem/db.go @@ -162,6 +162,38 @@ func (db *DB) UpdateExpansionStatus(idx int64, status string) error { return nil } +// QueryRows executes an arbitrary SQL query and returns results as maps. +func (db *DB) QueryRows(query string, args ...interface{}) ([]map[string]interface{}, error) { + rows, err := db.conn.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("query: %w", err) + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + return nil, fmt.Errorf("columns: %w", err) + } + + var result []map[string]interface{} + for rows.Next() { + values := make([]interface{}, len(cols)) + ptrs := make([]interface{}, len(cols)) + for i := range values { + ptrs[i] = &values[i] + } + if err := rows.Scan(ptrs...); err != nil { + return nil, fmt.Errorf("scan: %w", err) + } + row := make(map[string]interface{}, len(cols)) + for i, col := range cols { + row[col] = values[i] + } + result = append(result, row) + } + return result, rows.Err() +} + // TableCounts returns row counts for all known tables. func (db *DB) TableCounts() (map[string]int, error) { tables := []string{"golden_set", "expansion_prompts", "seeds", "prompts",