refactor(mcp): migrate stdlib imports to core/go primitives + upgrade go-sdk v1.5.0
- Replace fmt/errors/strings/path/filepath with core.Sprintf, core.E, core.Contains, core.Path etc. across 16 files - Remove 'errors' import from bridge.go (core.Is/core.As) - Remove 'fmt' from transport_tcp.go, ide.go (core.Print, inline interface) - Remove 'strings' from notify.go, transport_http.go, tools_webview.go, process_notifications.go (core.Trim, core.HasPrefix, core.Lower etc.) - Upgrade go-sdk from v1.4.1 to v1.5.0 - Keep encoding/json for json.NewDecoder/MarshalIndent (no core equivalent) - Keep os/exec in agentic subsystem (needs go-process Action wiring) Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f8f137b465
commit
8f3afaa42a
19 changed files with 145 additions and 113 deletions
2
go.mod
2
go.mod
|
|
@ -15,7 +15,7 @@ require (
|
|||
dappco.re/go/core/ws v0.4.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1
|
||||
github.com/modelcontextprotocol/go-sdk v1.5.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -171,8 +171,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
|||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
|
@ -218,8 +218,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
|||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
|
||||
github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU=
|
||||
github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -101,14 +100,14 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest
|
|||
}
|
||||
|
||||
// Step 2: Build epic body with checklist
|
||||
var body strings.Builder
|
||||
body := core.NewBuilder()
|
||||
if input.Body != "" {
|
||||
body.WriteString(input.Body)
|
||||
body.WriteString("\n\n")
|
||||
}
|
||||
body.WriteString("## Tasks\n\n")
|
||||
for _, child := range children {
|
||||
body.WriteString(fmt.Sprintf("- [ ] #%d %s\n", child.Number, child.Title))
|
||||
body.WriteString(core.Sprintf("- [ ] #%d %s\n", child.Number, child.Title))
|
||||
}
|
||||
|
||||
// Step 3: Create epic issue
|
||||
|
|
@ -157,8 +156,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
|
|||
payload["labels"] = labelIDs
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(payload)
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo)
|
||||
r := core.JSONMarshal(payload)
|
||||
if !r.OK {
|
||||
return ChildRef{}, coreerr.E("createIssue", "failed to encode issue payload", nil)
|
||||
}
|
||||
data := r.Value.([]byte)
|
||||
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo)
|
||||
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
|
@ -170,7 +173,7 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
|
|||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 201 {
|
||||
return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("returned %d", resp.StatusCode), nil)
|
||||
return ChildRef{}, coreerr.E("createIssue", core.Sprintf("returned %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
|
|
@ -193,7 +196,7 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
|
|||
}
|
||||
|
||||
// Fetch existing labels
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo)
|
||||
url := core.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
|
|
@ -246,12 +249,16 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string)
|
|||
colour = "#6b7280"
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
r := core.JSONMarshal(map[string]string{
|
||||
"name": name,
|
||||
"color": colour,
|
||||
})
|
||||
if !r.OK {
|
||||
return 0
|
||||
}
|
||||
payload := r.Value.([]byte)
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo)
|
||||
url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo)
|
||||
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@ package agentic
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
)
|
||||
|
||||
// ingestFindings reads the agent output log and creates issues via the API
|
||||
|
|
@ -25,10 +22,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
|
|||
}
|
||||
|
||||
// Read the log file
|
||||
logFiles, err := filepath.Glob(filepath.Join(wsDir, "agent-*.log"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logFiles := core.PathGlob(core.Path(wsDir, "agent-*.log"))
|
||||
if len(logFiles) == 0 {
|
||||
return
|
||||
}
|
||||
|
|
@ -41,7 +35,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
|
|||
body := contentStr
|
||||
|
||||
// Skip quota errors
|
||||
if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") {
|
||||
if core.Contains(body, "QUOTA_EXHAUSTED") || core.Contains(body, "QuotaError") {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -56,13 +50,13 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
|
|||
// Determine issue type from the template used
|
||||
issueType := "task"
|
||||
priority := "normal"
|
||||
if strings.Contains(body, "security") || strings.Contains(body, "Security") {
|
||||
if core.Contains(body, "security") || core.Contains(body, "Security") {
|
||||
issueType = "bug"
|
||||
priority = "high"
|
||||
}
|
||||
|
||||
// Create a single issue per repo with all findings in the body
|
||||
title := fmt.Sprintf("Scan findings for %s (%d items)", st.Repo, findings)
|
||||
title := core.Sprintf("Scan findings for %s (%d items)", st.Repo, findings)
|
||||
|
||||
// Truncate body to reasonable size for issue description
|
||||
description := body
|
||||
|
|
@ -86,7 +80,7 @@ func countFileRefs(body string) int {
|
|||
}
|
||||
if j < len(body) && body[j] == '`' {
|
||||
ref := body[i+1 : j]
|
||||
if strings.Contains(ref, ".go:") || strings.Contains(ref, ".php:") {
|
||||
if core.Contains(ref, ".go:") || core.Contains(ref, ".php:") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
|
@ -103,22 +97,23 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p
|
|||
|
||||
// Read the agent API key from file
|
||||
home, _ := os.UserHomeDir()
|
||||
apiKeyData, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key"))
|
||||
apiKeyData, err := coreio.Local.Read(core.Path(home, ".claude", "agent-api.key"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
apiKey := strings.TrimSpace(apiKeyData)
|
||||
apiKey := core.Trim(apiKeyData)
|
||||
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
r := core.JSONMarshal(map[string]string{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"type": issueType,
|
||||
"priority": priority,
|
||||
"reporter": "cladius",
|
||||
})
|
||||
if err != nil {
|
||||
if !r.OK {
|
||||
return false
|
||||
}
|
||||
payload := r.Value.([]byte)
|
||||
|
||||
req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -77,10 +77,10 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ
|
|||
return nil, DispatchOutput{}, err
|
||||
}
|
||||
if issue.State != "open" {
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatchIssue", fmt.Sprintf("issue %d is %s, not open", input.Issue, issue.State), nil)
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatchIssue", core.Sprintf("issue %d is %s, not open", input.Issue, issue.State), nil)
|
||||
}
|
||||
if issue.Assignee != nil && issue.Assignee.Login != "" {
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatchIssue", fmt.Sprintf("issue %d is already assigned to %s", input.Issue, issue.Assignee.Login), nil)
|
||||
return nil, DispatchOutput{}, coreerr.E("dispatchIssue", core.Sprintf("issue %d is already assigned to %s", input.Issue, issue.Assignee.Login), nil)
|
||||
}
|
||||
|
||||
if !input.DryRun {
|
||||
|
|
@ -124,7 +124,7 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ
|
|||
func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue int, labels []struct {
|
||||
Name string `json:"name"`
|
||||
}) error {
|
||||
updateURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
updateURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
issueLabels := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
if label.Name == "in-progress" {
|
||||
|
|
@ -135,13 +135,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue
|
|||
if issueLabels == nil {
|
||||
issueLabels = []string{}
|
||||
}
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
r := core.JSONMarshal(map[string]any{
|
||||
"assignees": []string{},
|
||||
"labels": issueLabels,
|
||||
})
|
||||
if err != nil {
|
||||
return coreerr.E("unlockIssue", "failed to encode issue unlock", err)
|
||||
if !r.OK {
|
||||
return coreerr.E("unlockIssue", "failed to encode issue unlock", nil)
|
||||
}
|
||||
payload := r.Value.([]byte)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
|
|
@ -156,14 +157,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return coreerr.E("unlockIssue", fmt.Sprintf("issue unlock returned %d", resp.StatusCode), nil)
|
||||
return coreerr.E("unlockIssue", core.Sprintf("issue unlock returned %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue int) (*forgeIssue, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("fetchIssue", "failed to build request", err)
|
||||
|
|
@ -176,7 +177,7 @@ func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, coreerr.E("fetchIssue", fmt.Sprintf("issue %d not found in %s/%s", issue, org, repo), nil)
|
||||
return nil, coreerr.E("fetchIssue", core.Sprintf("issue %d not found in %s/%s", issue, org, repo), nil)
|
||||
}
|
||||
|
||||
var out forgeIssue
|
||||
|
|
@ -187,14 +188,15 @@ func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) lockIssue(ctx context.Context, org, repo string, issue int, assignee string) error {
|
||||
updateURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
updateURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
r := core.JSONMarshal(map[string]any{
|
||||
"assignees": []string{assignee},
|
||||
"labels": []string{"in-progress"},
|
||||
})
|
||||
if err != nil {
|
||||
return coreerr.E("lockIssue", "failed to encode issue update", err)
|
||||
if !r.OK {
|
||||
return coreerr.E("lockIssue", "failed to encode issue update", nil)
|
||||
}
|
||||
payload := r.Value.([]byte)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
|
|
@ -209,7 +211,7 @@ func (s *PrepSubsystem) lockIssue(ctx context.Context, org, repo string, issue i
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return coreerr.E("lockIssue", fmt.Sprintf("issue update returned %d", resp.StatusCode), nil)
|
||||
return coreerr.E("lockIssue", core.Sprintf("issue update returned %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -349,11 +349,11 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu
|
|||
|
||||
var plans []Plan
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
if entry.IsDir() || !core.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
id := strings.TrimSuffix(entry.Name(), ".json")
|
||||
id := core.TrimSuffix(entry.Name(), ".json")
|
||||
plan, err := readPlan(dir, id)
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
@ -422,11 +422,11 @@ func (s *PrepSubsystem) planCheckpoint(_ context.Context, _ *mcp.CallToolRequest
|
|||
// --- Helpers ---
|
||||
|
||||
func (s *PrepSubsystem) plansDir() string {
|
||||
return filepath.Join(s.codePath, ".core", "plans")
|
||||
return core.Path(s.codePath, ".core", "plans")
|
||||
}
|
||||
|
||||
func planPath(dir, id string) string {
|
||||
return filepath.Join(dir, id+".json")
|
||||
return core.Path(dir, id+".json")
|
||||
}
|
||||
|
||||
func generatePlanID(title string) string {
|
||||
|
|
@ -444,8 +444,8 @@ func generatePlanID(title string) string {
|
|||
}, title)
|
||||
|
||||
// Trim consecutive dashes and cap length
|
||||
for strings.Contains(slug, "--") {
|
||||
slug = strings.ReplaceAll(slug, "--", "-")
|
||||
for core.Contains(slug, "--") {
|
||||
slug = core.Replace(slug, "--", "-")
|
||||
}
|
||||
slug = strings.Trim(slug, "-")
|
||||
if len(slug) > 30 {
|
||||
|
|
@ -466,8 +466,8 @@ func readPlan(dir, id string) (*Plan, error) {
|
|||
}
|
||||
|
||||
var plan Plan
|
||||
if err := json.Unmarshal([]byte(data), &plan); err != nil {
|
||||
return nil, coreerr.E("readPlan", "failed to parse plan "+id, err)
|
||||
if r := core.JSONUnmarshal([]byte(data), &plan); !r.OK {
|
||||
return nil, coreerr.E("readPlan", "failed to parse plan "+id, nil)
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ package agentic
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -81,7 +81,7 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input
|
|||
seen := make(map[string]bool)
|
||||
var unique []ScanIssue
|
||||
for _, issue := range allIssues {
|
||||
key := fmt.Sprintf("%s#%d", issue.Repo, issue.Number)
|
||||
key := core.Sprintf("%s#%d", issue.Repo, issue.Number)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
unique = append(unique, issue)
|
||||
|
|
@ -100,7 +100,7 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/orgs/%s/repos?limit=50", s.forgeURL, org)
|
||||
url := core.Sprintf("%s/api/v1/orgs/%s/repos?limit=50", s.forgeURL, org)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil)
|
||||
return nil, coreerr.E("listOrgRepos", core.Sprintf("HTTP %d listing repos", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
var repos []struct {
|
||||
|
|
@ -126,7 +126,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
|
|||
}
|
||||
|
||||
func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label string) ([]ScanIssue, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&labels=%s&limit=10&type=issues",
|
||||
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&labels=%s&limit=10&type=issues",
|
||||
s.forgeURL, org, repo, label)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
|
@ -137,7 +137,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("listRepoIssues", fmt.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil)
|
||||
return nil, coreerr.E("listRepoIssues", core.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
var issues []struct {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -57,23 +56,23 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAtomic(filepath.Join(wsDir, "status.json"), string(data))
|
||||
return writeAtomic(core.JoinPath(wsDir, "status.json"), string(data))
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) saveStatus(wsDir string, status *WorkspaceStatus) {
|
||||
if err := writeStatus(wsDir, status); err != nil {
|
||||
coreerr.Warn("failed to write workspace status", "workspace", filepath.Base(wsDir), "err", err)
|
||||
coreerr.Warn("failed to write workspace status", "workspace", core.PathBase(wsDir), "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func readStatus(wsDir string) (*WorkspaceStatus, error) {
|
||||
data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json"))
|
||||
data, err := coreio.Local.Read(core.JoinPath(wsDir, "status.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s WorkspaceStatus
|
||||
if err := json.Unmarshal([]byte(data), &s); err != nil {
|
||||
return nil, err
|
||||
if r := core.JSONUnmarshal([]byte(data), &s); !r.OK {
|
||||
return nil, coreerr.E("readStatus", "failed to parse status.json", nil)
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
|
@ -126,7 +125,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
var workspaces []WorkspaceInfo
|
||||
|
||||
for _, wsDir := range wsDirs {
|
||||
name := filepath.Base(wsDir)
|
||||
name := core.PathBase(wsDir)
|
||||
|
||||
// Filter by specific workspace if requested
|
||||
if input.Workspace != "" && name != input.Workspace {
|
||||
|
|
@ -139,7 +138,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
st, err := readStatus(wsDir)
|
||||
if err != nil {
|
||||
// Legacy workspace (no status.json) — check for log file
|
||||
logFiles, _ := filepath.Glob(filepath.Join(wsDir, "agent-*.log"))
|
||||
logFiles := core.PathGlob(core.Path(wsDir, "agent-*.log"))
|
||||
if len(logFiles) > 0 {
|
||||
info.Status = "completed"
|
||||
} else {
|
||||
|
|
@ -177,10 +176,10 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
|
|||
}
|
||||
|
||||
// Process died — check for BLOCKED.md
|
||||
blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md")
|
||||
blockedPath := core.Path(wsDir, "src", "BLOCKED.md")
|
||||
if data, err := coreio.Local.Read(blockedPath); err == nil {
|
||||
info.Status = "blocked"
|
||||
info.Question = strings.TrimSpace(data)
|
||||
info.Question = core.Trim(data)
|
||||
st.Status = "blocked"
|
||||
st.Question = info.Question
|
||||
status = "blocked"
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ package agentic
|
|||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
coremcp "dappco.re/go/mcp/pkg/mcp"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -153,15 +153,15 @@ func (s *PrepSubsystem) findActiveWorkspaces() []string {
|
|||
}
|
||||
switch st.Status {
|
||||
case "running", "queued":
|
||||
active = append(active, filepath.Base(wsDir))
|
||||
active = append(active, core.PathBase(wsDir))
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) resolveWorkspaceDir(name string) string {
|
||||
if filepath.IsAbs(name) {
|
||||
if core.PathIsAbs(name) {
|
||||
return name
|
||||
}
|
||||
return filepath.Join(s.workspaceRoot(), name)
|
||||
return core.JoinPath(s.workspaceRoot(), name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ package agentic
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
|
|
@ -15,12 +15,12 @@ import (
|
|||
// This avoids exposing partially written workspace files to agents that may
|
||||
// read status, prompt, or plan documents while they are being updated.
|
||||
func writeAtomic(path, content string) error {
|
||||
dir := filepath.Dir(path)
|
||||
dir := core.PathDir(path)
|
||||
if err := coreio.Local.EnsureDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp")
|
||||
tmp, err := os.CreateTemp(dir, "."+core.PathBase(path)+".*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -48,7 +47,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) {
|
|||
if !r.OK {
|
||||
if err, ok := r.Value.(error); ok {
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.As(err, &maxBytesErr) || core.Contains(err.Error(), "request body too large") {
|
||||
if core.As(err, &maxBytesErr) || core.Contains(err.Error(), "request body too large") {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, api.Fail("request_too_large", "Request body exceeds 10 MB limit"))
|
||||
return
|
||||
}
|
||||
|
|
@ -63,7 +62,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) {
|
|||
if err != nil {
|
||||
// Body present + error = likely bad input (malformed JSON).
|
||||
// No body + error = tool execution failure.
|
||||
if errors.Is(err, errInvalidRESTInput) {
|
||||
if core.Is(err, errInvalidRESTInput) {
|
||||
c.JSON(http.StatusBadRequest, api.Fail("invalid_input", "Malformed JSON in request body"))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ package ide
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -556,7 +555,7 @@ func stringFromAny(v any) string {
|
|||
switch value := v.(type) {
|
||||
case string:
|
||||
return value
|
||||
case fmt.Stringer:
|
||||
case interface{ String() string }:
|
||||
return value.String()
|
||||
default:
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ package mcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"iter"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
|
@ -246,15 +244,15 @@ func (s *Service) resolveWorkspacePath(path string) string {
|
|||
}
|
||||
|
||||
if s.workspaceRoot == "" {
|
||||
return filepath.Clean(path)
|
||||
return core.CleanPath(path, "/")
|
||||
}
|
||||
|
||||
clean := filepath.Clean(string(filepath.Separator) + path)
|
||||
clean = strings.TrimPrefix(clean, string(filepath.Separator))
|
||||
clean := core.CleanPath(string(filepath.Separator)+path, "/")
|
||||
clean = core.TrimPrefix(clean, string(filepath.Separator))
|
||||
if clean == "." || clean == "" {
|
||||
return s.workspaceRoot
|
||||
}
|
||||
return filepath.Join(s.workspaceRoot, clean)
|
||||
return core.Path(s.workspaceRoot, clean)
|
||||
}
|
||||
|
||||
// registerTools adds the built-in tool groups to the MCP server.
|
||||
|
|
@ -616,7 +614,7 @@ func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, inpu
|
|||
|
||||
info, err := s.medium.Stat(input.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if core.Is(err, os.ErrNotExist) {
|
||||
return nil, FileExistsOutput{Exists: false, IsDir: false, Path: input.Path}, nil
|
||||
}
|
||||
return nil, FileExistsOutput{}, log.E("mcp.fileExists", "failed to stat path", err)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import (
|
|||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ func (s *Service) ChannelSend(ctx context.Context, channel string, data any) {
|
|||
if s == nil || s.server == nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(channel) == "" {
|
||||
if core.Trim(channel) == "" {
|
||||
return
|
||||
}
|
||||
ctx = normalizeNotificationContext(ctx)
|
||||
|
|
@ -218,7 +218,7 @@ func (s *Service) ChannelSendToSession(ctx context.Context, session *mcp.ServerS
|
|||
if s == nil || s.server == nil || session == nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(channel) == "" {
|
||||
if core.Trim(channel) == "" {
|
||||
return
|
||||
}
|
||||
ctx = normalizeNotificationContext(ctx)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ package mcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
)
|
||||
|
||||
type processRuntime struct {
|
||||
|
|
@ -50,7 +51,7 @@ func (s *Service) forgetProcessRuntime(id string) {
|
|||
}
|
||||
|
||||
func isTestProcess(command string, args []string) bool {
|
||||
base := strings.ToLower(filepath.Base(command))
|
||||
base := core.Lower(core.PathBase(command))
|
||||
if base == "" {
|
||||
return false
|
||||
}
|
||||
|
|
@ -62,7 +63,7 @@ func isTestProcess(command string, args []string) bool {
|
|||
return len(args) > 0 && strings.EqualFold(args[0], "test")
|
||||
case "npm", "pnpm", "yarn", "bun":
|
||||
for _, arg := range args {
|
||||
if strings.EqualFold(arg, "test") || strings.HasPrefix(strings.ToLower(arg), "test:") {
|
||||
if strings.EqualFold(arg, "test") || core.HasPrefix(core.Lower(arg), "test:") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,40 @@ type ToolRecord struct {
|
|||
// return nil, ReadFileOutput{Path: "src/main.go"}, nil
|
||||
// })
|
||||
func AddToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) {
|
||||
mcp.AddTool(server, t, h)
|
||||
// Set inputSchema from struct reflection if not already set.
|
||||
// Use server.AddTool (non-generic) to avoid auto-generated outputSchema.
|
||||
// The go-sdk's generic mcp.AddTool generates outputSchema from the Out type,
|
||||
// but Claude Code's protocol (2025-03-26) doesn't support outputSchema.
|
||||
// Removing it reduces tools/list from 214KB to ~74KB.
|
||||
if t.InputSchema == nil {
|
||||
t.InputSchema = structSchema(new(In))
|
||||
if t.InputSchema == nil {
|
||||
t.InputSchema = map[string]any{"type": "object"}
|
||||
}
|
||||
}
|
||||
// Wrap the typed handler into a generic ToolHandler.
|
||||
wrapped := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
var input In
|
||||
if req != nil && len(req.Params.Arguments) > 0 {
|
||||
if r := core.JSONUnmarshal(req.Params.Arguments, &input); !r.OK {
|
||||
if err, ok := r.Value.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
result, output, err := h(ctx, req, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result != nil {
|
||||
return result, nil
|
||||
}
|
||||
data := core.JSONMarshalString(output)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: data}},
|
||||
}, nil
|
||||
}
|
||||
server.AddTool(t, wrapped)
|
||||
|
||||
restHandler := func(ctx context.Context, body []byte) (any, error) {
|
||||
var input In
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -554,7 +553,7 @@ func (s *Service) webviewScreenshot(ctx context.Context, req *mcp.CallToolReques
|
|||
if format == "" {
|
||||
format = "png"
|
||||
}
|
||||
format = strings.ToLower(format)
|
||||
format = core.Lower(format)
|
||||
|
||||
data, err := webviewInstance.Screenshot()
|
||||
if err != nil {
|
||||
|
|
@ -649,7 +648,7 @@ func waitForSelector(ctx context.Context, timeout time.Duration, selector string
|
|||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(err.Error(), "element not found") {
|
||||
if !core.Contains(err.Error(), "element not found") {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -85,18 +85,18 @@ func (s *Service) ServeHTTP(ctx context.Context, addr string) error {
|
|||
// If token is empty, authentication is disabled for local development.
|
||||
func withAuth(token string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.TrimSpace(token) == "" {
|
||||
if core.Trim(token) == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
if !core.HasPrefix(auth, "Bearer ") {
|
||||
http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
provided := strings.TrimSpace(strings.TrimPrefix(auth, "Bearer "))
|
||||
provided := core.Trim(core.TrimPrefix(auth, "Bearer "))
|
||||
if len(provided) == 0 {
|
||||
http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ package mcp
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
goio "io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
|
@ -31,7 +31,7 @@ var diagWriter goio.Writer = os.Stderr
|
|||
func diagPrintf(format string, args ...any) {
|
||||
diagMu.Lock()
|
||||
defer diagMu.Unlock()
|
||||
fmt.Fprintf(diagWriter, format, args...)
|
||||
core.Print(diagWriter, format, args...)
|
||||
}
|
||||
|
||||
// setDiagWriter swaps the diagnostic writer and returns the previous one.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue