Idiomatic Go client wrapping the FastAPI surface in server.py — Healthz, Run, UploadFile/UploadReader, and admin token CRUD. stdlib net/http only, context-first signatures, typed errors (ErrAuth sentinel, RunFailure for /run 502s, APIError for other 4xx/5xx, TransportError for network/EOF). RunResult.Result is captured as json.RawMessage and materialized via .AsJSON(out) / .AsText() because claude returns either parsed JSON or plain text depending on prompt. UploadFile streams via io.Pipe + multipart without buffering the file in memory. Module: gitea.sulkta.com/Sulkta-Coop/clawdforge/clients/go Includes cmd/cf-cli demo binary and httptest-based test suite (13 tests).
95 lines
3 KiB
Go
95 lines
3 KiB
Go
package clawdforge
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// Healthz is the parsed response from GET /healthz.
|
|
type Healthz struct {
|
|
OK bool `json:"ok"`
|
|
ClaudePresent bool `json:"claude_present"`
|
|
ClaudeVersion string `json:"claude_version"`
|
|
}
|
|
|
|
// RunRequest is the body for POST /run. Matches server.py RunRequest exactly.
|
|
//
|
|
// Only Prompt is required. Model, System, Files, TimeoutSecs are zero-value
|
|
// optional — empty strings/slices and zero ints are omitted from the wire.
|
|
type RunRequest struct {
|
|
Prompt string `json:"prompt"`
|
|
Model string `json:"model,omitempty"`
|
|
System string `json:"system,omitempty"`
|
|
Files []string `json:"files,omitempty"`
|
|
TimeoutSecs int `json:"timeout_secs,omitempty"`
|
|
}
|
|
|
|
// RunResult is the parsed response from POST /run on success (HTTP 200).
|
|
//
|
|
// Result is captured raw because the upstream may return either a parsed
|
|
// JSON value (object/array/etc) or a plain string. Use AsJSON or AsText
|
|
// to materialize it.
|
|
type RunResult struct {
|
|
OK bool `json:"ok"`
|
|
Result json.RawMessage `json:"result"`
|
|
DurationMS int `json:"duration_ms"`
|
|
StopReason string `json:"stop_reason"`
|
|
}
|
|
|
|
// AsJSON unmarshals the Result field into out. Use this when your prompt
|
|
// asked claude for JSON and you have a target type.
|
|
//
|
|
// var data map[string]string
|
|
// if err := res.AsJSON(&data); err != nil { ... }
|
|
func (r *RunResult) AsJSON(out any) error {
|
|
if len(r.Result) == 0 {
|
|
return fmt.Errorf("clawdforge: empty result")
|
|
}
|
|
if err := json.Unmarshal(r.Result, out); err != nil {
|
|
return fmt.Errorf("clawdforge: result is not JSON: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AsText returns the Result as a string. Works for both JSON-string results
|
|
// (e.g. claude returned "hello") and structured JSON (returns the raw JSON
|
|
// representation as a string in that case).
|
|
func (r *RunResult) AsText() (string, error) {
|
|
if len(r.Result) == 0 {
|
|
return "", fmt.Errorf("clawdforge: empty result")
|
|
}
|
|
// First try to decode as a plain JSON string
|
|
var s string
|
|
if err := json.Unmarshal(r.Result, &s); err == nil {
|
|
return s, nil
|
|
}
|
|
// Otherwise return the raw JSON encoding
|
|
return string(r.Result), nil
|
|
}
|
|
|
|
// FileToken is the parsed response from POST /files.
|
|
type FileToken struct {
|
|
FileToken string `json:"file_token"`
|
|
TTLSecs int `json:"ttl_secs"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
// AppToken is one entry from GET /admin/tokens, also returned (with the
|
|
// plaintext Token populated) by POST /admin/tokens.
|
|
type AppToken struct {
|
|
Name string `json:"name"`
|
|
Token string `json:"token,omitempty"` // only set on create
|
|
IPCidrs []string `json:"ip_cidrs,omitempty"`
|
|
CreatedAt int64 `json:"created_at,omitempty"`
|
|
}
|
|
|
|
// TokenList is the response from GET /admin/tokens.
|
|
type TokenList struct {
|
|
Tokens []AppToken `json:"tokens"`
|
|
}
|
|
|
|
// CreateTokenRequest is the body for POST /admin/tokens.
|
|
type CreateTokenRequest struct {
|
|
Name string `json:"name"`
|
|
IPCidrs []string `json:"ip_cidrs,omitempty"`
|
|
}
|