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).
162 lines
3.3 KiB
Go
162 lines
3.3 KiB
Go
// cf-cli is a tiny demo client for clawdforge.
|
|
//
|
|
// cf-cli health
|
|
// cf-cli run "Reply with JSON: {\"hello\":\"world\"}"
|
|
// cf-cli upload /path/to/file.png
|
|
//
|
|
// Configuration via env:
|
|
//
|
|
// CLAWDFORGE_URL base URL, default http://192.168.0.5:8800
|
|
// CLAWDFORGE_TOKEN bearer token (required for run/upload)
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
cf "gitea.sulkta.com/Sulkta-Coop/clawdforge/clients/go"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
baseURL := envOr("CLAWDFORGE_URL", "http://192.168.0.5:8800")
|
|
token := os.Getenv("CLAWDFORGE_TOKEN")
|
|
client := cf.New(baseURL, token)
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
cmd := os.Args[1]
|
|
switch cmd {
|
|
case "health", "healthz":
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
h, err := client.Healthz(ctx)
|
|
fail(err)
|
|
printJSON(h)
|
|
|
|
case "run":
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: cf-cli run <prompt> [model] [timeout_secs]")
|
|
os.Exit(2)
|
|
}
|
|
prompt := os.Args[2]
|
|
model := ""
|
|
timeoutSecs := 60
|
|
if len(os.Args) >= 4 {
|
|
model = os.Args[3]
|
|
}
|
|
if len(os.Args) >= 5 {
|
|
n, err := strconv.Atoi(os.Args[4])
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "timeout_secs must be int")
|
|
os.Exit(2)
|
|
}
|
|
timeoutSecs = n
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSecs+30)*time.Second)
|
|
defer cancel()
|
|
res, err := client.Run(ctx, cf.RunRequest{
|
|
Prompt: prompt,
|
|
Model: model,
|
|
TimeoutSecs: timeoutSecs,
|
|
})
|
|
if err != nil {
|
|
var rf *cf.RunFailure
|
|
if errors.As(err, &rf) {
|
|
fmt.Fprintf(os.Stderr, "run failed (stop_reason=%s): %s\n", rf.StopReason, rf.Err)
|
|
if rf.Stderr != "" {
|
|
fmt.Fprintln(os.Stderr, "--- stderr ---")
|
|
fmt.Fprintln(os.Stderr, rf.Stderr)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
fail(err)
|
|
}
|
|
printJSON(res)
|
|
|
|
case "upload":
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: cf-cli upload <path> [ttl_secs]")
|
|
os.Exit(2)
|
|
}
|
|
path := os.Args[2]
|
|
ttl := 0
|
|
if len(os.Args) >= 4 {
|
|
n, err := strconv.Atoi(os.Args[3])
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "ttl_secs must be int")
|
|
os.Exit(2)
|
|
}
|
|
ttl = n
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
|
defer cancel()
|
|
ft, err := client.UploadFile(ctx, path, ttl)
|
|
fail(err)
|
|
printJSON(ft)
|
|
|
|
case "tokens":
|
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
toks, err := client.ListTokens(ctx)
|
|
fail(err)
|
|
printJSON(toks)
|
|
|
|
default:
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, `cf-cli — clawdforge demo client
|
|
|
|
usage:
|
|
cf-cli health
|
|
cf-cli run <prompt> [model] [timeout_secs]
|
|
cf-cli upload <path> [ttl_secs]
|
|
cf-cli tokens
|
|
|
|
env:
|
|
CLAWDFORGE_URL (default http://192.168.0.5:8800)
|
|
CLAWDFORGE_TOKEN (required)`)
|
|
}
|
|
|
|
func envOr(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func fail(err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
if errors.Is(err, cf.ErrAuth) {
|
|
fmt.Fprintln(os.Stderr, "(auth — check CLAWDFORGE_TOKEN)")
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
func printJSON(v any) {
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "marshal:", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println(string(b))
|
|
}
|