From 424b5eb91d41483ff4f002b2ae5f28581369a5ac Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 16 Mar 2026 21:57:12 +0000 Subject: [PATCH] refactor: replace fmt.Errorf/os.* with go-io/go-log conventions Replace all fmt.Errorf and errors.New in production code with coreerr.E("caller.Method", "message", err) from go-log. Replace all os.ReadFile/os.WriteFile/os.MkdirAll/os.Remove with coreio.Local equivalents from go-io. Test files are untouched. 24 files across pkg/mcp/agentic/, pkg/mcp/brain/, pkg/mcp/ide/, pkg/mcp/, and cmd/brain-seed/. Co-Authored-By: Virgil --- cmd/brain-seed/main.go | 23 ++++++++------- pkg/mcp/agentic/dispatch.go | 18 +++++++----- pkg/mcp/agentic/epic.go | 13 +++++---- pkg/mcp/agentic/ingest.go | 12 ++++---- pkg/mcp/agentic/plan.go | 45 ++++++++++++++-------------- pkg/mcp/agentic/pr.go | 23 ++++++++------- pkg/mcp/agentic/prep.go | 58 ++++++++++++++++++------------------- pkg/mcp/agentic/queue.go | 5 ++-- pkg/mcp/agentic/resume.go | 16 +++++----- pkg/mcp/agentic/scan.go | 7 +++-- pkg/mcp/agentic/status.go | 15 +++++----- pkg/mcp/brain/brain.go | 4 +-- pkg/mcp/brain/direct.go | 20 +++++++------ pkg/mcp/brain/tools.go | 10 +++---- pkg/mcp/ide/bridge.go | 18 +++++------- pkg/mcp/ide/ide.go | 4 +-- pkg/mcp/ide/tools_chat.go | 4 +-- pkg/mcp/mcp.go | 36 +++++++++++------------ pkg/mcp/tools_metrics.go | 19 ++++++------ pkg/mcp/tools_process.go | 22 +++++++------- pkg/mcp/tools_rag.go | 19 ++++++------ pkg/mcp/tools_webview.go | 27 +++++++++-------- pkg/mcp/tools_ws.go | 2 +- pkg/mcp/transport_unix.go | 6 ++-- 24 files changed, 217 insertions(+), 209 deletions(-) diff --git a/cmd/brain-seed/main.go b/cmd/brain-seed/main.go index 692e1ae..3dd5f2a 100644 --- a/cmd/brain-seed/main.go +++ b/cmd/brain-seed/main.go @@ -26,6 +26,9 @@ import ( "regexp" "strings" "time" + + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" ) var ( @@ -272,26 +275,26 @@ func callBrainRemember(content, memType string, tags []string, project string, c body, err := json.Marshal(payload) if err != nil { - return fmt.Errorf("marshal: %w", err) + return coreerr.E("callBrainRemember", "marshal", err) } req, err := http.NewRequest("POST", *apiURL+"/tools/call", bytes.NewReader(body)) if err != nil { - return fmt.Errorf("request: %w", err) + return coreerr.E("callBrainRemember", "request", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+*apiKey) resp, err := httpClient.Do(req) if err != nil { - return fmt.Errorf("http: %w", err) + return coreerr.E("callBrainRemember", "http", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { - return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) + return coreerr.E("callBrainRemember", "HTTP "+string(respBody), nil) } var result struct { @@ -299,10 +302,10 @@ func callBrainRemember(content, memType string, tags []string, project string, c Error string `json:"error"` } if err := json.Unmarshal(respBody, &result); err != nil { - return fmt.Errorf("decode: %w", err) + return coreerr.E("callBrainRemember", "decode", err) } if !result.Success { - return fmt.Errorf("API: %s", result.Error) + return coreerr.E("callBrainRemember", "API: "+result.Error, nil) } return nil @@ -361,13 +364,13 @@ var headingRe = regexp.MustCompile(`^#{1,3}\s+(.+)$`) // parseMarkdownSections splits a markdown file by headings. func parseMarkdownSections(path string) []section { - data, err := os.ReadFile(path) + data, err := coreio.Local.Read(path) if err != nil || len(data) == 0 { return nil } var sections []section - lines := strings.Split(string(data), "\n") + lines := strings.Split(data, "\n") var curHeading string var curContent []string @@ -395,10 +398,10 @@ func parseMarkdownSections(path string) []section { } // If no headings found, treat entire file as one section - if len(sections) == 0 && strings.TrimSpace(string(data)) != "" { + if len(sections) == 0 && strings.TrimSpace(data) != "" { sections = append(sections, section{ heading: strings.TrimSuffix(filepath.Base(path), ".md"), - content: strings.TrimSpace(string(data)), + content: strings.TrimSpace(data), }) } diff --git a/pkg/mcp/agentic/dispatch.go b/pkg/mcp/agentic/dispatch.go index bb3b885..a8ca334 100644 --- a/pkg/mcp/agentic/dispatch.go +++ b/pkg/mcp/agentic/dispatch.go @@ -12,6 +12,8 @@ import ( "syscall" "time" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -77,16 +79,16 @@ func agentCommand(agent, prompt string) (string, []string, error) { script := filepath.Join(home, "Code", "core", "agent", "scripts", "local-agent.sh") return "bash", []string{script, prompt}, nil default: - return "", nil, fmt.Errorf("unknown agent: %s", agent) + return "", nil, coreerr.E("agentCommand", "unknown agent: "+agent, nil) } } func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input DispatchInput) (*mcp.CallToolResult, DispatchOutput, error) { if input.Repo == "" { - return nil, DispatchOutput{}, fmt.Errorf("repo is required") + return nil, DispatchOutput{}, coreerr.E("dispatch", "repo is required", nil) } if input.Task == "" { - return nil, DispatchOutput{}, fmt.Errorf("task is required") + return nil, DispatchOutput{}, coreerr.E("dispatch", "task is required", nil) } if input.Org == "" { input.Org = "core" @@ -111,7 +113,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, } _, prepOut, err := s.prepWorkspace(ctx, req, prepInput) if err != nil { - return nil, DispatchOutput{}, fmt.Errorf("prep workspace failed: %w", err) + return nil, DispatchOutput{}, coreerr.E("dispatch", "prep workspace failed", err) } wsDir := prepOut.WorkspaceDir @@ -122,13 +124,13 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, if input.DryRun { // Read PROMPT.md for the dry run output - promptContent, _ := os.ReadFile(filepath.Join(wsDir, "PROMPT.md")) + promptRaw, _ := coreio.Local.Read(filepath.Join(wsDir, "PROMPT.md")) return nil, DispatchOutput{ Success: true, Agent: input.Agent, Repo: input.Repo, WorkspaceDir: wsDir, - Prompt: string(promptContent), + Prompt: promptRaw, }, nil } @@ -164,7 +166,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", input.Agent)) outFile, err := os.Create(outputFile) if err != nil { - return nil, DispatchOutput{}, fmt.Errorf("failed to create log file: %w", err) + return nil, DispatchOutput{}, coreerr.E("dispatch", "failed to create log file", err) } // Fully detach from terminal: @@ -183,7 +185,7 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, if err := cmd.Start(); err != nil { outFile.Close() - return nil, DispatchOutput{}, fmt.Errorf("failed to spawn %s: %w", input.Agent, err) + return nil, DispatchOutput{}, coreerr.E("dispatch", "failed to spawn "+input.Agent, err) } pid := cmd.Process.Pid diff --git a/pkg/mcp/agentic/epic.go b/pkg/mcp/agentic/epic.go index dfce777..29924ce 100644 --- a/pkg/mcp/agentic/epic.go +++ b/pkg/mcp/agentic/epic.go @@ -10,6 +10,7 @@ import ( "net/http" "strings" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -53,13 +54,13 @@ func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) { func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest, input EpicInput) (*mcp.CallToolResult, EpicOutput, error) { if input.Title == "" { - return nil, EpicOutput{}, fmt.Errorf("title is required") + return nil, EpicOutput{}, coreerr.E("createEpic", "title is required", nil) } if len(input.Tasks) == 0 { - return nil, EpicOutput{}, fmt.Errorf("at least one task is required") + return nil, EpicOutput{}, coreerr.E("createEpic", "at least one task is required", nil) } if s.forgeToken == "" { - return nil, EpicOutput{}, fmt.Errorf("no Forge token configured") + return nil, EpicOutput{}, coreerr.E("createEpic", "no Forge token configured", nil) } if input.Org == "" { input.Org = "core" @@ -112,7 +113,7 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest epicLabels := append(labelIDs, s.resolveLabelIDs(ctx, input.Org, input.Repo, []string{"epic"})...) epic, err := s.createIssue(ctx, input.Org, input.Repo, input.Title, body.String(), epicLabels) if err != nil { - return nil, EpicOutput{}, fmt.Errorf("failed to create epic: %w", err) + return nil, EpicOutput{}, coreerr.E("createEpic", "failed to create epic", err) } out := EpicOutput{ @@ -162,12 +163,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body resp, err := s.client.Do(req) if err != nil { - return ChildRef{}, fmt.Errorf("create issue request failed: %w", err) + return ChildRef{}, coreerr.E("createIssue", "request failed", err) } defer resp.Body.Close() if resp.StatusCode != 201 { - return ChildRef{}, fmt.Errorf("create issue returned %d", resp.StatusCode) + return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("returned %d", resp.StatusCode), nil) } var result struct { diff --git a/pkg/mcp/agentic/ingest.go b/pkg/mcp/agentic/ingest.go index aafef73..bb683b6 100644 --- a/pkg/mcp/agentic/ingest.go +++ b/pkg/mcp/agentic/ingest.go @@ -10,6 +10,8 @@ import ( "os" "path/filepath" "strings" + + coreio "forge.lthn.ai/core/go-io" ) // ingestFindings reads the agent output log and creates issues via the API @@ -26,12 +28,12 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { return } - content, err := os.ReadFile(logFiles[0]) - if err != nil || len(content) < 100 { + contentStr, err := coreio.Local.Read(logFiles[0]) + if err != nil || len(contentStr) < 100 { return } - body := string(content) + body := contentStr // Skip quota errors if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { @@ -93,11 +95,11 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p // Read the agent API key from file home, _ := os.UserHomeDir() - apiKeyData, err := os.ReadFile(filepath.Join(home, ".claude", "agent-api.key")) + apiKeyData, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key")) if err != nil { return } - apiKey := strings.TrimSpace(string(apiKeyData)) + apiKey := strings.TrimSpace(apiKeyData) payload, _ := json.Marshal(map[string]string{ "title": title, diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index d37c377..cf4cf4e 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -7,12 +7,13 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" - "fmt" "os" "path/filepath" "strings" "time" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -145,10 +146,10 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCreateOutput, error) { if input.Title == "" { - return nil, PlanCreateOutput{}, fmt.Errorf("title is required") + return nil, PlanCreateOutput{}, coreerr.E("planCreate", "title is required", nil) } if input.Objective == "" { - return nil, PlanCreateOutput{}, fmt.Errorf("objective is required") + return nil, PlanCreateOutput{}, coreerr.E("planCreate", "objective is required", nil) } id := generatePlanID(input.Title) @@ -177,7 +178,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in path, err := writePlan(s.plansDir(), &plan) if err != nil { - return nil, PlanCreateOutput{}, fmt.Errorf("failed to write plan: %w", err) + return nil, PlanCreateOutput{}, coreerr.E("planCreate", "failed to write plan", err) } return nil, PlanCreateOutput{ @@ -189,7 +190,7 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, input PlanReadInput) (*mcp.CallToolResult, PlanReadOutput, error) { if input.ID == "" { - return nil, PlanReadOutput{}, fmt.Errorf("id is required") + return nil, PlanReadOutput{}, coreerr.E("planRead", "id is required", nil) } plan, err := readPlan(s.plansDir(), input.ID) @@ -205,7 +206,7 @@ func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, inpu func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, input PlanUpdateInput) (*mcp.CallToolResult, PlanUpdateOutput, error) { if input.ID == "" { - return nil, PlanUpdateOutput{}, fmt.Errorf("id is required") + return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "id is required", nil) } plan, err := readPlan(s.plansDir(), input.ID) @@ -216,7 +217,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in // Apply partial updates if input.Status != "" { if !validPlanStatus(input.Status) { - return nil, PlanUpdateOutput{}, fmt.Errorf("invalid status: %s (valid: draft, ready, in_progress, needs_verification, verified, approved)", input.Status) + return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "invalid status: "+input.Status+" (valid: draft, ready, in_progress, needs_verification, verified, approved)", nil) } plan.Status = input.Status } @@ -239,7 +240,7 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in plan.UpdatedAt = time.Now() if _, err := writePlan(s.plansDir(), plan); err != nil { - return nil, PlanUpdateOutput{}, fmt.Errorf("failed to write plan: %w", err) + return nil, PlanUpdateOutput{}, coreerr.E("planUpdate", "failed to write plan", err) } return nil, PlanUpdateOutput{ @@ -250,16 +251,16 @@ func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanDeleteOutput, error) { if input.ID == "" { - return nil, PlanDeleteOutput{}, fmt.Errorf("id is required") + return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "id is required", nil) } path := planPath(s.plansDir(), input.ID) if _, err := os.Stat(path); err != nil { - return nil, PlanDeleteOutput{}, fmt.Errorf("plan not found: %s", input.ID) + return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "plan not found: "+input.ID, nil) } - if err := os.Remove(path); err != nil { - return nil, PlanDeleteOutput{}, fmt.Errorf("failed to delete plan: %w", err) + if err := coreio.Local.Delete(path); err != nil { + return nil, PlanDeleteOutput{}, coreerr.E("planDelete", "failed to delete plan", err) } return nil, PlanDeleteOutput{ @@ -270,13 +271,13 @@ func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, in func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, PlanListOutput, error) { dir := s.plansDir() - if err := os.MkdirAll(dir, 0755); err != nil { - return nil, PlanListOutput{}, fmt.Errorf("failed to access plans directory: %w", err) + if err := coreio.Local.EnsureDir(dir); err != nil { + return nil, PlanListOutput{}, coreerr.E("planList", "failed to access plans directory", err) } entries, err := os.ReadDir(dir) if err != nil { - return nil, PlanListOutput{}, fmt.Errorf("failed to read plans directory: %w", err) + return nil, PlanListOutput{}, coreerr.E("planList", "failed to read plans directory", err) } var plans []Plan @@ -351,21 +352,21 @@ func generatePlanID(title string) string { } func readPlan(dir, id string) (*Plan, error) { - data, err := os.ReadFile(planPath(dir, id)) + data, err := coreio.Local.Read(planPath(dir, id)) if err != nil { - return nil, fmt.Errorf("plan not found: %s", id) + return nil, coreerr.E("readPlan", "plan not found: "+id, nil) } var plan Plan - if err := json.Unmarshal(data, &plan); err != nil { - return nil, fmt.Errorf("failed to parse plan %s: %w", id, err) + if err := json.Unmarshal([]byte(data), &plan); err != nil { + return nil, coreerr.E("readPlan", "failed to parse plan "+id, err) } return &plan, nil } func writePlan(dir string, plan *Plan) (string, error) { - if err := os.MkdirAll(dir, 0755); err != nil { - return "", fmt.Errorf("failed to create plans directory: %w", err) + if err := coreio.Local.EnsureDir(dir); err != nil { + return "", coreerr.E("writePlan", "failed to create plans directory", err) } path := planPath(dir, plan.ID) @@ -374,7 +375,7 @@ func writePlan(dir string, plan *Plan) (string, error) { return "", err } - return path, os.WriteFile(path, data, 0644) + return path, coreio.Local.Write(path, string(data)) } func validPlanStatus(status string) bool { diff --git a/pkg/mcp/agentic/pr.go b/pkg/mcp/agentic/pr.go index b86beb6..e200de6 100644 --- a/pkg/mcp/agentic/pr.go +++ b/pkg/mcp/agentic/pr.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -47,10 +48,10 @@ func (s *PrepSubsystem) registerCreatePRTool(server *mcp.Server) { func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, input CreatePRInput) (*mcp.CallToolResult, CreatePROutput, error) { if input.Workspace == "" { - return nil, CreatePROutput{}, fmt.Errorf("workspace is required") + return nil, CreatePROutput{}, coreerr.E("createPR", "workspace is required", nil) } if s.forgeToken == "" { - return nil, CreatePROutput{}, fmt.Errorf("no Forge token configured") + return nil, CreatePROutput{}, coreerr.E("createPR", "no Forge token configured", nil) } home, _ := os.UserHomeDir() @@ -58,13 +59,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in srcDir := filepath.Join(wsDir, "src") if _, err := os.Stat(srcDir); err != nil { - return nil, CreatePROutput{}, fmt.Errorf("workspace not found: %s", input.Workspace) + return nil, CreatePROutput{}, coreerr.E("createPR", "workspace not found: "+input.Workspace, nil) } // Read workspace status for repo, branch, issue context st, err := readStatus(wsDir) if err != nil { - return nil, CreatePROutput{}, fmt.Errorf("no status.json: %w", err) + return nil, CreatePROutput{}, coreerr.E("createPR", "no status.json", err) } if st.Branch == "" { @@ -73,7 +74,7 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in branchCmd.Dir = srcDir out, err := branchCmd.Output() if err != nil { - return nil, CreatePROutput{}, fmt.Errorf("failed to detect branch: %w", err) + return nil, CreatePROutput{}, coreerr.E("createPR", "failed to detect branch", err) } st.Branch = strings.TrimSpace(string(out)) } @@ -116,13 +117,13 @@ func (s *PrepSubsystem) createPR(ctx context.Context, _ *mcp.CallToolRequest, in pushCmd.Dir = srcDir pushOut, err := pushCmd.CombinedOutput() if err != nil { - return nil, CreatePROutput{}, fmt.Errorf("git push failed: %s: %w", string(pushOut), err) + return nil, CreatePROutput{}, coreerr.E("createPR", "git push failed: "+string(pushOut), err) } // Create PR via Forge API prURL, prNum, err := s.forgeCreatePR(ctx, org, st.Repo, st.Branch, base, title, body) if err != nil { - return nil, CreatePROutput{}, fmt.Errorf("failed to create PR: %w", err) + return nil, CreatePROutput{}, coreerr.E("createPR", "failed to create PR", err) } // Update status with PR URL @@ -177,7 +178,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base resp, err := s.client.Do(req) if err != nil { - return "", 0, fmt.Errorf("request failed: %w", err) + return "", 0, coreerr.E("forgeCreatePR", "request failed", err) } defer resp.Body.Close() @@ -185,7 +186,7 @@ func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base var errBody map[string]any json.NewDecoder(resp.Body).Decode(&errBody) msg, _ := errBody["message"].(string) - return "", 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, msg) + return "", 0, coreerr.E("forgeCreatePR", fmt.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil) } var pr struct { @@ -252,7 +253,7 @@ func (s *PrepSubsystem) registerListPRsTool(server *mcp.Server) { func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, input ListPRsInput) (*mcp.CallToolResult, ListPRsOutput, error) { if s.forgeToken == "" { - return nil, ListPRsOutput{}, fmt.Errorf("no Forge token configured") + return nil, ListPRsOutput{}, coreerr.E("listPRs", "no Forge token configured", nil) } if input.Org == "" { @@ -309,7 +310,7 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string resp, err := s.client.Do(req) if err != nil || resp.StatusCode != 200 { - return nil, fmt.Errorf("failed to list PRs for %s: %v", repo, err) + return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err) } defer resp.Body.Close() diff --git a/pkg/mcp/agentic/prep.go b/pkg/mcp/agentic/prep.go index 54af85a..a7be87b 100644 --- a/pkg/mcp/agentic/prep.go +++ b/pkg/mcp/agentic/prep.go @@ -17,6 +17,8 @@ import ( "strings" "time" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" "gopkg.in/yaml.v3" ) @@ -43,8 +45,8 @@ func NewPrep() *PrepSubsystem { brainKey := os.Getenv("CORE_BRAIN_KEY") if brainKey == "" { - if data, err := os.ReadFile(filepath.Join(home, ".claude", "brain.key")); err == nil { - brainKey = strings.TrimSpace(string(data)) + if data, err := coreio.Local.Read(filepath.Join(home, ".claude", "brain.key")); err == nil { + brainKey = strings.TrimSpace(data) } } @@ -122,7 +124,7 @@ type PrepOutput struct { func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolRequest, input PrepInput) (*mcp.CallToolResult, PrepOutput, error) { if input.Repo == "" { - return nil, PrepOutput{}, fmt.Errorf("repo is required") + return nil, PrepOutput{}, coreerr.E("prepWorkspace", "repo is required", nil) } if input.Org == "" { input.Org = "core" @@ -171,29 +173,29 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques branchCmd.Run() // Create context dirs inside src/ - os.MkdirAll(filepath.Join(srcDir, "kb"), 0755) - os.MkdirAll(filepath.Join(srcDir, "specs"), 0755) + coreio.Local.EnsureDir(filepath.Join(srcDir, "kb")) + coreio.Local.EnsureDir(filepath.Join(srcDir, "specs")) // Remote stays as local clone origin — agent cannot push to forge. // Reviewer pulls changes from workspace and pushes after verification. // 2. Copy CLAUDE.md and GEMINI.md to workspace claudeMdPath := filepath.Join(repoPath, "CLAUDE.md") - if data, err := os.ReadFile(claudeMdPath); err == nil { - os.WriteFile(filepath.Join(wsDir, "src", "CLAUDE.md"), data, 0644) + if data, err := coreio.Local.Read(claudeMdPath); err == nil { + coreio.Local.Write(filepath.Join(wsDir, "src", "CLAUDE.md"), data) out.ClaudeMd = true } // Copy GEMINI.md from core/agent (ethics framework for all agents) agentGeminiMd := filepath.Join(s.codePath, "core", "agent", "GEMINI.md") - if data, err := os.ReadFile(agentGeminiMd); err == nil { - os.WriteFile(filepath.Join(wsDir, "src", "GEMINI.md"), data, 0644) + if data, err := coreio.Local.Read(agentGeminiMd); err == nil { + coreio.Local.Write(filepath.Join(wsDir, "src", "GEMINI.md"), data) } // Copy persona if specified if input.Persona != "" { personaPath := filepath.Join(s.codePath, "core", "agent", "prompts", "personas", input.Persona+".md") - if data, err := os.ReadFile(personaPath); err == nil { - os.WriteFile(filepath.Join(wsDir, "src", "PERSONA.md"), data, 0644) + if data, err := coreio.Local.Read(personaPath); err == nil { + coreio.Local.Write(filepath.Join(wsDir, "src", "PERSONA.md"), data) } } @@ -203,7 +205,7 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques } else if input.Task != "" { todo := fmt.Sprintf("# TASK: %s\n\n**Repo:** %s/%s\n**Status:** ready\n\n## Objective\n\n%s\n", input.Task, input.Org, input.Repo, input.Task) - os.WriteFile(filepath.Join(wsDir, "src", "TODO.md"), []byte(todo), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), todo) } // 4. Generate CONTEXT.md from OpenBrain @@ -300,7 +302,7 @@ Do NOT push. Commit only — a reviewer will verify and push. prompt = "Read TODO.md and complete the task. Work in src/.\n" } - os.WriteFile(filepath.Join(wsDir, "src", "PROMPT.md"), []byte(prompt), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "PROMPT.md"), prompt) } // --- Plan template rendering --- @@ -310,18 +312,16 @@ Do NOT push. Commit only — a reviewer will verify and push. func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map[string]string, task string, wsDir string) { // Look for template in core/agent/prompts/templates/ templatePath := filepath.Join(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yaml") - data, err := os.ReadFile(templatePath) + content, err := coreio.Local.Read(templatePath) if err != nil { // Try .yml extension templatePath = filepath.Join(s.codePath, "core", "agent", "prompts", "templates", templateSlug+".yml") - data, err = os.ReadFile(templatePath) + content, err = coreio.Local.Read(templatePath) if err != nil { return // Template not found, skip silently } } - content := string(data) - // Substitute variables ({{variable_name}} → value) for key, value := range variables { content = strings.ReplaceAll(content, "{{"+key+"}}", value) @@ -380,7 +380,7 @@ func (s *PrepSubsystem) writePlanFromTemplate(templateSlug string, variables map plan.WriteString("\n**Commit after completing this phase.**\n\n---\n\n") } - os.WriteFile(filepath.Join(wsDir, "src", "PLAN.md"), []byte(plan.String()), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "PLAN.md"), plan.String()) } // --- Helpers (unchanged) --- @@ -440,7 +440,7 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i return '-' }, page.Title) + ".md" - os.WriteFile(filepath.Join(wsDir, "src", "kb", filename), content, 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "kb", filename), string(content)) count++ } @@ -453,8 +453,8 @@ func (s *PrepSubsystem) copySpecs(wsDir string) int { for _, file := range specFiles { src := filepath.Join(s.specsPath, file) - if data, err := os.ReadFile(src); err == nil { - os.WriteFile(filepath.Join(wsDir, "src", "specs", file), data, 0644) + if data, err := coreio.Local.Read(src); err == nil { + coreio.Local.Write(filepath.Join(wsDir, "src", "specs", file), data) count++ } } @@ -503,7 +503,7 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string) content.WriteString(fmt.Sprintf("### %d. %s [%s] (score: %.3f)\n\n%s\n\n", i+1, memProject, memType, score, memContent)) } - os.WriteFile(filepath.Join(wsDir, "src", "CONTEXT.md"), []byte(content.String()), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "CONTEXT.md"), content.String()) return len(result.Memories) } @@ -511,24 +511,24 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { goWorkPath := filepath.Join(s.codePath, "go.work") modulePath := "forge.lthn.ai/core/" + repo - workData, err := os.ReadFile(goWorkPath) + workData, err := coreio.Local.Read(goWorkPath) if err != nil { return 0 } var consumers []string - for _, line := range strings.Split(string(workData), "\n") { + for _, line := range strings.Split(workData, "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "./") { continue } dir := filepath.Join(s.codePath, strings.TrimPrefix(line, "./")) goMod := filepath.Join(dir, "go.mod") - modData, err := os.ReadFile(goMod) + modData, err := coreio.Local.Read(goMod) if err != nil { continue } - if strings.Contains(string(modData), modulePath) && !strings.HasPrefix(string(modData), "module "+modulePath) { + if strings.Contains(modData, modulePath) && !strings.HasPrefix(modData, "module "+modulePath) { consumers = append(consumers, filepath.Base(dir)) } } @@ -540,7 +540,7 @@ func (s *PrepSubsystem) findConsumers(repo, wsDir string) int { content += "- " + c + "\n" } content += fmt.Sprintf("\n**Breaking change risk: %d consumers.**\n", len(consumers)) - os.WriteFile(filepath.Join(wsDir, "src", "CONSUMERS.md"), []byte(content), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "CONSUMERS.md"), content) } return len(consumers) @@ -557,7 +557,7 @@ func (s *PrepSubsystem) gitLog(repoPath, wsDir string) int { lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) > 0 && lines[0] != "" { content := "# Recent Changes\n\n```\n" + string(output) + "```\n" - os.WriteFile(filepath.Join(wsDir, "src", "RECENT.md"), []byte(content), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "RECENT.md"), content) } return len(lines) @@ -590,5 +590,5 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu content += fmt.Sprintf("**Repo:** %s/%s\n\n---\n\n", org, repo) content += "## Objective\n\n" + issueData.Body + "\n" - os.WriteFile(filepath.Join(wsDir, "src", "TODO.md"), []byte(content), 0644) + coreio.Local.Write(filepath.Join(wsDir, "src", "TODO.md"), content) } diff --git a/pkg/mcp/agentic/queue.go b/pkg/mcp/agentic/queue.go index b0dba4b..f42add2 100644 --- a/pkg/mcp/agentic/queue.go +++ b/pkg/mcp/agentic/queue.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + coreio "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -48,12 +49,12 @@ func (s *PrepSubsystem) loadAgentsConfig() *AgentsConfig { } for _, path := range paths { - data, err := os.ReadFile(path) + data, err := coreio.Local.Read(path) if err != nil { continue } var cfg AgentsConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { + if err := yaml.Unmarshal([]byte(data), &cfg); err != nil { continue } return &cfg diff --git a/pkg/mcp/agentic/resume.go b/pkg/mcp/agentic/resume.go index 1abb8fe..dba9de8 100644 --- a/pkg/mcp/agentic/resume.go +++ b/pkg/mcp/agentic/resume.go @@ -10,6 +10,8 @@ import ( "path/filepath" "syscall" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -40,7 +42,7 @@ func (s *PrepSubsystem) registerResumeTool(server *mcp.Server) { func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, input ResumeInput) (*mcp.CallToolResult, ResumeOutput, error) { if input.Workspace == "" { - return nil, ResumeOutput{}, fmt.Errorf("workspace is required") + return nil, ResumeOutput{}, coreerr.E("resume", "workspace is required", nil) } home, _ := os.UserHomeDir() @@ -49,17 +51,17 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu // Verify workspace exists if _, err := os.Stat(srcDir); err != nil { - return nil, ResumeOutput{}, fmt.Errorf("workspace not found: %s", input.Workspace) + return nil, ResumeOutput{}, coreerr.E("resume", "workspace not found: "+input.Workspace, nil) } // Read current status st, err := readStatus(wsDir) if err != nil { - return nil, ResumeOutput{}, fmt.Errorf("no status.json in workspace: %w", err) + return nil, ResumeOutput{}, coreerr.E("resume", "no status.json in workspace", err) } if st.Status != "blocked" && st.Status != "failed" && st.Status != "completed" { - return nil, ResumeOutput{}, fmt.Errorf("workspace is %s, not resumable (must be blocked, failed, or completed)", st.Status) + return nil, ResumeOutput{}, coreerr.E("resume", "workspace is "+st.Status+", not resumable (must be blocked, failed, or completed)", nil) } // Determine agent @@ -72,8 +74,8 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu if input.Answer != "" { answerPath := filepath.Join(srcDir, "ANSWER.md") content := fmt.Sprintf("# Answer\n\n%s\n", input.Answer) - if err := os.WriteFile(answerPath, []byte(content), 0644); err != nil { - return nil, ResumeOutput{}, fmt.Errorf("failed to write ANSWER.md: %w", err) + if err := coreio.Local.Write(answerPath, content); err != nil { + return nil, ResumeOutput{}, coreerr.E("resume", "failed to write ANSWER.md", err) } } @@ -113,7 +115,7 @@ func (s *PrepSubsystem) resume(ctx context.Context, _ *mcp.CallToolRequest, inpu if err := cmd.Start(); err != nil { outFile.Close() - return nil, ResumeOutput{}, fmt.Errorf("failed to spawn %s: %w", agent, err) + return nil, ResumeOutput{}, coreerr.E("resume", "failed to spawn "+agent, err) } // Update status diff --git a/pkg/mcp/agentic/scan.go b/pkg/mcp/agentic/scan.go index 6b2525a..ca78717 100644 --- a/pkg/mcp/agentic/scan.go +++ b/pkg/mcp/agentic/scan.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -38,7 +39,7 @@ type ScanIssue struct { func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input ScanInput) (*mcp.CallToolResult, ScanOutput, error) { if s.forgeToken == "" { - return nil, ScanOutput{}, fmt.Errorf("no Forge token configured") + return nil, ScanOutput{}, coreerr.E("scan", "no Forge token configured", nil) } if input.Org == "" { @@ -105,7 +106,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, resp, err := s.client.Do(req) if err != nil || resp.StatusCode != 200 { - return nil, fmt.Errorf("failed to list repos: %v", err) + return nil, coreerr.E("listOrgRepos", "failed to list repos", err) } defer resp.Body.Close() @@ -129,7 +130,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str resp, err := s.client.Do(req) if err != nil || resp.StatusCode != 200 { - return nil, fmt.Errorf("failed to list issues for %s: %v", repo, err) + return nil, coreerr.E("listRepoIssues", "failed to list issues for "+repo, err) } defer resp.Body.Close() diff --git a/pkg/mcp/agentic/status.go b/pkg/mcp/agentic/status.go index e0e7b86..db30b33 100644 --- a/pkg/mcp/agentic/status.go +++ b/pkg/mcp/agentic/status.go @@ -5,12 +5,13 @@ package agentic import ( "context" "encoding/json" - "fmt" "os" "path/filepath" "strings" "time" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -49,16 +50,16 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error { if err != nil { return err } - return os.WriteFile(filepath.Join(wsDir, "status.json"), data, 0644) + return coreio.Local.Write(filepath.Join(wsDir, "status.json"), string(data)) } func readStatus(wsDir string) (*WorkspaceStatus, error) { - data, err := os.ReadFile(filepath.Join(wsDir, "status.json")) + data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json")) if err != nil { return nil, err } var s WorkspaceStatus - if err := json.Unmarshal(data, &s); err != nil { + if err := json.Unmarshal([]byte(data), &s); err != nil { return nil, err } return &s, nil @@ -99,7 +100,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu entries, err := os.ReadDir(wsRoot) if err != nil { - return nil, StatusOutput{}, fmt.Errorf("no workspaces found: %w", err) + return nil, StatusOutput{}, coreerr.E("status", "no workspaces found", err) } var workspaces []WorkspaceInfo @@ -150,9 +151,9 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu if err != nil || proc.Signal(nil) != nil { // Process died — check for BLOCKED.md blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") - if data, err := os.ReadFile(blockedPath); err == nil { + if data, err := coreio.Local.Read(blockedPath); err == nil { info.Status = "blocked" - info.Question = strings.TrimSpace(string(data)) + info.Question = strings.TrimSpace(data) st.Status = "blocked" st.Question = info.Question } else { diff --git a/pkg/mcp/brain/brain.go b/pkg/mcp/brain/brain.go index 2b2fd3e..1037d0a 100644 --- a/pkg/mcp/brain/brain.go +++ b/pkg/mcp/brain/brain.go @@ -6,15 +6,15 @@ package brain import ( "context" - "errors" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) // errBridgeNotAvailable is returned when a tool requires the Laravel bridge // but it has not been initialised (headless mode). -var errBridgeNotAvailable = errors.New("brain: bridge not available") +var errBridgeNotAvailable = coreerr.E("brain", "bridge not available", nil) // Subsystem implements mcp.Subsystem for OpenBrain knowledge store operations. // It proxies brain_* tool calls to the Laravel backend via the shared IDE bridge. diff --git a/pkg/mcp/brain/direct.go b/pkg/mcp/brain/direct.go index ec15517..de4cb2d 100644 --- a/pkg/mcp/brain/direct.go +++ b/pkg/mcp/brain/direct.go @@ -13,6 +13,8 @@ import ( "strings" "time" + coreio "forge.lthn.ai/core/go-io" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -36,8 +38,8 @@ func NewDirect() *DirectSubsystem { apiKey := os.Getenv("CORE_BRAIN_KEY") if apiKey == "" { - if data, err := os.ReadFile(os.ExpandEnv("$HOME/.claude/brain.key")); err == nil { - apiKey = strings.TrimSpace(string(data)) + if data, err := coreio.Local.Read(os.ExpandEnv("$HOME/.claude/brain.key")); err == nil { + apiKey = strings.TrimSpace(data) } } @@ -74,21 +76,21 @@ func (s *DirectSubsystem) Shutdown(_ context.Context) error { return nil } func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body any) (map[string]any, error) { if s.apiKey == "" { - return nil, fmt.Errorf("brain: no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)") + 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, fmt.Errorf("brain: marshal request: %w", err) + 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, fmt.Errorf("brain: create request: %w", err) + return nil, coreerr.E("brain.apiCall", "create request", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") @@ -96,22 +98,22 @@ func (s *DirectSubsystem) apiCall(ctx context.Context, method, path string, body resp, err := s.client.Do(req) if err != nil { - return nil, fmt.Errorf("brain: API call failed: %w", err) + 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, fmt.Errorf("brain: read response: %w", err) + return nil, coreerr.E("brain.apiCall", "read response", err) } if resp.StatusCode >= 400 { - return nil, fmt.Errorf("brain: API returned %d: %s", resp.StatusCode, string(respData)) + 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, fmt.Errorf("brain: parse response: %w", err) + return nil, coreerr.E("brain.apiCall", "parse response", err) } return result, nil diff --git a/pkg/mcp/brain/tools.go b/pkg/mcp/brain/tools.go index 9a8f12b..47d1e02 100644 --- a/pkg/mcp/brain/tools.go +++ b/pkg/mcp/brain/tools.go @@ -4,9 +4,9 @@ package brain import ( "context" - "fmt" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -140,7 +140,7 @@ func (s *Subsystem) brainRemember(_ context.Context, _ *mcp.CallToolRequest, inp }, }) if err != nil { - return nil, RememberOutput{}, fmt.Errorf("failed to send brain_remember: %w", err) + return nil, RememberOutput{}, coreerr.E("brain.remember", "failed to send brain_remember", err) } return nil, RememberOutput{ @@ -163,7 +163,7 @@ func (s *Subsystem) brainRecall(_ context.Context, _ *mcp.CallToolRequest, input }, }) if err != nil { - return nil, RecallOutput{}, fmt.Errorf("failed to send brain_recall: %w", err) + return nil, RecallOutput{}, coreerr.E("brain.recall", "failed to send brain_recall", err) } return nil, RecallOutput{ @@ -185,7 +185,7 @@ func (s *Subsystem) brainForget(_ context.Context, _ *mcp.CallToolRequest, input }, }) if err != nil { - return nil, ForgetOutput{}, fmt.Errorf("failed to send brain_forget: %w", err) + return nil, ForgetOutput{}, coreerr.E("brain.forget", "failed to send brain_forget", err) } return nil, ForgetOutput{ @@ -210,7 +210,7 @@ func (s *Subsystem) brainList(_ context.Context, _ *mcp.CallToolRequest, input L }, }) if err != nil { - return nil, ListOutput{}, fmt.Errorf("failed to send brain_list: %w", err) + return nil, ListOutput{}, coreerr.E("brain.list", "failed to send brain_list", err) } return nil, ListOutput{ diff --git a/pkg/mcp/ide/bridge.go b/pkg/mcp/ide/bridge.go index 56ce884..40351cd 100644 --- a/pkg/mcp/ide/bridge.go +++ b/pkg/mcp/ide/bridge.go @@ -3,13 +3,11 @@ package ide import ( "context" "encoding/json" - "errors" - "fmt" - "log" "net/http" "sync" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-ws" "github.com/gorilla/websocket" ) @@ -74,12 +72,12 @@ func (b *Bridge) Send(msg BridgeMessage) error { b.mu.Lock() defer b.mu.Unlock() if b.conn == nil { - return errors.New("bridge: not connected") + return coreerr.E("bridge.Send", "not connected", nil) } msg.Timestamp = time.Now() data, err := json.Marshal(msg) if err != nil { - return fmt.Errorf("bridge: marshal failed: %w", err) + return coreerr.E("bridge.Send", "marshal failed", err) } return b.conn.WriteMessage(websocket.TextMessage, data) } @@ -95,7 +93,7 @@ func (b *Bridge) connectLoop(ctx context.Context) { } if err := b.dial(ctx); err != nil { - log.Printf("ide bridge: connect failed: %v", err) + coreerr.Warn("ide bridge: connect failed", "err", err) select { case <-ctx.Done(): return @@ -132,7 +130,7 @@ func (b *Bridge) dial(ctx context.Context) error { b.connected = true b.mu.Unlock() - log.Printf("ide bridge: connected to %s", b.cfg.LaravelWSURL) + coreerr.Info("ide bridge: connected", "url", b.cfg.LaravelWSURL) return nil } @@ -155,13 +153,13 @@ func (b *Bridge) readLoop(ctx context.Context) { _, data, err := b.conn.ReadMessage() if err != nil { - log.Printf("ide bridge: read error: %v", err) + coreerr.Warn("ide bridge: read error", "err", err) return } var msg BridgeMessage if err := json.Unmarshal(data, &msg); err != nil { - log.Printf("ide bridge: unmarshal error: %v", err) + coreerr.Warn("ide bridge: unmarshal error", "err", err) continue } @@ -186,6 +184,6 @@ func (b *Bridge) dispatch(msg BridgeMessage) { } if err := b.hub.SendToChannel(channel, wsMsg); err != nil { - log.Printf("ide bridge: dispatch to %s failed: %v", channel, err) + coreerr.Warn("ide bridge: dispatch failed", "channel", channel, "err", err) } } diff --git a/pkg/mcp/ide/ide.go b/pkg/mcp/ide/ide.go index ba3a833..64c4274 100644 --- a/pkg/mcp/ide/ide.go +++ b/pkg/mcp/ide/ide.go @@ -2,15 +2,15 @@ package ide import ( "context" - "errors" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-ws" "github.com/modelcontextprotocol/go-sdk/mcp" ) // errBridgeNotAvailable is returned when a tool requires the Laravel bridge // but it has not been initialised (headless mode). -var errBridgeNotAvailable = errors.New("bridge not available") +var errBridgeNotAvailable = coreerr.E("ide", "bridge not available", nil) // Subsystem implements mcp.Subsystem and mcp.SubsystemWithShutdown for the IDE. type Subsystem struct { diff --git a/pkg/mcp/ide/tools_chat.go b/pkg/mcp/ide/tools_chat.go index bbdc6b0..d4ac1bf 100644 --- a/pkg/mcp/ide/tools_chat.go +++ b/pkg/mcp/ide/tools_chat.go @@ -2,9 +2,9 @@ package ide import ( "context" - "fmt" "time" + coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -126,7 +126,7 @@ func (s *Subsystem) chatSend(_ context.Context, _ *mcp.CallToolRequest, input Ch Data: input.Message, }) if err != nil { - return nil, ChatSendOutput{}, fmt.Errorf("failed to send message: %w", err) + return nil, ChatSendOutput{}, coreerr.E("ide.chatSend", "failed to send message", err) } return nil, ChatSendOutput{ Sent: true, diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 7854cf3..28c72de 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -6,8 +6,6 @@ package mcp import ( "context" - "errors" - "fmt" "iter" "net/http" "os" @@ -54,11 +52,11 @@ func WithWorkspaceRoot(root string) Option { // Create sandboxed medium for this workspace abs, err := filepath.Abs(root) if err != nil { - return fmt.Errorf("invalid workspace root: %w", err) + return log.E("WithWorkspaceRoot", "invalid workspace root", err) } m, err := io.NewSandboxed(abs) if err != nil { - return fmt.Errorf("failed to create workspace medium: %w", err) + return log.E("WithWorkspaceRoot", "failed to create workspace medium", err) } s.workspaceRoot = abs s.medium = m @@ -85,19 +83,19 @@ func New(opts ...Option) (*Service, error) { // Default to current working directory with sandboxed medium cwd, err := os.Getwd() if err != nil { - return nil, fmt.Errorf("failed to get working directory: %w", err) + return nil, log.E("mcp.New", "failed to get working directory", err) } s.workspaceRoot = cwd m, err := io.NewSandboxed(cwd) if err != nil { - return nil, fmt.Errorf("failed to create sandboxed medium: %w", err) + return nil, log.E("mcp.New", "failed to create sandboxed medium", err) } s.medium = m // Apply options for _, opt := range opts { if err := opt(s); err != nil { - return nil, fmt.Errorf("failed to apply option: %w", err) + return nil, log.E("mcp.New", "failed to apply option", err) } } @@ -136,7 +134,7 @@ func (s *Service) Shutdown(ctx context.Context) error { for _, sub := range s.subsystems { if sh, ok := sub.(SubsystemWithShutdown); ok { if err := sh.Shutdown(ctx); err != nil { - return fmt.Errorf("shutdown %s: %w", sub.Name(), err) + return log.E("mcp.Shutdown", "shutdown "+sub.Name(), err) } } } @@ -363,7 +361,7 @@ type EditDiffOutput struct { func (s *Service) readFile(ctx context.Context, req *mcp.CallToolRequest, input ReadFileInput) (*mcp.CallToolResult, ReadFileOutput, error) { content, err := s.medium.Read(input.Path) if err != nil { - return nil, ReadFileOutput{}, fmt.Errorf("failed to read file: %w", err) + return nil, ReadFileOutput{}, log.E("mcp.readFile", "failed to read file", err) } return nil, ReadFileOutput{ Content: content, @@ -375,7 +373,7 @@ func (s *Service) readFile(ctx context.Context, req *mcp.CallToolRequest, input func (s *Service) writeFile(ctx context.Context, req *mcp.CallToolRequest, input WriteFileInput) (*mcp.CallToolResult, WriteFileOutput, error) { // Medium.Write creates parent directories automatically if err := s.medium.Write(input.Path, input.Content); err != nil { - return nil, WriteFileOutput{}, fmt.Errorf("failed to write file: %w", err) + return nil, WriteFileOutput{}, log.E("mcp.writeFile", "failed to write file", err) } return nil, WriteFileOutput{Success: true, Path: input.Path}, nil } @@ -383,7 +381,7 @@ func (s *Service) writeFile(ctx context.Context, req *mcp.CallToolRequest, input func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, input ListDirectoryInput) (*mcp.CallToolResult, ListDirectoryOutput, error) { entries, err := s.medium.List(input.Path) if err != nil { - return nil, ListDirectoryOutput{}, fmt.Errorf("failed to list directory: %w", err) + return nil, ListDirectoryOutput{}, log.E("mcp.listDirectory", "failed to list directory", err) } result := make([]DirectoryEntry, 0, len(entries)) for _, e := range entries { @@ -407,21 +405,21 @@ func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, i func (s *Service) createDirectory(ctx context.Context, req *mcp.CallToolRequest, input CreateDirectoryInput) (*mcp.CallToolResult, CreateDirectoryOutput, error) { if err := s.medium.EnsureDir(input.Path); err != nil { - return nil, CreateDirectoryOutput{}, fmt.Errorf("failed to create directory: %w", err) + return nil, CreateDirectoryOutput{}, log.E("mcp.createDirectory", "failed to create directory", err) } return nil, CreateDirectoryOutput{Success: true, Path: input.Path}, nil } func (s *Service) deleteFile(ctx context.Context, req *mcp.CallToolRequest, input DeleteFileInput) (*mcp.CallToolResult, DeleteFileOutput, error) { if err := s.medium.Delete(input.Path); err != nil { - return nil, DeleteFileOutput{}, fmt.Errorf("failed to delete file: %w", err) + return nil, DeleteFileOutput{}, log.E("mcp.deleteFile", "failed to delete file", err) } return nil, DeleteFileOutput{Success: true, Path: input.Path}, nil } func (s *Service) renameFile(ctx context.Context, req *mcp.CallToolRequest, input RenameFileInput) (*mcp.CallToolResult, RenameFileOutput, error) { if err := s.medium.Rename(input.OldPath, input.NewPath); err != nil { - return nil, RenameFileOutput{}, fmt.Errorf("failed to rename file: %w", err) + return nil, RenameFileOutput{}, log.E("mcp.renameFile", "failed to rename file", err) } return nil, RenameFileOutput{Success: true, OldPath: input.OldPath, NewPath: input.NewPath}, nil } @@ -472,12 +470,12 @@ func (s *Service) getSupportedLanguages(ctx context.Context, req *mcp.CallToolRe func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input EditDiffInput) (*mcp.CallToolResult, EditDiffOutput, error) { if input.OldString == "" { - return nil, EditDiffOutput{}, errors.New("old_string cannot be empty") + return nil, EditDiffOutput{}, log.E("mcp.editDiff", "old_string cannot be empty", nil) } content, err := s.medium.Read(input.Path) if err != nil { - return nil, EditDiffOutput{}, fmt.Errorf("failed to read file: %w", err) + return nil, EditDiffOutput{}, log.E("mcp.editDiff", "failed to read file", err) } count := 0 @@ -485,19 +483,19 @@ func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input if input.ReplaceAll { count = strings.Count(content, input.OldString) if count == 0 { - return nil, EditDiffOutput{}, errors.New("old_string not found in file") + return nil, EditDiffOutput{}, log.E("mcp.editDiff", "old_string not found in file", nil) } content = strings.ReplaceAll(content, input.OldString, input.NewString) } else { if !strings.Contains(content, input.OldString) { - return nil, EditDiffOutput{}, errors.New("old_string not found in file") + return nil, EditDiffOutput{}, log.E("mcp.editDiff", "old_string not found in file", nil) } content = strings.Replace(content, input.OldString, input.NewString, 1) count = 1 } if err := s.medium.Write(input.Path, content); err != nil { - return nil, EditDiffOutput{}, fmt.Errorf("failed to write file: %w", err) + return nil, EditDiffOutput{}, log.E("mcp.editDiff", "failed to write file", err) } return nil, EditDiffOutput{ diff --git a/pkg/mcp/tools_metrics.go b/pkg/mcp/tools_metrics.go index d0e3811..7fac228 100644 --- a/pkg/mcp/tools_metrics.go +++ b/pkg/mcp/tools_metrics.go @@ -2,7 +2,6 @@ package mcp import ( "context" - "errors" "fmt" "strconv" "strings" @@ -80,7 +79,7 @@ func (s *Service) metricsRecord(ctx context.Context, req *mcp.CallToolRequest, i // Validate input if input.Type == "" { - return nil, MetricsRecordOutput{}, errors.New("type cannot be empty") + return nil, MetricsRecordOutput{}, log.E("metricsRecord", "type cannot be empty", nil) } // Create the event @@ -95,7 +94,7 @@ func (s *Service) metricsRecord(ctx context.Context, req *mcp.CallToolRequest, i // Record the event if err := ai.Record(event); err != nil { log.Error("mcp: metrics record failed", "type", input.Type, "err", err) - return nil, MetricsRecordOutput{}, fmt.Errorf("failed to record metrics: %w", err) + return nil, MetricsRecordOutput{}, log.E("metricsRecord", "failed to record metrics", err) } return nil, MetricsRecordOutput{ @@ -117,7 +116,7 @@ func (s *Service) metricsQuery(ctx context.Context, req *mcp.CallToolRequest, in // Parse the duration duration, err := parseDuration(since) if err != nil { - return nil, MetricsQueryOutput{}, fmt.Errorf("invalid since value: %w", err) + return nil, MetricsQueryOutput{}, log.E("metricsQuery", "invalid since value", err) } sinceTime := time.Now().Add(-duration) @@ -126,7 +125,7 @@ func (s *Service) metricsQuery(ctx context.Context, req *mcp.CallToolRequest, in events, err := ai.ReadEvents(sinceTime) if err != nil { log.Error("mcp: metrics query failed", "since", since, "err", err) - return nil, MetricsQueryOutput{}, fmt.Errorf("failed to read metrics: %w", err) + return nil, MetricsQueryOutput{}, log.E("metricsQuery", "failed to read metrics", err) } // Get summary @@ -179,12 +178,12 @@ func convertMetricCounts(data any) []MetricCount { // parseDuration parses a duration string like "7d", "24h", "30m". func parseDuration(s string) (time.Duration, error) { if s == "" { - return 0, errors.New("duration cannot be empty") + return 0, log.E("parseDuration", "duration cannot be empty", nil) } s = strings.TrimSpace(s) if len(s) < 2 { - return 0, fmt.Errorf("invalid duration format: %q", s) + return 0, log.E("parseDuration", "invalid duration format: "+s, nil) } // Get the numeric part and unit @@ -193,11 +192,11 @@ func parseDuration(s string) (time.Duration, error) { num, err := strconv.Atoi(numStr) if err != nil { - return 0, fmt.Errorf("invalid duration number: %q", numStr) + return 0, log.E("parseDuration", "invalid duration number: "+numStr, err) } if num <= 0 { - return 0, fmt.Errorf("duration must be positive: %d", num) + return 0, log.E("parseDuration", fmt.Sprintf("duration must be positive: %d", num), nil) } switch unit { @@ -208,6 +207,6 @@ func parseDuration(s string) (time.Duration, error) { case 'm': return time.Duration(num) * time.Minute, nil default: - return 0, fmt.Errorf("invalid duration unit: %q (expected d, h, or m)", string(unit)) + return 0, log.E("parseDuration", "invalid duration unit: "+string(unit)+" (expected d, h, or m)", nil) } } diff --git a/pkg/mcp/tools_process.go b/pkg/mcp/tools_process.go index 9fab75b..6c41fc7 100644 --- a/pkg/mcp/tools_process.go +++ b/pkg/mcp/tools_process.go @@ -2,8 +2,6 @@ package mcp import ( "context" - "errors" - "fmt" "time" "forge.lthn.ai/core/go-log" @@ -12,7 +10,7 @@ import ( ) // errIDEmpty is returned when a process tool call omits the required ID. -var errIDEmpty = errors.New("id cannot be empty") +var errIDEmpty = log.E("process", "id cannot be empty", nil) // ProcessStartInput contains parameters for starting a new process. type ProcessStartInput struct { @@ -148,7 +146,7 @@ func (s *Service) processStart(ctx context.Context, req *mcp.CallToolRequest, in s.logger.Security("MCP tool execution", "tool", "process_start", "command", input.Command, "args", input.Args, "dir", input.Dir, "user", log.Username()) if input.Command == "" { - return nil, ProcessStartOutput{}, errors.New("command cannot be empty") + return nil, ProcessStartOutput{}, log.E("processStart", "command cannot be empty", nil) } opts := process.RunOptions{ @@ -161,7 +159,7 @@ func (s *Service) processStart(ctx context.Context, req *mcp.CallToolRequest, in proc, err := s.processService.StartWithOptions(ctx, opts) if err != nil { log.Error("mcp: process start failed", "command", input.Command, "err", err) - return nil, ProcessStartOutput{}, fmt.Errorf("failed to start process: %w", err) + return nil, ProcessStartOutput{}, log.E("processStart", "failed to start process", err) } info := proc.Info() @@ -185,14 +183,14 @@ func (s *Service) processStop(ctx context.Context, req *mcp.CallToolRequest, inp proc, err := s.processService.Get(input.ID) if err != nil { log.Error("mcp: process stop failed", "id", input.ID, "err", err) - return nil, ProcessStopOutput{}, fmt.Errorf("process not found: %w", err) + return nil, ProcessStopOutput{}, log.E("processStop", "process not found", err) } // For graceful stop, we use Kill() which sends SIGKILL // A more sophisticated implementation could use SIGTERM first if err := proc.Kill(); err != nil { log.Error("mcp: process stop kill failed", "id", input.ID, "err", err) - return nil, ProcessStopOutput{}, fmt.Errorf("failed to stop process: %w", err) + return nil, ProcessStopOutput{}, log.E("processStop", "failed to stop process", err) } return nil, ProcessStopOutput{ @@ -212,7 +210,7 @@ func (s *Service) processKill(ctx context.Context, req *mcp.CallToolRequest, inp if err := s.processService.Kill(input.ID); err != nil { log.Error("mcp: process kill failed", "id", input.ID, "err", err) - return nil, ProcessKillOutput{}, fmt.Errorf("failed to kill process: %w", err) + return nil, ProcessKillOutput{}, log.E("processKill", "failed to kill process", err) } return nil, ProcessKillOutput{ @@ -266,7 +264,7 @@ func (s *Service) processOutput(ctx context.Context, req *mcp.CallToolRequest, i output, err := s.processService.Output(input.ID) if err != nil { log.Error("mcp: process output failed", "id", input.ID, "err", err) - return nil, ProcessOutputOutput{}, fmt.Errorf("failed to get process output: %w", err) + return nil, ProcessOutputOutput{}, log.E("processOutput", "failed to get process output", err) } return nil, ProcessOutputOutput{ @@ -283,18 +281,18 @@ func (s *Service) processInput(ctx context.Context, req *mcp.CallToolRequest, in return nil, ProcessInputOutput{}, errIDEmpty } if input.Input == "" { - return nil, ProcessInputOutput{}, errors.New("input cannot be empty") + return nil, ProcessInputOutput{}, log.E("processInput", "input cannot be empty", nil) } proc, err := s.processService.Get(input.ID) if err != nil { log.Error("mcp: process input get failed", "id", input.ID, "err", err) - return nil, ProcessInputOutput{}, fmt.Errorf("process not found: %w", err) + return nil, ProcessInputOutput{}, log.E("processInput", "process not found", err) } if err := proc.SendInput(input.Input); err != nil { log.Error("mcp: process input send failed", "id", input.ID, "err", err) - return nil, ProcessInputOutput{}, fmt.Errorf("failed to send input: %w", err) + return nil, ProcessInputOutput{}, log.E("processInput", "failed to send input", err) } return nil, ProcessInputOutput{ diff --git a/pkg/mcp/tools_rag.go b/pkg/mcp/tools_rag.go index 89499f1..96ee716 100644 --- a/pkg/mcp/tools_rag.go +++ b/pkg/mcp/tools_rag.go @@ -2,11 +2,10 @@ package mcp import ( "context" - "errors" "fmt" - "forge.lthn.ai/core/go-rag" "forge.lthn.ai/core/go-log" + "forge.lthn.ai/core/go-rag" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -108,14 +107,14 @@ func (s *Service) ragQuery(ctx context.Context, req *mcp.CallToolRequest, input // Validate input if input.Question == "" { - return nil, RAGQueryOutput{}, errors.New("question cannot be empty") + return nil, RAGQueryOutput{}, log.E("ragQuery", "question cannot be empty", nil) } // Call the RAG query function results, err := rag.QueryDocs(ctx, input.Question, collection, topK) if err != nil { log.Error("mcp: rag query failed", "question", input.Question, "collection", collection, "err", err) - return nil, RAGQueryOutput{}, fmt.Errorf("failed to query RAG: %w", err) + return nil, RAGQueryOutput{}, log.E("ragQuery", "failed to query RAG", err) } // Convert results @@ -151,14 +150,14 @@ func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input // Validate input if input.Path == "" { - return nil, RAGIngestOutput{}, errors.New("path cannot be empty") + return nil, RAGIngestOutput{}, log.E("ragIngest", "path cannot be empty", nil) } // Check if path is a file or directory using the medium info, err := s.medium.Stat(input.Path) if err != nil { log.Error("mcp: rag ingest stat failed", "path", input.Path, "err", err) - return nil, RAGIngestOutput{}, fmt.Errorf("failed to access path: %w", err) + return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to access path", err) } var message string @@ -168,7 +167,7 @@ func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input err = rag.IngestDirectory(ctx, input.Path, collection, input.Recreate) if err != nil { log.Error("mcp: rag ingest directory failed", "path", input.Path, "collection", collection, "err", err) - return nil, RAGIngestOutput{}, fmt.Errorf("failed to ingest directory: %w", err) + return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to ingest directory", err) } message = fmt.Sprintf("Successfully ingested directory %s into collection %s", input.Path, collection) } else { @@ -176,7 +175,7 @@ func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input chunks, err = rag.IngestSingleFile(ctx, input.Path, collection) if err != nil { log.Error("mcp: rag ingest file failed", "path", input.Path, "collection", collection, "err", err) - return nil, RAGIngestOutput{}, fmt.Errorf("failed to ingest file: %w", err) + return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to ingest file", err) } message = fmt.Sprintf("Successfully ingested file %s (%d chunks) into collection %s", input.Path, chunks, collection) } @@ -198,7 +197,7 @@ func (s *Service) ragCollections(ctx context.Context, req *mcp.CallToolRequest, qdrantClient, err := rag.NewQdrantClient(rag.DefaultQdrantConfig()) if err != nil { log.Error("mcp: rag collections connect failed", "err", err) - return nil, RAGCollectionsOutput{}, fmt.Errorf("failed to connect to Qdrant: %w", err) + return nil, RAGCollectionsOutput{}, log.E("ragCollections", "failed to connect to Qdrant", err) } defer func() { _ = qdrantClient.Close() }() @@ -206,7 +205,7 @@ func (s *Service) ragCollections(ctx context.Context, req *mcp.CallToolRequest, collectionNames, err := qdrantClient.ListCollections(ctx) if err != nil { log.Error("mcp: rag collections list failed", "err", err) - return nil, RAGCollectionsOutput{}, fmt.Errorf("failed to list collections: %w", err) + return nil, RAGCollectionsOutput{}, log.E("ragCollections", "failed to list collections", err) } // Build collection info list diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index 8fbf941..7bc1e32 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -3,7 +3,6 @@ package mcp import ( "context" "encoding/base64" - "errors" "fmt" "time" @@ -18,8 +17,8 @@ var webviewInstance *webview.Webview // Sentinel errors for webview tools. var ( - errNotConnected = errors.New("not connected; use webview_connect first") - errSelectorRequired = errors.New("selector is required") + errNotConnected = log.E("webview", "not connected; use webview_connect first", nil) + errSelectorRequired = log.E("webview", "selector is required", nil) ) // WebviewConnectInput contains parameters for connecting to Chrome DevTools. @@ -210,7 +209,7 @@ func (s *Service) webviewConnect(ctx context.Context, req *mcp.CallToolRequest, s.logger.Security("MCP tool execution", "tool", "webview_connect", "debug_url", input.DebugURL, "user", log.Username()) if input.DebugURL == "" { - return nil, WebviewConnectOutput{}, errors.New("debug_url is required") + return nil, WebviewConnectOutput{}, log.E("webviewConnect", "debug_url is required", nil) } // Close existing connection if any @@ -232,7 +231,7 @@ func (s *Service) webviewConnect(ctx context.Context, req *mcp.CallToolRequest, wv, err := webview.New(opts...) if err != nil { log.Error("mcp: webview connect failed", "debug_url", input.DebugURL, "err", err) - return nil, WebviewConnectOutput{}, fmt.Errorf("failed to connect: %w", err) + return nil, WebviewConnectOutput{}, log.E("webviewConnect", "failed to connect", err) } webviewInstance = wv @@ -256,7 +255,7 @@ func (s *Service) webviewDisconnect(ctx context.Context, req *mcp.CallToolReques if err := webviewInstance.Close(); err != nil { log.Error("mcp: webview disconnect failed", "err", err) - return nil, WebviewDisconnectOutput{}, fmt.Errorf("failed to disconnect: %w", err) + return nil, WebviewDisconnectOutput{}, log.E("webviewDisconnect", "failed to disconnect", err) } webviewInstance = nil @@ -276,12 +275,12 @@ func (s *Service) webviewNavigate(ctx context.Context, req *mcp.CallToolRequest, } if input.URL == "" { - return nil, WebviewNavigateOutput{}, errors.New("url is required") + return nil, WebviewNavigateOutput{}, log.E("webviewNavigate", "url is required", nil) } if err := webviewInstance.Navigate(input.URL); err != nil { log.Error("mcp: webview navigate failed", "url", input.URL, "err", err) - return nil, WebviewNavigateOutput{}, fmt.Errorf("failed to navigate: %w", err) + return nil, WebviewNavigateOutput{}, log.E("webviewNavigate", "failed to navigate", err) } return nil, WebviewNavigateOutput{ @@ -304,7 +303,7 @@ func (s *Service) webviewClick(ctx context.Context, req *mcp.CallToolRequest, in if err := webviewInstance.Click(input.Selector); err != nil { log.Error("mcp: webview click failed", "selector", input.Selector, "err", err) - return nil, WebviewClickOutput{}, fmt.Errorf("failed to click: %w", err) + return nil, WebviewClickOutput{}, log.E("webviewClick", "failed to click", err) } return nil, WebviewClickOutput{Success: true}, nil @@ -324,7 +323,7 @@ func (s *Service) webviewType(ctx context.Context, req *mcp.CallToolRequest, inp if err := webviewInstance.Type(input.Selector, input.Text); err != nil { log.Error("mcp: webview type failed", "selector", input.Selector, "err", err) - return nil, WebviewTypeOutput{}, fmt.Errorf("failed to type: %w", err) + return nil, WebviewTypeOutput{}, log.E("webviewType", "failed to type", err) } return nil, WebviewTypeOutput{Success: true}, nil @@ -346,7 +345,7 @@ func (s *Service) webviewQuery(ctx context.Context, req *mcp.CallToolRequest, in elements, err := webviewInstance.QuerySelectorAll(input.Selector) if err != nil { log.Error("mcp: webview query all failed", "selector", input.Selector, "err", err) - return nil, WebviewQueryOutput{}, fmt.Errorf("failed to query: %w", err) + return nil, WebviewQueryOutput{}, log.E("webviewQuery", "failed to query", err) } output := WebviewQueryOutput{ @@ -429,7 +428,7 @@ func (s *Service) webviewEval(ctx context.Context, req *mcp.CallToolRequest, inp } if input.Script == "" { - return nil, WebviewEvalOutput{}, errors.New("script is required") + return nil, WebviewEvalOutput{}, log.E("webviewEval", "script is required", nil) } result, err := webviewInstance.Evaluate(input.Script) @@ -463,7 +462,7 @@ func (s *Service) webviewScreenshot(ctx context.Context, req *mcp.CallToolReques data, err := webviewInstance.Screenshot() if err != nil { log.Error("mcp: webview screenshot failed", "err", err) - return nil, WebviewScreenshotOutput{}, fmt.Errorf("failed to capture screenshot: %w", err) + return nil, WebviewScreenshotOutput{}, log.E("webviewScreenshot", "failed to capture screenshot", err) } return nil, WebviewScreenshotOutput{ @@ -487,7 +486,7 @@ func (s *Service) webviewWait(ctx context.Context, req *mcp.CallToolRequest, inp if err := webviewInstance.WaitForSelector(input.Selector); err != nil { log.Error("mcp: webview wait failed", "selector", input.Selector, "err", err) - return nil, WebviewWaitOutput{}, fmt.Errorf("failed to wait for selector: %w", err) + return nil, WebviewWaitOutput{}, log.E("webviewWait", "failed to wait for selector", err) } return nil, WebviewWaitOutput{ diff --git a/pkg/mcp/tools_ws.go b/pkg/mcp/tools_ws.go index ccae53c..254ee3b 100644 --- a/pkg/mcp/tools_ws.go +++ b/pkg/mcp/tools_ws.go @@ -83,7 +83,7 @@ func (s *Service) wsStart(ctx context.Context, req *mcp.CallToolRequest, input W ln, err := net.Listen("tcp", addr) if err != nil { log.Error("mcp: ws start listen failed", "addr", addr, "err", err) - return nil, WSStartOutput{}, fmt.Errorf("failed to listen on %s: %w", addr, err) + return nil, WSStartOutput{}, log.E("wsStart", "failed to listen on "+addr, err) } actualAddr := ln.Addr().String() diff --git a/pkg/mcp/transport_unix.go b/pkg/mcp/transport_unix.go index c70d5d9..0506231 100644 --- a/pkg/mcp/transport_unix.go +++ b/pkg/mcp/transport_unix.go @@ -3,8 +3,8 @@ package mcp import ( "context" "net" - "os" + "forge.lthn.ai/core/go-io" "forge.lthn.ai/core/go-log" ) @@ -13,7 +13,7 @@ import ( // It accepts connections and spawns a new MCP server session for each connection. func (s *Service) ServeUnix(ctx context.Context, socketPath string) error { // Clean up any stale socket file - if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + if err := io.Local.Delete(socketPath); err != nil { s.logger.Warn("Failed to remove stale socket", "path", socketPath, "err", err) } @@ -23,7 +23,7 @@ func (s *Service) ServeUnix(ctx context.Context, socketPath string) error { } defer func() { _ = listener.Close() - _ = os.Remove(socketPath) + _ = io.Local.Delete(socketPath) }() // Close listener when context is cancelled to unblock Accept