clawdforge/clients/go/cmd/cf-cli/main.go
Kayos 3c62613c30 clients/go: initial Go SDK for clawdforge
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).
2026-04-28 22:36:56 -07:00

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))
}