refactor(agentic): use go-forge library instead of raw HTTP

Replace raw http.Client calls with go-forge typed API:
- prep.go: getIssueBody via forge.Issues.Get, pullWikiContent
  via forge.Wiki.ListPages/GetPage
- pr.go: forgeCreatePR via forge.Pulls.Create, commentOnIssue
  via forge.Issues.CreateComment, listRepoPRs via forge.Pulls.ListAll
- scan.go: listOrgRepos via forge.Repos.ListOrgRepos

Eliminates manual JSON marshalling, auth headers, pagination loops,
and anonymous struct declarations. One Forge client, one auth,
type-safe responses.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-22 14:15:41 +00:00
parent bfe70871a2
commit 7f84ac8348
3 changed files with 64 additions and 185 deletions

View file

@ -3,13 +3,12 @@
package agentic
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os/exec"
core "dappco.re/go/core"
"dappco.re/go/core/forge"
forge_types "dappco.re/go/core/forge/types"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -164,53 +163,20 @@ func (s *PrepSubsystem) buildPRBody(st *WorkspaceStatus) string {
}
func (s *PrepSubsystem) forgeCreatePR(ctx context.Context, org, repo, head, base, title, body string) (string, int, error) {
payload, _ := json.Marshal(map[string]any{
"title": title,
"body": body,
"head": head,
"base": base,
pr, err := s.forge.Pulls.Create(ctx, forge.Params{"owner": org, "repo": repo}, &forge_types.CreatePullRequestOption{
Title: title,
Body: body,
Head: head,
Base: base,
})
url := core.Sprintf("%s/api/v1/repos/%s/%s/pulls", 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)
resp, err := s.client.Do(req)
if err != nil {
return "", 0, core.E("forgeCreatePR", "request failed", err)
return "", 0, core.E("forgeCreatePR", "create PR failed", err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
var errBody map[string]any
json.NewDecoder(resp.Body).Decode(&errBody)
msg, _ := errBody["message"].(string)
return "", 0, core.E("forgeCreatePR", core.Sprintf("HTTP %d: %s", resp.StatusCode, msg), nil)
}
var pr struct {
Number int `json:"number"`
HTMLURL string `json:"html_url"`
}
json.NewDecoder(resp.Body).Decode(&pr)
return pr.HTMLURL, pr.Number, nil
return pr.HTMLURL, int(pr.Index), nil
}
func (s *PrepSubsystem) commentOnIssue(ctx context.Context, org, repo string, issue int, comment string) {
payload, _ := json.Marshal(map[string]string{"body": comment})
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d/comments", s.forgeURL, org, repo, issue)
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil {
return
}
resp.Body.Close()
s.forge.Issues.CreateComment(ctx, org, repo, int64(issue), comment)
}
// --- agentic_list_prs ---
@ -309,54 +275,30 @@ func (s *PrepSubsystem) listPRs(ctx context.Context, _ *mcp.CallToolRequest, inp
}
func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string) ([]PRInfo, error) {
url := core.Sprintf("%s/api/v1/repos/%s/%s/pulls?state=%s&limit=10",
s.forgeURL, org, repo, state)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
prs, err := s.forge.Pulls.ListAll(ctx, forge.Params{"owner": org, "repo": repo})
if err != nil {
return nil, core.E("listRepoPRs", "failed to list PRs for "+repo, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, core.E("listRepoPRs", core.Sprintf("HTTP %d listing PRs for %s", resp.StatusCode, repo), nil)
}
var prs []struct {
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
Mergeable bool `json:"mergeable"`
HTMLURL string `json:"html_url"`
Head struct {
Ref string `json:"ref"`
} `json:"head"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
User struct {
Login string `json:"login"`
} `json:"user"`
Labels []struct {
Name string `json:"name"`
} `json:"labels"`
}
json.NewDecoder(resp.Body).Decode(&prs)
var result []PRInfo
for _, pr := range prs {
if state != "" && state != "all" && string(pr.State) != state {
continue
}
var labels []string
for _, l := range pr.Labels {
labels = append(labels, l.Name)
}
author := ""
if pr.User != nil {
author = pr.User.UserName
}
result = append(result, PRInfo{
Repo: repo,
Number: pr.Number,
Number: int(pr.Index),
Title: pr.Title,
State: pr.State,
Author: pr.User.Login,
State: string(pr.State),
Author: author,
Branch: pr.Head.Ref,
Base: pr.Base.Ref,
Labels: labels,

View file

@ -16,6 +16,7 @@ import (
"dappco.re/go/agent/pkg/lib"
core "dappco.re/go/core"
"dappco.re/go/core/forge"
coremcp "forge.lthn.ai/core/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
"gopkg.in/yaml.v3"
@ -34,6 +35,7 @@ type CompletionNotifier interface {
// sub := agentic.NewPrep()
// sub.RegisterTools(server)
type PrepSubsystem struct {
forge *forge.Forge
forgeURL string
forgeToken string
brainURL string
@ -65,8 +67,11 @@ func NewPrep() *PrepSubsystem {
}
}
forgeURL := envOr("FORGE_URL", "https://forge.lthn.ai")
return &PrepSubsystem{
forgeURL: envOr("FORGE_URL", "https://forge.lthn.ai"),
forge: forge.NewForge(forgeURL, forgeToken),
forgeURL: forgeURL,
forgeToken: forgeToken,
brainURL: envOr("CORE_BRAIN_URL", "https://api.lthn.sh"),
brainKey: brainKey,
@ -257,6 +262,22 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
return nil, out, nil
}
// --- Public API for CLI testing ---
// TestPrepWorkspace exposes prepWorkspace for CLI testing.
//
// _, out, err := prep.TestPrepWorkspace(ctx, input)
func (s *PrepSubsystem) TestPrepWorkspace(ctx context.Context, input PrepInput) (*mcp.CallToolResult, PrepOutput, error) {
return s.prepWorkspace(ctx, nil, input)
}
// TestBuildPrompt exposes buildPrompt for CLI testing.
//
// prompt, memories, consumers := prep.TestBuildPrompt(ctx, input, "dev", repoPath)
func (s *PrepSubsystem) TestBuildPrompt(ctx context.Context, input PrepInput, branch, repoPath string) (string, int, int) {
return s.buildPrompt(ctx, input, branch, repoPath)
}
// --- Prompt Building ---
// buildPrompt assembles all context into a single prompt string.
@ -348,30 +369,12 @@ func (s *PrepSubsystem) buildPrompt(ctx context.Context, input PrepInput, branch
// --- Context Helpers (return strings, not write files) ---
func (s *PrepSubsystem) getIssueBody(ctx context.Context, org, repo string, issue int) string {
if s.forgeToken == "" {
idx := core.Sprintf("%d", issue)
iss, err := s.forge.Issues.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": idx})
if err != nil {
return ""
}
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
return ""
}
defer resp.Body.Close()
var issueData struct {
Title string `json:"title"`
Body string `json:"body"`
}
json.NewDecoder(resp.Body).Decode(&issueData)
return core.Sprintf("# %s\n\n%s", issueData.Title, issueData.Body)
return core.Sprintf("# %s\n\n%s", iss.Title, iss.Body)
}
func (s *PrepSubsystem) brainRecall(ctx context.Context, repo string) (string, int) {
@ -473,64 +476,26 @@ func (s *PrepSubsystem) getGitLog(repoPath string) string {
}
func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) string {
if s.forgeToken == "" {
pages, err := s.forge.Wiki.ListPages(ctx, org, repo)
if err != nil || len(pages) == 0 {
return ""
}
url := core.Sprintf("%s/api/v1/repos/%s/%s/wiki/pages", s.forgeURL, org, repo)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
return ""
}
defer resp.Body.Close()
var pages []struct {
Title string `json:"title"`
SubURL string `json:"sub_url"`
}
json.NewDecoder(resp.Body).Decode(&pages)
b := core.NewBuilder()
for _, page := range pages {
subURL := page.SubURL
if subURL == "" {
subURL = page.Title
for _, meta := range pages {
name := meta.SubURL
if name == "" {
name = meta.Title
}
pageURL := core.Sprintf("%s/api/v1/repos/%s/%s/wiki/page/%s", s.forgeURL, org, repo, subURL)
pageReq, _ := http.NewRequestWithContext(ctx, "GET", pageURL, nil)
pageReq.Header.Set("Authorization", "token "+s.forgeToken)
pageResp, pErr := s.client.Do(pageReq)
if pErr != nil || pageResp.StatusCode != 200 {
if pageResp != nil {
pageResp.Body.Close()
}
page, pErr := s.forge.Wiki.GetPage(ctx, org, repo, name)
if pErr != nil || page.ContentBase64 == "" {
continue
}
var pageData struct {
ContentBase64 string `json:"content_base64"`
}
json.NewDecoder(pageResp.Body).Decode(&pageData)
pageResp.Body.Close()
if pageData.ContentBase64 == "" {
continue
}
content, _ := base64.StdEncoding.DecodeString(pageData.ContentBase64)
b.WriteString("### " + page.Title + "\n\n")
content, _ := base64.StdEncoding.DecodeString(page.ContentBase64)
b.WriteString("### " + meta.Title + "\n\n")
b.WriteString(string(content))
b.WriteString("\n\n")
}
return b.String()
}

View file

@ -104,42 +104,14 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input
}
func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, error) {
repos, err := s.forge.Repos.ListOrgRepos(ctx, org)
if err != nil {
return nil, core.E("scan.listOrgRepos", "failed to list repos", err)
}
var allNames []string
page := 1
for {
u := core.Sprintf("%s/api/v1/orgs/%s/repos?limit=50&page=%d", s.forgeURL, org, page)
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return nil, core.E("scan.listOrgRepos", "failed to create request", err)
}
req.Header.Set("Authorization", "token "+s.forgeToken)
resp, err := s.client.Do(req)
if err != nil {
return nil, core.E("scan.listOrgRepos", "failed to list repos", err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
return nil, core.E("scan.listOrgRepos", core.Sprintf("HTTP %d listing repos", resp.StatusCode), nil)
}
var repos []struct {
Name string `json:"name"`
}
json.NewDecoder(resp.Body).Decode(&repos)
resp.Body.Close()
for _, r := range repos {
allNames = append(allNames, r.Name)
}
// If we got fewer than the limit, we've reached the last page
if len(repos) < 50 {
break
}
page++
for _, r := range repos {
allNames = append(allNames, r.Name)
}
return allNames, nil
}