diff --git a/.gitignore b/.gitignore index 042b2ab..17df47e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +dist/ .env xworkmate-bridge xworkmate-go-core-linux diff --git a/internal/acp/catalog.go b/internal/acp/catalog.go index 36fbaf1..690807e 100644 --- a/internal/acp/catalog.go +++ b/internal/acp/catalog.go @@ -13,13 +13,6 @@ type CapabilityCatalog struct { ProviderProbeSummary []any `json:"providerProbeSummary"` } -func (c *CapabilityCatalog) Update(providers []any, targets []any) { - c.mu.Lock() - defer c.mu.Unlock() - c.ProviderCatalog = providers - c.AvailableExecutionTargets = targets -} - func (c *CapabilityCatalog) Get() map[string]any { c.mu.RLock() defer c.mu.RUnlock() diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go deleted file mode 100644 index 9a49722..0000000 --- a/internal/handler/auth_handler.go +++ /dev/null @@ -1,50 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "net/http" - - "xworkmate-bridge/internal/service" -) - -type Authenticator interface { - Authenticate(username, password string) error -} - -type AuthHandler struct { - service Authenticator -} - -func NewAuthHandler(svc Authenticator) *AuthHandler { - return &AuthHandler{service: svc} -} - -func NewServiceAdapter(svc *service.AuthService) Authenticator { - return authServiceAdapter{service: svc} -} - -func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - var payload struct { - Username string `json:"username"` - Password string `json:"password"` - } - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - http.Error(w, "invalid json", http.StatusBadRequest) - return - } - if err := h.service.Authenticate(payload.Username, payload.Password); err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) -} - -type authServiceAdapter struct { - service *service.AuthService -} - -func (a authServiceAdapter) Authenticate(username, password string) error { - return a.service.Authenticate(context.TODO(), username, password) -} diff --git a/internal/handler/auth_handler_test.go b/internal/handler/auth_handler_test.go deleted file mode 100644 index a900a29..0000000 --- a/internal/handler/auth_handler_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package handler - -import ( - "bytes" - "errors" - "net/http" - "net/http/httptest" - "testing" -) - -type fakeAuthenticator struct { - err error -} - -func (f fakeAuthenticator) Authenticate(username, password string) error { - return f.err -} - -func TestAuthHandlerRejectsInvalidJSON(t *testing.T) { - handler := NewAuthHandler(fakeAuthenticator{}) - req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString("{")) - rec := httptest.NewRecorder() - - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusBadRequest { - t.Fatalf("expected 400, got %d", rec.Code) - } -} - -func TestAuthHandlerReturnsUnauthorizedOnServiceFailure(t *testing.T) { - handler := NewAuthHandler(fakeAuthenticator{err: errors.New("invalid credentials")}) - req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`)) - rec := httptest.NewRecorder() - - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("expected 401, got %d", rec.Code) - } -} - -func TestAuthHandlerReturnsOKOnSuccess(t *testing.T) { - handler := NewAuthHandler(fakeAuthenticator{}) - req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`)) - rec := httptest.NewRecorder() - - handler.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d", rec.Code) - } -} diff --git a/internal/handler/token_auth_handler.go b/internal/handler/token_auth_handler.go deleted file mode 100644 index 6c1688b..0000000 --- a/internal/handler/token_auth_handler.go +++ /dev/null @@ -1,40 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - - "xworkmate-bridge/internal/service" - "xworkmate-bridge/internal/shared" -) - -type TokenAuthHandler struct { - service *service.StaticTokenAuthService -} - -func NewTokenAuthHandler(service *service.StaticTokenAuthService) *TokenAuthHandler { - return &TokenAuthHandler{service: service} -} - -func (h *TokenAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if h.service == nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusServiceUnavailable) - _ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32000, "auth service unavailable")) - return - } - token := r.Header.Get("Authorization") - if h.service.ValidateAuthorizationHeader(token) { - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{ - "jsonrpc": "2.0", - "ok": true, - "type": "res", - "payload": map[string]any{"authenticated": true}, - }) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - _ = json.NewEncoder(w).Encode(shared.ErrorEnvelope(nil, -32001, "unauthorized")) -} diff --git a/internal/handler/token_auth_handler_test.go b/internal/handler/token_auth_handler_test.go deleted file mode 100644 index 368d9f5..0000000 --- a/internal/handler/token_auth_handler_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package handler - -import ( - "net/http" - "net/http/httptest" - "testing" - - "xworkmate-bridge/internal/service" -) - -func TestTokenAuthHandlerServeHTTP(t *testing.T) { - h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret")) - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", "secret") - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected 200, got %d", rec.Code) - } -} - -func TestTokenAuthHandlerRejectsUnauthorized(t *testing.T) { - h := NewTokenAuthHandler(service.NewStaticTokenAuthService("secret")) - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("expected 401, got %d", rec.Code) - } -} diff --git a/internal/mounts/config.go b/internal/mounts/config.go deleted file mode 100644 index 084cfac..0000000 --- a/internal/mounts/config.go +++ /dev/null @@ -1,146 +0,0 @@ -package mounts - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "time" -) - -const ( - codexManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK" - codexManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK" - opencodeManagedMCPBlockStart = "# BEGIN XWORKMATE MANAGED MCP BLOCK" - opencodeManagedMCPBlockEnd = "# END XWORKMATE MANAGED MCP BLOCK" -) - -var mcpServerSectionPattern = regexp.MustCompile( - `(?m)^\[mcp_servers\.[^\]]+\]`, -) - -func countMCPSections(content string) int { - return len(mcpServerSectionPattern.FindAllStringIndex(content, -1)) -} - -func defaultCodexHome() string { - home, err := os.UserHomeDir() - if err != nil || strings.TrimSpace(home) == "" { - return "" - } - return filepath.Join(home, ".codex") -} - -func defaultOpencodeHome() string { - home, err := os.UserHomeDir() - if err != nil || strings.TrimSpace(home) == "" { - return "" - } - return filepath.Join(home, ".opencode") -} - -func defaultOpenClawHome() string { - home, err := os.UserHomeDir() - if err != nil || strings.TrimSpace(home) == "" { - return "" - } - return filepath.Join(home, ".openclaw") -} - -func stripManagedBlock(content, startMarker, endMarker string) string { - if strings.TrimSpace(content) == "" { - return content - } - - remaining := content - for { - start := strings.Index(remaining, startMarker) - if start < 0 { - break - } - end := strings.Index(remaining[start:], endMarker) - if end < 0 { - remaining = remaining[:start] - break - } - end += start - remaining = remaining[:start] + remaining[end+len(endMarker):] - } - return remaining -} - -func mergeManagedBlock(content, block, startMarker, endMarker string) string { - preserved := strings.TrimRight( - stripManagedBlock(content, startMarker, endMarker), - "\n", - ) - if preserved == "" { - return block + "\n" - } - return preserved + "\n\n" + block + "\n" -} - -func buildCodexManagedMCPBlock(servers []ManagedMCPServer) string { - var buffer strings.Builder - buffer.WriteString(codexManagedMCPBlockStart) - buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n") - _, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)) - for _, server := range servers { - _, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID) - _, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command) - if len(server.Args) > 0 { - _, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args)) - } - buffer.WriteString("\n") - } - buffer.WriteString(codexManagedMCPBlockEnd) - return strings.TrimRight(buffer.String(), "\n") -} - -func buildOpencodeManagedMCPBlock(servers []ManagedMCPServer) string { - var buffer strings.Builder - buffer.WriteString(opencodeManagedMCPBlockStart) - buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n") - _, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)) - for _, server := range servers { - _, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID) - if strings.TrimSpace(server.URL) != "" { - _, _ = fmt.Fprintf(&buffer, "url = %q\n", strings.TrimSpace(server.URL)) - } else { - buffer.WriteString("type = \"stdio\"\n") - _, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command) - if len(server.Args) > 0 { - _, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args)) - } - } - buffer.WriteString("\n") - } - buffer.WriteString(opencodeManagedMCPBlockEnd) - return strings.TrimRight(buffer.String(), "\n") -} - -func formatTOMLArray(items []string) string { - if len(items) == 0 { - return "[]" - } - var quoted []string - for _, item := range items { - quoted = append(quoted, fmt.Sprintf("%q", item)) - } - return "[" + strings.Join(quoted, ", ") + "]" -} - -func applyManagedBlock(configPath, block, startMarker, endMarker string) error { - configDir := filepath.Dir(configPath) - if err := os.MkdirAll(configDir, 0o755); err != nil { - return err - } - - content, err := os.ReadFile(configPath) - if err != nil && !os.IsNotExist(err) { - return err - } - merged := mergeManagedBlock(string(content), block, startMarker, endMarker) - return os.WriteFile(configPath, []byte(merged), 0o644) -} diff --git a/internal/mounts/reconcile.go b/internal/mounts/reconcile.go deleted file mode 100644 index b753fb9..0000000 --- a/internal/mounts/reconcile.go +++ /dev/null @@ -1,444 +0,0 @@ -package mounts - -import ( - "context" - "encoding/json" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - "time" -) - -type ManagedMCPServer struct { - ID string - Name string - Transport string - Command string - URL string - Args []string - Enabled bool -} - -type Config struct { - AutoSync bool - UsesAris bool - ManagedMCPServers []ManagedMCPServer -} - -type ArisInput struct { - Available bool - BundleVersion string - LLMChatServerPath string - SkillCount int - BridgeAvailable bool - Error string -} - -type Request struct { - Config Config - AIGatewayURL string - ConfiguredCodexCLIPath string - CodexHome string - OpencodeHome string - OpenClawHome string - Aris ArisInput -} - -type MountTargetState struct { - TargetID string - Label string - Available bool - SupportsSkills bool - SupportsMCP bool - SupportsAIGatewayInjection bool - DiscoveryState string - SyncState string - DiscoveredSkillCount int - DiscoveredMCPCount int - ManagedMCPCount int - Detail string -} - -type Result struct { - MountTargets []MountTargetState - ArisBundleVersion string - ArisCompatStatus string -} - -func Reconcile(request Request) Result { - states := []MountTargetState{ - reconcileAris(request.Config, request.Aris), - reconcileCodex( - request.Config, - request.AIGatewayURL, - request.ConfiguredCodexCLIPath, - request.CodexHome, - ), - reconcileCLIListTarget( - request.Config, - "claude", - "Claude", - []string{"claude", "mcp", "list"}, - ), - reconcileCLIListTarget( - request.Config, - "gemini", - "Gemini", - []string{"gemini", "mcp", "list"}, - ), - reconcileOpencode(request.Config, request.OpencodeHome), - reconcileOpenClaw(request.Config, request.OpenClawHome), - } - - result := Result{ - MountTargets: states, - ArisBundleVersion: strings.TrimSpace(request.Aris.BundleVersion), - ArisCompatStatus: "idle", - } - for _, state := range states { - if state.TargetID == "aris" { - result.ArisCompatStatus = state.SyncState - break - } - } - return result -} - -func ResultMap(result Result) map[string]any { - rawTargets := make([]map[string]any, 0, len(result.MountTargets)) - for _, target := range result.MountTargets { - rawTargets = append(rawTargets, map[string]any{ - "targetId": target.TargetID, - "label": target.Label, - "available": target.Available, - "supportsSkills": target.SupportsSkills, - "supportsMcp": target.SupportsMCP, - "supportsAiGatewayInjection": target.SupportsAIGatewayInjection, - "discoveryState": target.DiscoveryState, - "syncState": target.SyncState, - "discoveredSkillCount": target.DiscoveredSkillCount, - "discoveredMcpCount": target.DiscoveredMCPCount, - "managedMcpCount": target.ManagedMCPCount, - "detail": target.Detail, - }) - } - return map[string]any{ - "mountTargets": rawTargets, - "arisBundleVersion": result.ArisBundleVersion, - "arisCompatStatus": result.ArisCompatStatus, - } -} - -func reconcileAris(config Config, input ArisInput) MountTargetState { - state := placeholderState("aris", "ARIS", true, true, false) - if strings.TrimSpace(input.Error) != "" { - state.Available = false - state.DiscoveryState = "error" - state.SyncState = "error" - state.Detail = strings.TrimSpace(input.Error) - return state - } - if !input.Available { - state.DiscoveryState = "missing" - state.SyncState = "missing" - state.Detail = "Embedded ARIS bundle is unavailable." - return state - } - - state.Available = true - state.DiscoveryState = "ready" - state.DiscoveredSkillCount = input.SkillCount - llmChatReady := strings.TrimSpace(input.LLMChatServerPath) != "" - if config.UsesAris && llmChatReady && input.BridgeAvailable { - state.SyncState = "ready" - state.DiscoveredMCPCount = 1 - state.ManagedMCPCount = 1 - state.Detail = "Embedded bundle " + - strings.TrimSpace(input.BundleVersion) + - " ready; XWorkmate Go core manages llm-chat and claude-review." - return state - } - state.SyncState = "embedded" - if llmChatReady { - state.DiscoveredMCPCount = 1 - } - if llmChatReady { - state.Detail = "Embedded bundle extracted, but the XWorkmate Go core is not available yet." - } else { - state.Detail = "Embedded bundle extracted, but llm-chat metadata is missing." - } - return state -} - -func reconcileCodex( - config Config, - aiGatewayURL string, - configuredCodexCLIPath string, - codexHome string, -) MountTargetState { - state := placeholderState("codex", "Codex", true, true, true) - available := codexAvailable(configuredCodexCLIPath) - configHome := strings.TrimSpace(codexHome) - if configHome == "" { - configHome = defaultCodexHome() - } - configPath := filepath.Join(configHome, "config.toml") - content, _ := os.ReadFile(configPath) - discovered := countMCPSections(string(content)) - managedServers := enabledCodexServers(config.ManagedMCPServers) - if available && config.AutoSync && len(managedServers) > 0 { - _ = applyManagedBlock( - configPath, - buildCodexManagedMCPBlock(managedServers), - codexManagedMCPBlockStart, - codexManagedMCPBlockEnd, - ) - } - state.Available = available - if available { - state.DiscoveryState = "ready" - } else { - state.DiscoveryState = "missing" - } - switch { - case !available: - state.SyncState = "missing" - case config.AutoSync: - state.SyncState = "ready" - default: - state.SyncState = "disabled" - } - state.DiscoveredMCPCount = discovered - state.ManagedMCPCount = len(managedServers) - state.Detail = "Codex is exposed through the bridge control plane.\n" + - "Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" + - "Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc" - return state -} - -func reconcileCLIListTarget( - config Config, - targetID string, - label string, - command []string, -) MountTargetState { - state := placeholderState(targetID, label, true, true, true) - available := binaryExists(command[0]) - discovered := 0 - if available { - discovered = countListedEntries(command) - } - state.Available = available - if available { - state.DiscoveryState = "ready" - } else { - state.DiscoveryState = "missing" - } - if available && config.AutoSync { - state.SyncState = "launch-only" - } else { - state.SyncState = "disabled" - } - state.DiscoveredMCPCount = discovered - state.ManagedMCPCount = len(enabledServers(config.ManagedMCPServers)) - - if targetID == "gemini" { - state.Detail = "Gemini is exposed through the bridge control plane.\n" + - "Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" + - "Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc" - } else { - state.Detail = "MCP discovery uses `" + strings.Join(command, " ") + - "`; LLM API stays launch-scoped." - } - return state -} - -func reconcileOpencode(config Config, opencodeHome string) MountTargetState { - state := placeholderState("opencode", "OpenCode", true, true, true) - available := binaryExists("opencode") - configHome := strings.TrimSpace(opencodeHome) - if configHome == "" { - configHome = defaultOpencodeHome() - } - configPath := filepath.Join(configHome, "config.toml") - content, _ := os.ReadFile(configPath) - discovered := countMCPSections(string(content)) - managedServers := enabledServers(config.ManagedMCPServers) - if available && config.AutoSync && len(managedServers) > 0 { - _ = applyManagedBlock( - configPath, - buildOpencodeManagedMCPBlock(managedServers), - opencodeManagedMCPBlockStart, - opencodeManagedMCPBlockEnd, - ) - } - state.Available = available - if available { - state.DiscoveryState = "ready" - } else { - state.DiscoveryState = "missing" - } - switch { - case !available: - state.SyncState = "missing" - case config.AutoSync: - state.SyncState = "ready" - default: - state.SyncState = "disabled" - } - state.DiscoveredMCPCount = discovered - state.ManagedMCPCount = len(managedServers) - state.Detail = "OpenCode is exposed through the bridge control plane.\n" + - "Canonical WebSocket endpoint: https://xworkmate-bridge.svc.plus/acp\n" + - "Secondary HTTP RPC endpoint: https://xworkmate-bridge.svc.plus/acp/rpc" - return state -} - -func reconcileOpenClaw(config Config, openClawHome string) MountTargetState { - state := placeholderState("openclaw", "OpenClaw", true, false, true) - available := binaryExists("openclaw") - state.Available = available - if available { - state.DiscoveryState = "ready" - } else { - state.DiscoveryState = "missing" - } - if available && config.AutoSync { - state.SyncState = "launch-only" - } else { - state.SyncState = "disabled" - } - state.Detail = "OpenClaw acts as the host/control plane mount." - - configHome := strings.TrimSpace(openClawHome) - if configHome == "" { - configHome = defaultOpenClawHome() - } - configPath := filepath.Join(configHome, "openclaw.json") - if content, err := os.ReadFile(configPath); err == nil { - var decoded map[string]any - if err := json.Unmarshal(content, &decoded); err == nil { - agents := 0 - if rawAgents, ok := decoded["agents"].(map[string]any); ok { - if rawList, ok := rawAgents["list"].([]any); ok { - agents = len(rawList) - } - } - skillsDir := filepath.Join(configHome, "skills") - if entries, err := os.ReadDir(skillsDir); err == nil { - state.DiscoveredSkillCount = len(entries) - } - state.Detail = "agents: " + itoa(agents) + " ยท skills: " + - itoa(state.DiscoveredSkillCount) - } else { - state.Detail = "OpenClaw config detected but could not be fully parsed." - } - } - return state -} - -func placeholderState( - targetID string, - label string, - supportsSkills bool, - supportsMCP bool, - supportsAIGatewayInjection bool, -) MountTargetState { - return MountTargetState{ - TargetID: targetID, - Label: label, - SupportsSkills: supportsSkills, - SupportsMCP: supportsMCP, - SupportsAIGatewayInjection: supportsAIGatewayInjection, - DiscoveryState: "idle", - SyncState: "idle", - } -} - -func codexAvailable(configuredPath string) bool { - if strings.TrimSpace(configuredPath) != "" { - if _, err := os.Stat(strings.TrimSpace(configuredPath)); err == nil { - return true - } - } - return binaryExists("codex") -} - -func binaryExists(command string) bool { - _, err := exec.LookPath(command) - return err == nil -} - -func countListedEntries(command []string) int { - output := strings.TrimSpace(runCommand(command)) - if output == "" || - strings.Contains(output, "No MCP servers configured") || - strings.Contains(output, "No MCP servers configured yet") || - strings.Contains(output, "No MCP servers configured.") { - return 0 - } - lines := strings.Split(output, "\n") - count := 0 - for _, line := range lines { - trimmed := strings.TrimSpace(line) - switch { - case trimmed == "": - case strings.HasPrefix(trimmed, "Usage:"): - case strings.HasPrefix(trimmed, "โ”Œ"): - case strings.HasPrefix(trimmed, "โ”‚"): - case strings.HasPrefix(trimmed, "โ””"): - default: - count++ - } - } - return count -} - -func runCommand(command []string) string { - if len(command) == 0 { - return "" - } - ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, command[0], command[1:]...) - output, err := cmd.CombinedOutput() - if err != nil && len(output) == 0 { - return "" - } - return string(output) -} - -func enabledServers(servers []ManagedMCPServer) []ManagedMCPServer { - filtered := make([]ManagedMCPServer, 0, len(servers)) - for _, server := range servers { - if !server.Enabled { - continue - } - filtered = append(filtered, server) - } - sort.SliceStable(filtered, func(i, j int) bool { - return filtered[i].ID < filtered[j].ID - }) - return filtered -} - -func enabledCodexServers(servers []ManagedMCPServer) []ManagedMCPServer { - filtered := make([]ManagedMCPServer, 0, len(servers)) - for _, server := range servers { - if !server.Enabled || strings.TrimSpace(server.Command) == "" { - continue - } - filtered = append(filtered, server) - } - sort.SliceStable(filtered, func(i, j int) bool { - return filtered[i].ID < filtered[j].ID - }) - return filtered -} - -func itoa(value int) string { - return strconv.Itoa(value) -} diff --git a/internal/mounts/reconcile_test.go b/internal/mounts/reconcile_test.go deleted file mode 100644 index d88d39b..0000000 --- a/internal/mounts/reconcile_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package mounts - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestReconcileCodexAppliesManagedBlockAndPreservesUserEntries(t *testing.T) { - tempDir := t.TempDir() - configuredBinary := filepath.Join(tempDir, "custom-codex") - if err := os.WriteFile(configuredBinary, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { - t.Fatalf("write configured binary: %v", err) - } - configPath := filepath.Join(tempDir, "config.toml") - if err := os.WriteFile(configPath, []byte(` -[mcp_servers.user_server] -command = "user-mcp" -`), 0o644); err != nil { - t.Fatalf("write config: %v", err) - } - - result := Reconcile(Request{ - Config: Config{ - AutoSync: true, - ManagedMCPServers: []ManagedMCPServer{ - {ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "7777"}, Enabled: true}, - }, - }, - ConfiguredCodexCLIPath: configuredBinary, - CodexHome: tempDir, - }) - - content, err := os.ReadFile(configPath) - if err != nil { - t.Fatalf("read config: %v", err) - } - if !strings.Contains(string(content), `[mcp_servers.user_server]`) { - t.Fatalf("expected user entry preserved: %s", string(content)) - } - if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) { - t.Fatalf("expected managed entry written: %s", string(content)) - } - if strings.Count(string(content), codexManagedMCPBlockStart) != 1 { - t.Fatalf("expected single managed block: %s", string(content)) - } - if result.MountTargets[1].ManagedMCPCount != 1 { - t.Fatalf("expected codex managed count 1, got %d", result.MountTargets[1].ManagedMCPCount) - } -} - -func TestReconcileOpencodeAppliesManagedBlockAndPreservesUserEntries(t *testing.T) { - tempDir := t.TempDir() - binDir := t.TempDir() - originalPath := os.Getenv("PATH") - t.Setenv("PATH", binDir+string(os.PathListSeparator)+originalPath) - if err := os.WriteFile(filepath.Join(binDir, "opencode"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { - t.Fatalf("write opencode binary: %v", err) - } - configPath := filepath.Join(tempDir, "config.toml") - if err := os.WriteFile(configPath, []byte(` -[model] -name = "user-default" -`), 0o644); err != nil { - t.Fatalf("write config: %v", err) - } - - result := Reconcile(Request{ - Config: Config{ - AutoSync: true, - ManagedMCPServers: []ManagedMCPServer{ - {ID: "xworkmate_server", Command: "xworkmate-mcp", Args: []string{"--port", "3001"}, Enabled: true}, - }, - }, - OpencodeHome: tempDir, - }) - - content, err := os.ReadFile(configPath) - if err != nil { - t.Fatalf("read config: %v", err) - } - if !strings.Contains(string(content), `[model]`) { - t.Fatalf("expected user config preserved: %s", string(content)) - } - if !strings.Contains(string(content), `[mcp_servers.xworkmate_server]`) { - t.Fatalf("expected managed opencode entry written: %s", string(content)) - } - if strings.Count(string(content), opencodeManagedMCPBlockStart) != 1 { - t.Fatalf("expected single opencode managed block: %s", string(content)) - } - if result.MountTargets[4].ManagedMCPCount != 1 { - t.Fatalf("expected opencode managed count 1, got %d", result.MountTargets[4].ManagedMCPCount) - } -} - -func TestReconcileArisReportsReadyWhenBundleAndBridgeAreAvailable(t *testing.T) { - result := Reconcile(Request{ - Config: Config{UsesAris: true}, - Aris: ArisInput{ - Available: true, - BundleVersion: "test", - LLMChatServerPath: "mcp-server.py", - SkillCount: 2, - BridgeAvailable: true, - }, - }) - - if got := result.MountTargets[0].SyncState; got != "ready" { - t.Fatalf("expected ready aris state, got %q", got) - } - if got := result.ArisBundleVersion; got != "test" { - t.Fatalf("expected bundle version test, got %q", got) - } -} diff --git a/internal/shared/rpc.go b/internal/shared/rpc.go index 3864aa9..6e8495b 100644 --- a/internal/shared/rpc.go +++ b/internal/shared/rpc.go @@ -92,22 +92,3 @@ func NotificationEnvelope(method string, params map[string]any) map[string]any { "seq": 0, } } - -func ToolTextResult(id any, content string) map[string]any { - result := map[string]any{ - "content": []map[string]any{ - {"type": "text", "text": content}, - }, - } - return ResultEnvelope(id, result) -} - -func ToolErrorResult(id any, err error) map[string]any { - result := map[string]any{ - "content": []map[string]any{ - {"type": "text", "text": fmt.Sprintf("Error: %v", err)}, - }, - "isError": true, - } - return ResultEnvelope(id, result) -} diff --git a/internal/shared/tools.go b/internal/shared/tools.go index f925390..1e8b9f9 100644 --- a/internal/shared/tools.go +++ b/internal/shared/tools.go @@ -342,30 +342,6 @@ func HandleChatTool(arguments map[string]any) (string, error) { return CallOpenAICompatible(baseURL, apiKey, model, messages) } -func HandleClaudeReviewTool(arguments map[string]any) (string, error) { - prompt := strings.TrimSpace(StringArg(arguments, "prompt", "")) - if prompt == "" { - return "", errors.New("prompt is required") - } - model := strings.TrimSpace( - StringArg(arguments, "model", EnvOrDefault("CLAUDE_REVIEW_MODEL", "")), - ) - system := strings.TrimSpace( - StringArg(arguments, "system", EnvOrDefault("CLAUDE_REVIEW_SYSTEM", "")), - ) - tools := strings.TrimSpace( - StringArg(arguments, "tools", EnvOrDefault("CLAUDE_REVIEW_TOOLS", "")), - ) - timeout := IntArg(EnvOrDefault("CLAUDE_REVIEW_TIMEOUT_SEC", "600"), 600) - return RunClaudeReview( - prompt, - model, - system, - tools, - time.Duration(timeout)*time.Second, - ) -} - func CallOpenAICompatible( baseURL, apiKey,