* feat(go): make go fmt git-aware by default - By default, only check changed Go files (modified, staged, untracked) - Add --all flag to check all files (previous behaviour) - Reduces noise when running fmt on large codebases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(build): minimal output by default, add missing i18n - Default output now shows single line: "Success Built N artifacts (dir)" - Add --verbose/-v flag to show full detailed output - Add all missing i18n translations for build commands - Errors still show failure reason in minimal mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add root-level `core git` command - Create pkg/gitcmd with git workflow commands as root menu - Export command builders from pkg/dev (AddCommitCommand, etc.) - Commands available under both `core git` and `core dev` for compatibility - Git commands: health, commit, push, pull, work, sync, apply - GitHub orchestration stays in dev: issues, reviews, ci, impact Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(qa): add docblock coverage checking Implement docblock/docstring coverage analysis for Go code: - New `core qa docblock` command to check coverage - Shows compact file:line list when under threshold - Integrate with `core go qa` as a default check - Add --docblock-threshold flag (default 80%) The checker uses Go AST parsing to find exported symbols (functions, types, consts, vars) without documentation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - Fix doc comment: "status" → "health" in gitcmd package - Implement --check flag for `core go fmt` (exits non-zero if files need formatting) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add docstrings for 100% coverage Add documentation comments to all exported symbols: - pkg/build: ProjectType constants - pkg/cli: LogLevel, RenderStyle, TableStyle - pkg/framework: ServiceFor, MustServiceFor, Core.Core - pkg/git: GitError.Error, GitError.Unwrap - pkg/i18n: Handler Match/Handle methods - pkg/log: Level constants - pkg/mcp: Tool input/output types - pkg/php: Service constants, QA types, service methods - pkg/process: ServiceError.Error - pkg/repos: RepoType constants - pkg/setup: ChangeType, ChangeCategory constants - pkg/workspace: AddWorkspaceCommands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: standardize line endings to LF Add .gitattributes to enforce LF line endings for all text files. Normalize all existing files to use Unix-style line endings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - cmd_format.go: validate --check/--fix mutual exclusivity, capture stderr - cmd_docblock.go: return error instead of os.Exit(1) for proper error handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review feedback (round 2) - linuxkit.go: propagate state update errors, handle cmd.Wait() errors in waitForExit - mcp.go: guard against empty old_string in editDiff to prevent runaway edits - cmd_docblock.go: log parse errors instead of silently skipping Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
322 lines
8 KiB
Go
322 lines
8 KiB
Go
package agentic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/host-uk/core/pkg/errors"
|
|
)
|
|
|
|
// Client is the API client for the core-agentic service.
|
|
type Client struct {
|
|
// BaseURL is the base URL of the API server.
|
|
BaseURL string
|
|
// Token is the authentication token.
|
|
Token string
|
|
// HTTPClient is the HTTP client used for requests.
|
|
HTTPClient *http.Client
|
|
// AgentID is the identifier for this agent when claiming tasks.
|
|
AgentID string
|
|
}
|
|
|
|
// NewClient creates a new agentic API client with the given base URL and token.
|
|
func NewClient(baseURL, token string) *Client {
|
|
return &Client{
|
|
BaseURL: strings.TrimSuffix(baseURL, "/"),
|
|
Token: token,
|
|
HTTPClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewClientFromConfig creates a new client from a Config struct.
|
|
func NewClientFromConfig(cfg *Config) *Client {
|
|
client := NewClient(cfg.BaseURL, cfg.Token)
|
|
client.AgentID = cfg.AgentID
|
|
return client
|
|
}
|
|
|
|
// ListTasks retrieves a list of tasks matching the given options.
|
|
func (c *Client) ListTasks(ctx context.Context, opts ListOptions) ([]Task, error) {
|
|
const op = "agentic.Client.ListTasks"
|
|
|
|
// Build query parameters
|
|
params := url.Values{}
|
|
if opts.Status != "" {
|
|
params.Set("status", string(opts.Status))
|
|
}
|
|
if opts.Priority != "" {
|
|
params.Set("priority", string(opts.Priority))
|
|
}
|
|
if opts.Project != "" {
|
|
params.Set("project", opts.Project)
|
|
}
|
|
if opts.ClaimedBy != "" {
|
|
params.Set("claimed_by", opts.ClaimedBy)
|
|
}
|
|
if opts.Limit > 0 {
|
|
params.Set("limit", strconv.Itoa(opts.Limit))
|
|
}
|
|
if len(opts.Labels) > 0 {
|
|
params.Set("labels", strings.Join(opts.Labels, ","))
|
|
}
|
|
|
|
endpoint := c.BaseURL + "/api/tasks"
|
|
if len(params) > 0 {
|
|
endpoint += "?" + params.Encode()
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, errors.E(op, "API error", err)
|
|
}
|
|
|
|
var tasks []Task
|
|
if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
|
|
return nil, errors.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// GetTask retrieves a single task by its ID.
|
|
func (c *Client) GetTask(ctx context.Context, id string) (*Task, error) {
|
|
const op = "agentic.Client.GetTask"
|
|
|
|
if id == "" {
|
|
return nil, errors.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s", c.BaseURL, url.PathEscape(id))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, errors.E(op, "API error", err)
|
|
}
|
|
|
|
var task Task
|
|
if err := json.NewDecoder(resp.Body).Decode(&task); err != nil {
|
|
return nil, errors.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return &task, nil
|
|
}
|
|
|
|
// ClaimTask claims a task for the current agent.
|
|
func (c *Client) ClaimTask(ctx context.Context, id string) (*Task, error) {
|
|
const op = "agentic.Client.ClaimTask"
|
|
|
|
if id == "" {
|
|
return nil, errors.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s/claim", c.BaseURL, url.PathEscape(id))
|
|
|
|
// Include agent ID in the claim request if available
|
|
var body io.Reader
|
|
if c.AgentID != "" {
|
|
data, _ := json.Marshal(map[string]string{"agent_id": c.AgentID})
|
|
body = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
|
if err != nil {
|
|
return nil, errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return nil, errors.E(op, "API error", err)
|
|
}
|
|
|
|
// Read body once to allow multiple decode attempts
|
|
bodyData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, errors.E(op, "failed to read response", err)
|
|
}
|
|
|
|
// Try decoding as ClaimResponse first
|
|
var result ClaimResponse
|
|
if err := json.Unmarshal(bodyData, &result); err == nil && result.Task != nil {
|
|
return result.Task, nil
|
|
}
|
|
|
|
// Try decoding as just a Task for simpler API responses
|
|
var task Task
|
|
if err := json.Unmarshal(bodyData, &task); err != nil {
|
|
return nil, errors.E(op, "failed to decode response", err)
|
|
}
|
|
|
|
return &task, nil
|
|
}
|
|
|
|
// UpdateTask updates a task with new status, progress, or notes.
|
|
func (c *Client) UpdateTask(ctx context.Context, id string, update TaskUpdate) error {
|
|
const op = "agentic.Client.UpdateTask"
|
|
|
|
if id == "" {
|
|
return errors.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s", c.BaseURL, url.PathEscape(id))
|
|
|
|
data, err := json.Marshal(update)
|
|
if err != nil {
|
|
return errors.E(op, "failed to marshal update", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return errors.E(op, "API error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CompleteTask marks a task as completed with the given result.
|
|
func (c *Client) CompleteTask(ctx context.Context, id string, result TaskResult) error {
|
|
const op = "agentic.Client.CompleteTask"
|
|
|
|
if id == "" {
|
|
return errors.E(op, "task ID is required", nil)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s/api/tasks/%s/complete", c.BaseURL, url.PathEscape(id))
|
|
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
return errors.E(op, "failed to marshal result", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if err := c.checkResponse(resp); err != nil {
|
|
return errors.E(op, "API error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setHeaders adds common headers to the request.
|
|
func (c *Client) setHeaders(req *http.Request) {
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", "core-agentic-client/1.0")
|
|
}
|
|
|
|
// checkResponse checks if the response indicates an error.
|
|
func (c *Client) checkResponse(resp *http.Response) error {
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return nil
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
// Try to parse as APIError
|
|
var apiErr APIError
|
|
if err := json.Unmarshal(body, &apiErr); err == nil && apiErr.Message != "" {
|
|
apiErr.Code = resp.StatusCode
|
|
return &apiErr
|
|
}
|
|
|
|
// Return generic error
|
|
return &APIError{
|
|
Code: resp.StatusCode,
|
|
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)),
|
|
Details: string(body),
|
|
}
|
|
}
|
|
|
|
// Ping tests the connection to the API server.
|
|
func (c *Client) Ping(ctx context.Context) error {
|
|
const op = "agentic.Client.Ping"
|
|
|
|
endpoint := c.BaseURL + "/api/health"
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return errors.E(op, "failed to create request", err)
|
|
}
|
|
|
|
c.setHeaders(req)
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return errors.E(op, "request failed", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return errors.E(op, fmt.Sprintf("server returned status %d", resp.StatusCode), nil)
|
|
}
|
|
|
|
return nil
|
|
}
|