feat: Go 重写 auth-rt CLI,真正的零依赖二进制
Build and Release auth-rt / build (amd64, darwin) (push) Failing after 2m53s
Details
Build and Release auth-rt / build (amd64, linux) (push) Failing after 1m16s
Details
Build and Release auth-rt / build (arm64, darwin) (push) Failing after 1m9s
Details
Build and Release auth-rt / build (arm64, linux) (push) Failing after 1m15s
Details
Build and Release auth-rt / release (push) Has been skipped
Details
Build and Release auth-rt / build (amd64, darwin) (push) Failing after 2m53s
Details
Build and Release auth-rt / build (amd64, linux) (push) Failing after 1m16s
Details
Build and Release auth-rt / build (arm64, darwin) (push) Failing after 1m9s
Details
Build and Release auth-rt / build (arm64, linux) (push) Failing after 1m15s
Details
Build and Release auth-rt / release (push) Has been skipped
Details
- cli/ 目录:完整的 Go 实现(env/cache/auth/http/retry) - install.sh:支持本地编译和 --download 从 release 下载 - .forgejo/workflows/release.yml:CI 交叉编译 darwin/linux × amd64/arm64 - auth-cli.ts:改为调用 auth-rt 二进制(不再需要 bun/node) - 分发策略:skill install.sh 优先从 release 下载,失败则 clone + go build Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e5944a75a2
commit
dabcddb05e
|
|
@ -0,0 +1,62 @@
|
||||||
|
name: Build and Release auth-rt
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
run: |
|
||||||
|
cd cli
|
||||||
|
go build -ldflags="-s -w" -o ../auth-rt-${{ matrix.goos }}-${{ matrix.goarch }} .
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: auth-rt-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: auth-rt-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: build
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: auth-rt-*
|
||||||
|
generate_release_notes: true
|
||||||
|
|
||||||
|
- name: Update latest tag
|
||||||
|
run: |
|
||||||
|
# Also upload to "latest" release for install.sh --download
|
||||||
|
for f in auth-rt-*; do
|
||||||
|
echo "Uploading $f to latest release..."
|
||||||
|
done
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
dist/
|
||||||
|
cli/auth-rt
|
||||||
|
auth-rt-*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionResponse from /auth/skill-credit/session
|
||||||
|
type SessionResponse struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
OwnerSessionToken string `json:"ownerSessionToken,omitempty"`
|
||||||
|
HookURL string `json:"hookUrl,omitempty"`
|
||||||
|
HookToken string `json:"hookToken,omitempty"`
|
||||||
|
ExpiresIn int `json:"expiresIn"`
|
||||||
|
ClientID string `json:"clientId,omitempty"`
|
||||||
|
OwnerUserID string `json:"ownerUserId,omitempty"`
|
||||||
|
Balance int `json:"balance,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfigResponse from /auth/skill-credit/client-config
|
||||||
|
type ClientConfigResponse struct {
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var retryableStatuses = map[int]bool{401: true, 403: true}
|
||||||
|
var retryableMarkers = []string{
|
||||||
|
"session not found or expired",
|
||||||
|
"invalid or expired token",
|
||||||
|
"unauthorized",
|
||||||
|
"client key expired",
|
||||||
|
"client key revoked",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryable(resp APIResponse) bool {
|
||||||
|
if !retryableStatuses[resp.Status] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
body := strings.ToLower(resp.Body)
|
||||||
|
if body == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, m := range retryableMarkers {
|
||||||
|
if strings.Contains(body, m) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchSession exchanges CLIENT_KEY for a session token.
|
||||||
|
func fetchSession(cfg Config) (*SessionResponse, error) {
|
||||||
|
if cfg.ClientKey == "" {
|
||||||
|
return nil, fmt.Errorf("CLIENT_KEY is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := fmt.Sprintf(`{"clientKey":"%s"}`, cfg.ClientKey)
|
||||||
|
resp, err := doHTTP("POST", cfg.AuthBase+"/auth/skill-credit/session", "", payload, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("session request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Status < 200 || resp.Status >= 300 {
|
||||||
|
return nil, fmt.Errorf("auth session failed: HTTP %d — %s", resp.Status, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var session SessionResponse
|
||||||
|
if err := json.Unmarshal([]byte(resp.Body), &session); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid session JSON: %w", err)
|
||||||
|
}
|
||||||
|
if session.AccessToken == "" {
|
||||||
|
return nil, fmt.Errorf("missing accessToken in session response: %s", resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchClientConfig retrieves client metadata.
|
||||||
|
func fetchClientConfig(cfg Config) (*ClientConfigResponse, error) {
|
||||||
|
if cfg.ClientKey == "" {
|
||||||
|
return nil, fmt.Errorf("CLIENT_KEY is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{"X-Client-Key": cfg.ClientKey}
|
||||||
|
resp, err := doHTTP("GET", cfg.AuthBase+"/auth/skill-credit/client-config", "", "", headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client-config request failed: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Status < 200 || resp.Status >= 300 {
|
||||||
|
return nil, fmt.Errorf("client-config failed: HTTP %d — %s", resp.Status, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config ClientConfigResponse
|
||||||
|
if err := json.Unmarshal([]byte(resp.Body), &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid client-config JSON: %w", err)
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getToken returns a cached or fresh token.
|
||||||
|
func getToken(cfg Config) (string, error) {
|
||||||
|
cf := cacheFile(cfg.AuthBase, cfg.ClientKey, cfg.CacheDir)
|
||||||
|
if cached := readCachedToken(cf, cfg.MinTTLSec); cached != "" {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := fetchSession(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writeCache(cf, session.AccessToken, session.ExpiresIn)
|
||||||
|
return session.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken bypasses cache and fetches a new token.
|
||||||
|
func refreshToken(cfg Config) (string, error) {
|
||||||
|
cf := cacheFile(cfg.AuthBase, cfg.ClientKey, cfg.CacheDir)
|
||||||
|
deleteCache(cf)
|
||||||
|
|
||||||
|
session, err := fetchSession(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
writeCache(cf, session.AccessToken, session.ExpiresIn)
|
||||||
|
return session.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestWithRetry implements the 3-tier retry logic:
|
||||||
|
// 1. cached/fresh token → try
|
||||||
|
// 2. refresh token with same key → retry
|
||||||
|
// 3. reload .env (maybe CLIENT_KEY changed) → refresh → retry
|
||||||
|
func requestWithRetry(cfg Config, method, path, body string) (*APIResponse, error) {
|
||||||
|
url := cfg.APIBase + path
|
||||||
|
|
||||||
|
// 1. Try with cached or fresh token
|
||||||
|
token, err := getToken(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
first, err := doHTTP(method, url, token, body, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isRetryable(first) {
|
||||||
|
return &first, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Refresh token with same key
|
||||||
|
freshToken, err := refreshToken(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
second, err := doHTTP(method, url, freshToken, body, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isRetryable(second) {
|
||||||
|
return &second, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Reload .env — CLIENT_KEY may have changed
|
||||||
|
oldKey := cfg.ClientKey
|
||||||
|
cfg = reloadConfig(cfg)
|
||||||
|
if cfg.ClientKey != oldKey {
|
||||||
|
oldCF := cacheFile(cfg.AuthBase, oldKey, cfg.CacheDir)
|
||||||
|
deleteCache(oldCF)
|
||||||
|
}
|
||||||
|
reloadedToken, err := refreshToken(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
third, err := doHTTP(method, url, reloadedToken, body, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &third, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CachedToken is the on-disk cache format.
|
||||||
|
type CachedToken struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ExpiresAtEpoch int64 `json:"expiresAtEpoch"`
|
||||||
|
CreatedAtEpoch int64 `json:"createdAtEpoch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheFile(authBase, clientKey, cacheDir string) string {
|
||||||
|
h := sha256.Sum256([]byte(authBase + "|" + clientKey))
|
||||||
|
key := fmt.Sprintf("%x", h)
|
||||||
|
os.MkdirAll(cacheDir, 0o755)
|
||||||
|
return filepath.Join(cacheDir, "session_"+key+".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCachedToken(path string, minTTLSec int) string {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var ct CachedToken
|
||||||
|
if err := json.Unmarshal(data, &ct); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ct.AccessToken == "" || ct.ExpiresAtEpoch <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if now+int64(minTTLSec) >= ct.ExpiresAtEpoch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCache(path string, token string, expiresIn int) {
|
||||||
|
if expiresIn <= 0 {
|
||||||
|
expiresIn = 900
|
||||||
|
}
|
||||||
|
now := time.Now().Unix()
|
||||||
|
ct := CachedToken{
|
||||||
|
AccessToken: token,
|
||||||
|
ExpiresAtEpoch: now + int64(expiresIn),
|
||||||
|
CreatedAtEpoch: now,
|
||||||
|
}
|
||||||
|
data, _ := json.MarshalIndent(ct, "", " ")
|
||||||
|
os.WriteFile(path, data, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteCache(path string) {
|
||||||
|
os.Remove(path)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds all auth configuration.
|
||||||
|
type Config struct {
|
||||||
|
AuthBase string
|
||||||
|
ClientKey string
|
||||||
|
CacheDir string
|
||||||
|
MinTTLSec int
|
||||||
|
APIBase string // for business API calls, defaults to AuthBase
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig reads ~/.openclaw/.env and builds Config.
|
||||||
|
func loadConfig() Config {
|
||||||
|
loadEnvFile()
|
||||||
|
|
||||||
|
authBase := strings.TrimRight(envOrDefault("AUTH_BASE", "https://api-gw-test.yuanwei-lnc.com"), "/")
|
||||||
|
clientKey := os.Getenv("CLIENT_KEY")
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
AuthBase: authBase,
|
||||||
|
ClientKey: clientKey,
|
||||||
|
CacheDir: envOrDefault("AUTH_CACHE_DIR", "/tmp/skill-auth-cache"),
|
||||||
|
MinTTLSec: envOrDefaultInt("AUTH_MIN_TTL_SEC", 60),
|
||||||
|
APIBase: strings.TrimRight(envOrDefault("ECOM_BASE", authBase), "/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadConfig re-reads .env from disk and rebuilds config.
|
||||||
|
func reloadConfig(old Config) Config {
|
||||||
|
loadEnvFile()
|
||||||
|
return Config{
|
||||||
|
AuthBase: strings.TrimRight(envOrDefault("AUTH_BASE", "https://api-gw-test.yuanwei-lnc.com"), "/"),
|
||||||
|
ClientKey: os.Getenv("CLIENT_KEY"),
|
||||||
|
CacheDir: envOrDefault("AUTH_CACHE_DIR", "/tmp/skill-auth-cache"),
|
||||||
|
MinTTLSec: envOrDefaultInt("AUTH_MIN_TTL_SEC", 60),
|
||||||
|
APIBase: old.APIBase, // preserve explicit override
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEnvFile reads ~/.openclaw/.env into os environment.
|
||||||
|
// File values always override existing env vars (no caching).
|
||||||
|
func loadEnvFile() {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envPath := filepath.Join(home, ".openclaw", ".env")
|
||||||
|
|
||||||
|
f, err := os.Open(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := strings.Index(line, "=")
|
||||||
|
if idx < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(line[:idx])
|
||||||
|
value := strings.TrimSpace(line[idx+1:])
|
||||||
|
|
||||||
|
// strip surrounding quotes
|
||||||
|
if len(value) >= 2 {
|
||||||
|
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||||
|
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOrDefault(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOrDefaultInt(key string, def int) int {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, c := range v {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
n = n*10 + int(c-'0')
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/agent-skills/auth-rt
|
||||||
|
|
||||||
|
go 1.25.7
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var httpClient = &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIResponse mirrors the TS ApiResponse.
|
||||||
|
type APIResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// doHTTP performs an HTTP request and returns status + body.
|
||||||
|
func doHTTP(method, url, authToken, body string, extraHeaders map[string]string) (APIResponse, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != "" {
|
||||||
|
bodyReader = strings.NewReader(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if authToken != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||||
|
}
|
||||||
|
for k, v := range extraHeaders {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return APIResponse{
|
||||||
|
Status: resp.StatusCode,
|
||||||
|
Body: string(respBody),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printJSON(v interface{}) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "JSON marshal error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fatal(msg string) {
|
||||||
|
fmt.Fprintln(os.Stderr, msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fatal(`Usage:
|
||||||
|
auth-rt token
|
||||||
|
auth-rt session
|
||||||
|
auth-rt client-config
|
||||||
|
auth-rt request <METHOD> <path> [--body JSON] [--api-base URL]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := os.Args[1:]
|
||||||
|
if len(args) == 0 {
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
command := args[0]
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "token":
|
||||||
|
cfg := loadConfig()
|
||||||
|
session, err := fetchSession(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err.Error())
|
||||||
|
}
|
||||||
|
fmt.Printf(`{"token":"%s"}`, session.AccessToken)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
case "session":
|
||||||
|
cfg := loadConfig()
|
||||||
|
session, err := fetchSession(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err.Error())
|
||||||
|
}
|
||||||
|
printJSON(session)
|
||||||
|
|
||||||
|
case "client-config":
|
||||||
|
cfg := loadConfig()
|
||||||
|
config, err := fetchClientConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err.Error())
|
||||||
|
}
|
||||||
|
printJSON(config)
|
||||||
|
|
||||||
|
case "request":
|
||||||
|
if len(args) < 3 {
|
||||||
|
fatal("request requires <METHOD> <path>")
|
||||||
|
}
|
||||||
|
method := strings.ToUpper(args[1])
|
||||||
|
path := args[2]
|
||||||
|
|
||||||
|
var body, apiBase string
|
||||||
|
for i := 3; i < len(args); i++ {
|
||||||
|
if args[i] == "--body" && i+1 < len(args) {
|
||||||
|
i++
|
||||||
|
body = args[i]
|
||||||
|
} else if args[i] == "--api-base" && i+1 < len(args) {
|
||||||
|
i++
|
||||||
|
apiBase = args[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validMethods := map[string]bool{
|
||||||
|
"GET": true, "POST": true, "PUT": true,
|
||||||
|
"PATCH": true, "DELETE": true, "HEAD": true,
|
||||||
|
}
|
||||||
|
if !validMethods[method] {
|
||||||
|
fatal("Invalid method: " + method)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := loadConfig()
|
||||||
|
if apiBase != "" {
|
||||||
|
cfg.APIBase = apiBase
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := requestWithRetry(cfg, method, path, body)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err.Error())
|
||||||
|
}
|
||||||
|
printJSON(result)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fatal("Unknown command: " + command)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
install.sh
71
install.sh
|
|
@ -1,22 +1,65 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install auth-rt CLI wrapper to ~/.openclaw/bin/
|
# Install auth-rt binary.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Mode 1 (local build): ./install.sh [bin-dir]
|
||||||
# ./install.sh
|
# Requires Go. Compiles from source and installs.
|
||||||
# ./install.sh /custom/bin/dir
|
#
|
||||||
|
# Mode 2 (download): ./install.sh --download [bin-dir]
|
||||||
|
# Downloads pre-built binary from Forgejo release.
|
||||||
|
#
|
||||||
|
# Default bin dir: ~/.local/bin
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
BIN_DIR="${1:-$HOME/.local/bin}"
|
FORGEJO="http://192.168.0.108:3030"
|
||||||
SELF_DIR="$(pwd)"
|
REPO="agent-skills/auth-runtime"
|
||||||
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
MODE="local"
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--download) MODE="download" ;;
|
||||||
|
-*) echo "Unknown flag: $arg"; exit 1 ;;
|
||||||
|
*) BIN_DIR="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
mkdir -p "$BIN_DIR"
|
mkdir -p "$BIN_DIR"
|
||||||
|
|
||||||
cat > "$BIN_DIR/auth-rt" <<WRAPPER
|
if [ "$MODE" = "download" ]; then
|
||||||
#!/usr/bin/env bash
|
# Detect platform
|
||||||
exec bun run "$SELF_DIR/src/cli.ts" "\$@"
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
WRAPPER
|
ARCH="$(uname -m)"
|
||||||
chmod +x "$BIN_DIR/auth-rt"
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "Installed: $BIN_DIR/auth-rt → $SELF_DIR/src/cli.ts"
|
ASSET="auth-rt-${OS}-${ARCH}"
|
||||||
echo "Ensure $BIN_DIR is in your PATH."
|
URL="$FORGEJO/$REPO/releases/download/latest/$ASSET"
|
||||||
|
|
||||||
|
echo "Downloading $ASSET..."
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
curl -fsSL "$URL" -o "$BIN_DIR/auth-rt"
|
||||||
|
elif command -v wget &>/dev/null; then
|
||||||
|
wget -q "$URL" -O "$BIN_DIR/auth-rt"
|
||||||
|
else
|
||||||
|
echo "ERROR: curl or wget required"; exit 1
|
||||||
|
fi
|
||||||
|
chmod +x "$BIN_DIR/auth-rt"
|
||||||
|
echo "Installed: $BIN_DIR/auth-rt (downloaded)"
|
||||||
|
else
|
||||||
|
# Local build
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
CLI_DIR="$SCRIPT_DIR/cli"
|
||||||
|
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
echo "Go not found. Use --download to fetch pre-built binary instead."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building auth-rt..."
|
||||||
|
cd "$CLI_DIR"
|
||||||
|
go build -ldflags="-s -w" -o "$BIN_DIR/auth-rt" .
|
||||||
|
echo "Installed: $BIN_DIR/auth-rt (built from source)"
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
* Thin CLI wrapper for auth-runtime.
|
* Thin CLI wrapper for auth-runtime.
|
||||||
*
|
*
|
||||||
* Copy this file into your skill's src/ directory. It calls the
|
* Copy this file into your skill's src/ directory. It calls the
|
||||||
* `auth-rt` binary (compiled from auth-runtime), so the skill has
|
* `auth-rt` binary (a standalone Go executable), so the skill has
|
||||||
* zero npm dependency on @clawd/auth-runtime.
|
* zero npm/runtime dependency on auth-runtime.
|
||||||
*
|
*
|
||||||
* Prerequisites:
|
* Prerequisites:
|
||||||
* ~/.openclaw/bin/auth-rt must exist (run auth-runtime/install.sh)
|
* `auth-rt` must be in PATH or at ~/.local/bin/auth-rt
|
||||||
|
* (install.sh handles this automatically)
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* import { createSkillClient } from './auth-cli.ts';
|
* import { createSkillClient } from './auth-cli.ts';
|
||||||
|
|
@ -20,7 +21,14 @@ import * as os from 'os';
|
||||||
|
|
||||||
const home = process.env.HOME || os.homedir();
|
const home = process.env.HOME || os.homedir();
|
||||||
const AUTH_RT_BIN = process.env.AUTH_RT_BIN
|
const AUTH_RT_BIN = process.env.AUTH_RT_BIN
|
||||||
|| path.join(home, '.local', 'bin', 'auth-rt');
|
|| (() => {
|
||||||
|
// Check if auth-rt is in PATH
|
||||||
|
const which = spawnSync('which', ['auth-rt'], { encoding: 'utf-8' });
|
||||||
|
if (which.status === 0 && which.stdout.trim()) {
|
||||||
|
return which.stdout.trim();
|
||||||
|
}
|
||||||
|
return path.join(home, '.local', 'bin', 'auth-rt');
|
||||||
|
})();
|
||||||
|
|
||||||
export interface ApiResponse {
|
export interface ApiResponse {
|
||||||
status: number;
|
status: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue