ScuttleBot

scuttlebot / cmd / scuttlectl / internal / apiclient / apiclient.go
Blame History Raw 265 lines
1
// Package apiclient is a minimal HTTP client for the scuttlebot REST API.
2
package apiclient
3
4
import (
5
"bytes"
6
"encoding/json"
7
"fmt"
8
"io"
9
"net/http"
10
"strings"
11
)
12
13
// Client calls the scuttlebot REST API.
14
type Client struct {
15
base string
16
token string
17
http *http.Client
18
}
19
20
// New creates a Client targeting baseURL (e.g. "http://localhost:8080") with
21
// the given bearer token.
22
func New(baseURL, token string) *Client {
23
return &Client{base: baseURL, token: token, http: &http.Client{}}
24
}
25
26
// Status returns the raw JSON bytes from GET /v1/status.
27
func (c *Client) Status() (json.RawMessage, error) {
28
return c.get("/v1/status")
29
}
30
31
// ListAgents returns the raw JSON bytes from GET /v1/agents.
32
func (c *Client) ListAgents() (json.RawMessage, error) {
33
return c.get("/v1/agents")
34
}
35
36
// GetAgent returns the raw JSON bytes from GET /v1/agents/{nick}.
37
func (c *Client) GetAgent(nick string) (json.RawMessage, error) {
38
return c.get("/v1/agents/" + nick)
39
}
40
41
// RegisterAgent sends POST /v1/agents/register and returns raw JSON.
42
func (c *Client) RegisterAgent(nick, agentType string, channels []string) (json.RawMessage, error) {
43
body := map[string]any{"nick": nick}
44
if agentType != "" {
45
body["type"] = agentType
46
}
47
if len(channels) > 0 {
48
body["channels"] = channels
49
}
50
return c.post("/v1/agents/register", body)
51
}
52
53
// RevokeAgent sends POST /v1/agents/{nick}/revoke.
54
func (c *Client) RevokeAgent(nick string) error {
55
_, err := c.post("/v1/agents/"+nick+"/revoke", nil)
56
return err
57
}
58
59
// RotateAgent sends POST /v1/agents/{nick}/rotate and returns raw JSON.
60
func (c *Client) RotateAgent(nick string) (json.RawMessage, error) {
61
return c.post("/v1/agents/"+nick+"/rotate", nil)
62
}
63
64
// DeleteAgent sends DELETE /v1/agents/{nick}.
65
func (c *Client) DeleteAgent(nick string) error {
66
_, err := c.doNoBody("DELETE", "/v1/agents/"+nick)
67
return err
68
}
69
70
// ChannelUsers sends GET /v1/channels/{channel}/users and returns raw JSON.
71
func (c *Client) ChannelUsers(channel string) (json.RawMessage, error) {
72
return c.get("/v1/channels/" + channel + "/users")
73
}
74
75
// DeleteChannel sends DELETE /v1/channels/{channel}.
76
func (c *Client) DeleteChannel(channel string) error {
77
channel = strings.TrimPrefix(channel, "#")
78
_, err := c.doNoBody("DELETE", "/v1/channels/"+channel)
79
return err
80
}
81
82
// ListChannels sends GET /v1/channels and returns raw JSON.
83
func (c *Client) ListChannels() (json.RawMessage, error) {
84
return c.get("/v1/channels")
85
}
86
87
// ListLLMBackends sends GET /v1/llm/backends and returns raw JSON.
88
func (c *Client) ListLLMBackends() (json.RawMessage, error) {
89
return c.get("/v1/llm/backends")
90
}
91
92
// GetLLMBackend sends GET /v1/llm/backends and finds the named backend, returning raw JSON.
93
func (c *Client) GetLLMBackend(name string) (json.RawMessage, error) {
94
raw, err := c.get("/v1/llm/backends")
95
if err != nil {
96
return nil, err
97
}
98
var resp struct {
99
Backends []json.RawMessage `json:"backends"`
100
}
101
if err := json.Unmarshal(raw, &resp); err != nil {
102
return nil, err
103
}
104
for _, b := range resp.Backends {
105
var named struct {
106
Name string `json:"name"`
107
}
108
if json.Unmarshal(b, &named) == nil && named.Name == name {
109
return b, nil
110
}
111
}
112
return nil, fmt.Errorf("backend %q not found", name)
113
}
114
115
// CreateLLMBackend sends POST /v1/llm/backends.
116
func (c *Client) CreateLLMBackend(cfg map[string]any) error {
117
_, err := c.post("/v1/llm/backends", cfg)
118
return err
119
}
120
121
// DeleteLLMBackend sends DELETE /v1/llm/backends/{name}.
122
func (c *Client) DeleteLLMBackend(name string) error {
123
_, err := c.doNoBody("DELETE", "/v1/llm/backends/"+name)
124
return err
125
}
126
127
// ListAdmins sends GET /v1/admins and returns raw JSON.
128
func (c *Client) ListAdmins() (json.RawMessage, error) {
129
return c.get("/v1/admins")
130
}
131
132
// AddAdmin sends POST /v1/admins and returns raw JSON.
133
func (c *Client) AddAdmin(username, password string) (json.RawMessage, error) {
134
return c.post("/v1/admins", map[string]string{"username": username, "password": password})
135
}
136
137
// RemoveAdmin sends DELETE /v1/admins/{username}.
138
func (c *Client) RemoveAdmin(username string) error {
139
_, err := c.doNoBody("DELETE", "/v1/admins/"+username)
140
return err
141
}
142
143
// ListAPIKeys returns GET /v1/api-keys.
144
func (c *Client) ListAPIKeys() (json.RawMessage, error) {
145
return c.get("/v1/api-keys")
146
}
147
148
// CreateAPIKey sends POST /v1/api-keys.
149
func (c *Client) CreateAPIKey(name string, scopes []string, expiresIn string) (json.RawMessage, error) {
150
body := map[string]any{"name": name, "scopes": scopes}
151
if expiresIn != "" {
152
body["expires_in"] = expiresIn
153
}
154
return c.post("/v1/api-keys", body)
155
}
156
157
// RevokeAPIKey sends DELETE /v1/api-keys/{id}.
158
func (c *Client) RevokeAPIKey(id string) error {
159
_, err := c.doNoBody("DELETE", "/v1/api-keys/"+id)
160
return err
161
}
162
163
// GetTopology returns GET /v1/topology.
164
func (c *Client) GetTopology() (json.RawMessage, error) {
165
return c.get("/v1/topology")
166
}
167
168
// ProvisionChannel sends POST /v1/channels.
169
func (c *Client) ProvisionChannel(name string) (json.RawMessage, error) {
170
return c.post("/v1/channels", map[string]string{"name": name})
171
}
172
173
// DropChannel sends DELETE /v1/topology/channels/{channel}.
174
func (c *Client) DropChannel(channel string) error {
175
_, err := c.doNoBody("DELETE", "/v1/topology/channels/"+strings.TrimPrefix(channel, "#"))
176
return err
177
}
178
179
// GetConfig returns GET /v1/config.
180
func (c *Client) GetConfig() (json.RawMessage, error) {
181
return c.get("/v1/config")
182
}
183
184
// GetConfigHistory returns GET /v1/config/history.
185
func (c *Client) GetConfigHistory() (json.RawMessage, error) {
186
return c.get("/v1/config/history")
187
}
188
189
// GetSettings returns GET /v1/settings.
190
func (c *Client) GetSettings() (json.RawMessage, error) {
191
return c.get("/v1/settings")
192
}
193
194
// SetAdminPassword sends PUT /v1/admins/{username}/password.
195
func (c *Client) SetAdminPassword(username, password string) error {
196
_, err := c.put("/v1/admins/"+username+"/password", map[string]string{"password": password})
197
return err
198
}
199
200
func (c *Client) get(path string) (json.RawMessage, error) {
201
return c.do("GET", path, nil)
202
}
203
204
func (c *Client) post(path string, body any) (json.RawMessage, error) {
205
var buf bytes.Buffer
206
if body != nil {
207
if err := json.NewEncoder(&buf).Encode(body); err != nil {
208
return nil, err
209
}
210
}
211
return c.do("POST", path, &buf)
212
}
213
214
func (c *Client) put(path string, body any) (json.RawMessage, error) {
215
var buf bytes.Buffer
216
if body != nil {
217
if err := json.NewEncoder(&buf).Encode(body); err != nil {
218
return nil, err
219
}
220
}
221
return c.do("PUT", path, &buf)
222
}
223
224
func (c *Client) doNoBody(method, path string) (json.RawMessage, error) {
225
return c.do(method, path, nil)
226
}
227
228
func (c *Client) do(method, path string, body io.Reader) (json.RawMessage, error) {
229
req, err := http.NewRequest(method, c.base+path, body)
230
if err != nil {
231
return nil, err
232
}
233
req.Header.Set("Authorization", "Bearer "+c.token)
234
if body != nil {
235
req.Header.Set("Content-Type", "application/json")
236
}
237
238
resp, err := c.http.Do(req)
239
if err != nil {
240
return nil, fmt.Errorf("request failed: %w", err)
241
}
242
defer resp.Body.Close()
243
244
data, err := io.ReadAll(resp.Body)
245
if err != nil {
246
return nil, err
247
}
248
249
if resp.StatusCode >= 400 {
250
// Try to extract error message from JSON body.
251
var apiErr struct {
252
Error string `json:"error"`
253
}
254
if json.Unmarshal(data, &apiErr) == nil && apiErr.Error != "" {
255
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, apiErr.Error)
256
}
257
return nil, fmt.Errorf("API error %d", resp.StatusCode)
258
}
259
260
if len(data) == 0 {
261
return nil, nil
262
}
263
return json.RawMessage(data), nil
264
}
265

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button