code.oscarkilo.com/okg

Hash:
95b1523dd98958b5884ef0efba951419eda7a49c
Author:
Igor Naverniouk <[email protected]>
Date:
Thu Jun 4 20:38:57 2026 -0400
Message:
okg: extract //okg/internal/rest as shared HTTP helper okg/klee duplicated the host/api-key/JSON-marshaling plumbing that the soon-to-arrive okg/who needs too. Move it into a sibling internal package — //okg/internal/rest — and refactor klee to embed *rest.Client. - rest.Client carries Host, APIKey, HTTP. - rest.Client.{Do, GetJSON, PostJSON, PatchJSON} are the shared methods. - klee.Client becomes `{*rest.Client; <typed klee methods>}`. No behavior change. Existing klee tests pass through the embedded methods. Drops the public klee.Client.PostJSON wrapper, which was only used by the now-removed `okg auth login --user` flow.
diff --git a/internal/rest/rest.go b/internal/rest/rest.go
new file mode 100644
index 0000000..1a5c0c2
--- /dev/null
+++ b/internal/rest/rest.go
@@ -0,0 +1,100 @@
+// Package rest is the shared base for okg's per-service HTTP
+// clients (//okg/klee, //okg/who, etc.). It owns the
+// host/api-key/transport plumbing and JSON marshaling; each
+// domain-specific client embeds *Client and adds typed methods.
+package rest
+
+import "encoding/json"
+import "fmt"
+import "io"
+import "net/http"
+import "strings"
+
+// Client carries the host, API key, and underlying http.Client
+// that the JSON helpers below dispatch through.
+type Client struct {
+ Host string
+ APIKey string
+ HTTP *http.Client
+}
+
+func NewClient(host, apiKey string) *Client {
+ return &Client{
+ Host: host,
+ APIKey: apiKey,
+ HTTP: &http.Client{},
+ }
+}
+
+// Do builds a request to c.Host+path with the standard headers
+// (Accept, Authorization, Content-Type when a body is present)
+// and runs it through c.HTTP.
+func (c *Client) Do(
+ method, path string, body io.Reader,
+) (*http.Response, error) {
+ req, err := http.NewRequest(method, c.Host+path, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Accept", "application/json")
+ if c.APIKey != "" {
+ req.Header.Set(
+ "Authorization", "Bearer "+c.APIKey)
+ }
+ if body != nil {
+ req.Header.Set(
+ "Content-Type", "application/json")
+ }
+ return c.HTTP.Do(req)
+}
+
+// GetJSON issues a GET, returns an HTTP-status error on 4xx/5xx,
+// and decodes a successful body into dst (or skips decode if
+// dst is nil).
+func (c *Client) GetJSON(
+ path string, dst interface{},
+) error {
+ return c.exchange("GET", path, nil, dst)
+}
+
+// PostJSON issues a POST with payload (omitted if nil) and
+// decodes the response into dst (or skips decode if dst is nil).
+func (c *Client) PostJSON(
+ path string, payload, dst interface{},
+) error {
+ return c.exchange("POST", path, payload, dst)
+}
+
+// PatchJSON issues a PATCH; payload is required.
+func (c *Client) PatchJSON(
+ path string, payload, dst interface{},
+) error {
+ return c.exchange("PATCH", path, payload, dst)
+}
+
+func (c *Client) exchange(
+ method, path string, payload, dst interface{},
+) error {
+ var body io.Reader
+ if payload != nil {
+ data, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+ body = strings.NewReader(string(data))
+ }
+ resp, err := c.Do(method, path, body)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf(
+ "HTTP %d: %s", resp.StatusCode, b)
+ }
+ if dst != nil {
+ return json.NewDecoder(resp.Body).Decode(dst)
+ }
+ return nil
+}
diff --git a/klee/klee.go b/klee/klee.go
index 106e64e..35029e1 100644
--- a/klee/klee.go
+++ b/klee/klee.go
@@ -2,27 +2,22 @@
// git server API (code.oscarkilo.com).
package klee

-import "encoding/json"
import "fmt"
-import "io"
-import "net/http"
import "strings"
import "time"

-// Client talks to a Klee git server.
+import "oscarkilo.com/okg/internal/rest"
+
+// Client talks to a Klee git server. Embeds the shared rest
+// helpers (GetJSON, PostJSON, PatchJSON, Do); klee-specific
+// methods live below.
type Client struct {
- Host string // e.g. "https://code.oscarkilo.com"
- APIKey string // Bearer token
- HTTP *http.Client
+ *rest.Client
}

// NewClient creates a Klee client.
func NewClient(host, api_key string) *Client {
- return &Client{
- Host: host,
- APIKey: api_key,
- HTTP: &http.Client{},
- }
+ return &Client{Client: rest.NewClient(host, api_key)}
}

// PR is a pull request on Klee.
@@ -105,7 +100,7 @@ func (c *Client) ListPRs(
path := fmt.Sprintf(
"/%s/prs?state=%s", repo, state)
var prs []PR
- if err := c.getJSON(path, &prs); err != nil {
+ if err := c.GetJSON(path, &prs); err != nil {
return nil, err
}
return prs, nil
@@ -118,7 +113,7 @@ func (c *Client) GetPR(
path := fmt.Sprintf(
"/%s/pr/%d", repo, number)
var pr PR
- if err := c.getJSON(path, &pr); err != nil {
+ if err := c.GetJSON(path, &pr); err != nil {
return nil, err
}
return &pr, nil
@@ -130,7 +125,7 @@ func (c *Client) CreatePR(
) (*PR, error) {
path := fmt.Sprintf("/%s/prs", repo)
var pr PR
- if err := c.postJSON(path, req, &pr); err != nil {
+ if err := c.PostJSON(path, req, &pr); err != nil {
return nil, err
}
return &pr, nil
@@ -143,7 +138,7 @@ func (c *Client) GetComments(
path := fmt.Sprintf(
"/%s/pr/%d/comments", repo, number)
var comments []Comment
- if err := c.getJSON(path, &comments); err != nil {
+ if err := c.GetJSON(path, &comments); err != nil {
return nil, err
}
return comments, nil
@@ -156,7 +151,7 @@ func (c *Client) AddComment(
path := fmt.Sprintf(
"/%s/pr/%d/comments", repo, number)
var comment Comment
- if err := c.postJSON(
+ if err := c.PostJSON(
path, req, &comment); err != nil {
return nil, err
}
@@ -170,7 +165,7 @@ func (c *Client) MergePR(
path := fmt.Sprintf(
"/%s/pr/%d/merge", repo, number)
var pr PR
- if err := c.postJSON(path, nil, &pr); err != nil {
+ if err := c.PostJSON(path, nil, &pr); err != nil {
return nil, err
}
return &pr, nil
@@ -184,7 +179,7 @@ func (c *Client) SetPRState(
"/%s/pr/%d", repo, number)
payload := map[string]string{"state": state}
var pr PR
- if err := c.patchJSON(
+ if err := c.PatchJSON(
path, payload, &pr); err != nil {
return nil, err
}
@@ -198,7 +193,7 @@ func (c *Client) ListRepos() (
*LsResponse, error,
) {
var res LsResponse
- if err := c.getJSON("/.ls", &res); err != nil {
+ if err := c.GetJSON("/.ls", &res); err != nil {
return nil, err
}
return &res, nil
@@ -208,113 +203,7 @@ func (c *Client) ListRepos() (
func (c *Client) CreateRepo(
req CreateRepoRequest,
) error {
- return c.postJSON("/.add-repo", req, nil)
-}
-
-// --- HTTP helpers ---
-
-func (c *Client) do(
- method, path string, body io.Reader,
-) (*http.Response, error) {
- url := c.Host + path
- req, err := http.NewRequest(method, url, body)
- if err != nil {
- return nil, err
- }
- req.Header.Set("Accept", "application/json")
- if c.APIKey != "" {
- req.Header.Set("Authorization",
- "Bearer "+c.APIKey)
- }
- if body != nil {
- req.Header.Set(
- "Content-Type", "application/json")
- }
- return c.HTTP.Do(req)
-}
-
-func (c *Client) getJSON(
- path string, dst interface{},
-) error {
- resp, err := c.do("GET", path, nil)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- if resp.StatusCode >= 400 {
- b, _ := io.ReadAll(resp.Body)
- return fmt.Errorf(
- "HTTP %d: %s", resp.StatusCode, b)
- }
- return json.NewDecoder(resp.Body).Decode(dst)
-}
-
-func (c *Client) postJSON(
- path string,
- payload interface{},
- dst interface{},
-) error {
- var body io.Reader
- if payload != nil {
- data, err := json.Marshal(payload)
- if err != nil {
- return err
- }
- body = strings.NewReader(string(data))
- }
- resp, err := c.do("POST", path, body)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- if resp.StatusCode >= 400 {
- b, _ := io.ReadAll(resp.Body)
- return fmt.Errorf(
- "HTTP %d: %s", resp.StatusCode, b)
- }
- if dst != nil {
- return json.NewDecoder(
- resp.Body).Decode(dst)
- }
- return nil
-}
-
-func (c *Client) patchJSON(
- path string,
- payload interface{},
- dst interface{},
-) error {
- data, err := json.Marshal(payload)
- if err != nil {
- return err
- }
- body := strings.NewReader(string(data))
- resp, err := c.do("PATCH", path, body)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- if resp.StatusCode >= 400 {
- b, _ := io.ReadAll(resp.Body)
- return fmt.Errorf(
- "HTTP %d: %s", resp.StatusCode, b)
- }
- if dst != nil {
- return json.NewDecoder(
- resp.Body).Decode(dst)
- }
- return nil
-}
-
-// PostJSON performs a POST with a JSON body and
-// decodes the response. Useful for endpoints not
-// covered by typed methods.
-func (c *Client) PostJSON(
- path string,
- payload interface{},
- dst interface{},
-) error {
- return c.postJSON(path, payload, dst)
+ return c.PostJSON("/.add-repo", req, nil)
}

// AgentName extracts the agent name from a Klee
/dev/null
b/internal/rest/rest.go
1
// Package rest is the shared base for okg's per-service HTTP
2
// clients (//okg/klee, //okg/who, etc.). It owns the
3
// host/api-key/transport plumbing and JSON marshaling; each
4
// domain-specific client embeds *Client and adds typed methods.
5
package rest
6
7
import "encoding/json"
8
import "fmt"
9
import "io"
10
import "net/http"
11
import "strings"
12
13
// Client carries the host, API key, and underlying http.Client
14
// that the JSON helpers below dispatch through.
15
type Client struct {
16
Host string
17
APIKey string
18
HTTP *http.Client
19
}
20
21
func NewClient(host, apiKey string) *Client {
22
return &Client{
23
Host: host,
24
APIKey: apiKey,
25
HTTP: &http.Client{},
26
}
27
}
28
29
// Do builds a request to c.Host+path with the standard headers
30
// (Accept, Authorization, Content-Type when a body is present)
31
// and runs it through c.HTTP.
32
func (c *Client) Do(
33
method, path string, body io.Reader,
34
) (*http.Response, error) {
35
req, err := http.NewRequest(method, c.Host+path, body)
36
if err != nil {
37
return nil, err
38
}
39
req.Header.Set("Accept", "application/json")
40
if c.APIKey != "" {
41
req.Header.Set(
42
"Authorization", "Bearer "+c.APIKey)
43
}
44
if body != nil {
45
req.Header.Set(
46
"Content-Type", "application/json")
47
}
48
return c.HTTP.Do(req)
49
}
50
51
// GetJSON issues a GET, returns an HTTP-status error on 4xx/5xx,
52
// and decodes a successful body into dst (or skips decode if
53
// dst is nil).
54
func (c *Client) GetJSON(
55
path string, dst interface{},
56
) error {
57
return c.exchange("GET", path, nil, dst)
58
}
59
60
// PostJSON issues a POST with payload (omitted if nil) and
61
// decodes the response into dst (or skips decode if dst is nil).
62
func (c *Client) PostJSON(
63
path string, payload, dst interface{},
64
) error {
65
return c.exchange("POST", path, payload, dst)
66
}
67
68
// PatchJSON issues a PATCH; payload is required.
69
func (c *Client) PatchJSON(
70
path string, payload, dst interface{},
71
) error {
72
return c.exchange("PATCH", path, payload, dst)
73
}
74
75
func (c *Client) exchange(
76
method, path string, payload, dst interface{},
77
) error {
78
var body io.Reader
79
if payload != nil {
80
data, err := json.Marshal(payload)
81
if err != nil {
82
return err
83
}
84
body = strings.NewReader(string(data))
85
}
86
resp, err := c.Do(method, path, body)
87
if err != nil {
88
return err
89
}
90
defer resp.Body.Close()
91
if resp.StatusCode >= 400 {
92
b, _ := io.ReadAll(resp.Body)
93
return fmt.Errorf(
94
"HTTP %d: %s", resp.StatusCode, b)
95
}
96
if dst != nil {
97
return json.NewDecoder(resp.Body).Decode(dst)
98
}
99
return nil
100
}
a/klee/klee.go
b/klee/klee.go
1
// Package klee provides a Go client for the Klee
1
// Package klee provides a Go client for the Klee
2
// git server API (code.oscarkilo.com).
2
// git server API (code.oscarkilo.com).
3
package klee
3
package klee
4
4
5
import "encoding/json"
6
import "fmt"
5
import "fmt"
7
import "io"
8
import "net/http"
9
import "strings"
6
import "strings"
10
import "time"
7
import "time"
11
8
12
// Client talks to a Klee git server.
9
import "oscarkilo.com/okg/internal/rest"
10
11
// Client talks to a Klee git server. Embeds the shared rest
12
// helpers (GetJSON, PostJSON, PatchJSON, Do); klee-specific
13
// methods live below.
13
type Client struct {
14
type Client struct {
14
Host string // e.g. "https://code.oscarkilo.com"
15
*rest.Client
15
APIKey string // Bearer token
16
HTTP *http.Client
17
}
16
}
18
17
19
// NewClient creates a Klee client.
18
// NewClient creates a Klee client.
20
func NewClient(host, api_key string) *Client {
19
func NewClient(host, api_key string) *Client {
21
return &Client{
20
return &Client{Client: rest.NewClient(host, api_key)}
22
Host: host,
23
APIKey: api_key,
24
HTTP: &http.Client{},
25
}
26
}
21
}
27
22
28
// PR is a pull request on Klee.
23
// PR is a pull request on Klee.
29
type PR struct {
24
type PR struct {
30
Number int `json:"number"`
25
Number int `json:"number"`
31
Title string `json:"title"`
26
Title string `json:"title"`
32
Body string `json:"body"`
27
Body string `json:"body"`
33
State string `json:"state"`
28
State string `json:"state"`
34
Merged bool `json:"merged"`
29
Merged bool `json:"merged"`
35
Head string `json:"head"`
30
Head string `json:"head"`
36
Base string `json:"base"`
31
Base string `json:"base"`
37
Author string `json:"author"`
32
Author string `json:"author"`
38
Files []string `json:"files"`
33
Files []string `json:"files"`
39
Created time.Time `json:"created"`
34
Created time.Time `json:"created"`
40
Updated time.Time `json:"updated"`
35
Updated time.Time `json:"updated"`
41
MergedBy string `json:"merged_by,omitempty"`
36
MergedBy string `json:"merged_by,omitempty"`
42
MergedAt time.Time `json:"merged_at,omitempty"`
37
MergedAt time.Time `json:"merged_at,omitempty"`
43
}
38
}
44
39
45
// Comment is a PR comment on Klee.
40
// Comment is a PR comment on Klee.
46
type Comment struct {
41
type Comment struct {
47
ID int `json:"id"`
42
ID int `json:"id"`
48
Author string `json:"author"`
43
Author string `json:"author"`
49
Body string `json:"body"`
44
Body string `json:"body"`
50
Verdict string `json:"verdict,omitempty"`
45
Verdict string `json:"verdict,omitempty"`
51
File string `json:"file,omitempty"`
46
File string `json:"file,omitempty"`
52
Line int `json:"line,omitempty"`
47
Line int `json:"line,omitempty"`
53
Created time.Time `json:"created"`
48
Created time.Time `json:"created"`
54
}
49
}
55
50
56
// CreatePRRequest is the body for creating a PR.
51
// CreatePRRequest is the body for creating a PR.
57
type CreatePRRequest struct {
52
type CreatePRRequest struct {
58
Head string `json:"head"`
53
Head string `json:"head"`
59
Base string `json:"base"`
54
Base string `json:"base"`
60
Title string `json:"title"`
55
Title string `json:"title"`
61
Body string `json:"body"`
56
Body string `json:"body"`
62
}
57
}
63
58
64
// AddCommentRequest is the body for adding a
59
// AddCommentRequest is the body for adding a
65
// comment to a PR.
60
// comment to a PR.
66
type AddCommentRequest struct {
61
type AddCommentRequest struct {
67
Body string `json:"body"`
62
Body string `json:"body"`
68
Verdict string `json:"verdict,omitempty"`
63
Verdict string `json:"verdict,omitempty"`
69
}
64
}
70
65
71
// CreateRepoRequest is the body for creating a
66
// CreateRepoRequest is the body for creating a
72
// repo on Klee.
67
// repo on Klee.
73
type CreateRepoRequest struct {
68
type CreateRepoRequest struct {
74
RepoName string `json:"repo_name"`
69
RepoName string `json:"repo_name"`
75
ReaderUsername string `json:"reader_username"`
70
ReaderUsername string `json:"reader_username"`
76
}
71
}
77
72
78
// RepoInfo describes a repository.
73
// RepoInfo describes a repository.
79
type RepoInfo struct {
74
type RepoInfo struct {
80
Name string `json:"name"`
75
Name string `json:"name"`
81
IsPublic bool `json:"is_public"`
76
IsPublic bool `json:"is_public"`
82
Authz *RepoInfoAuthz `json:"authz"`
77
Authz *RepoInfoAuthz `json:"authz"`
83
}
78
}
84
79
85
// RepoInfoAuthz describes repo access.
80
// RepoInfoAuthz describes repo access.
86
type RepoInfoAuthz struct {
81
type RepoInfoAuthz struct {
87
IsOwner bool `json:"is_owner"`
82
IsOwner bool `json:"is_owner"`
88
IsReader bool `json:"is_reader"`
83
IsReader bool `json:"is_reader"`
89
OwnerUsername string `json:"owner_username"`
84
OwnerUsername string `json:"owner_username"`
90
ReaderUsername string `json:"reader_username"`
85
ReaderUsername string `json:"reader_username"`
91
}
86
}
92
87
93
// LsResponse is the response from /.ls.
88
// LsResponse is the response from /.ls.
94
type LsResponse struct {
89
type LsResponse struct {
95
Repos []RepoInfo `json:"repos"`
90
Repos []RepoInfo `json:"repos"`
96
CanCreateRepos bool `json:"can_create_repos"`
91
CanCreateRepos bool `json:"can_create_repos"`
97
}
92
}
98
93
99
// --- PR operations ---
94
// --- PR operations ---
100
95
101
// ListPRs lists PRs for a repo by state.
96
// ListPRs lists PRs for a repo by state.
102
func (c *Client) ListPRs(
97
func (c *Client) ListPRs(
103
repo, state string,
98
repo, state string,
104
) ([]PR, error) {
99
) ([]PR, error) {
105
path := fmt.Sprintf(
100
path := fmt.Sprintf(
106
"/%s/prs?state=%s", repo, state)
101
"/%s/prs?state=%s", repo, state)
107
var prs []PR
102
var prs []PR
108
if err := c.getJSON(path, &prs); err != nil {
103
if err := c.GetJSON(path, &prs); err != nil {
109
return nil, err
104
return nil, err
110
}
105
}
111
return prs, nil
106
return prs, nil
112
}
107
}
113
108
114
// GetPR fetches a single PR.
109
// GetPR fetches a single PR.
115
func (c *Client) GetPR(
110
func (c *Client) GetPR(
116
repo string, number int,
111
repo string, number int,
117
) (*PR, error) {
112
) (*PR, error) {
118
path := fmt.Sprintf(
113
path := fmt.Sprintf(
119
"/%s/pr/%d", repo, number)
114
"/%s/pr/%d", repo, number)
120
var pr PR
115
var pr PR
121
if err := c.getJSON(path, &pr); err != nil {
116
if err := c.GetJSON(path, &pr); err != nil {
122
return nil, err
117
return nil, err
123
}
118
}
124
return &pr, nil
119
return &pr, nil
125
}
120
}
126
121
127
// CreatePR creates a new PR.
122
// CreatePR creates a new PR.
128
func (c *Client) CreatePR(
123
func (c *Client) CreatePR(
129
repo string, req CreatePRRequest,
124
repo string, req CreatePRRequest,
130
) (*PR, error) {
125
) (*PR, error) {
131
path := fmt.Sprintf("/%s/prs", repo)
126
path := fmt.Sprintf("/%s/prs", repo)
132
var pr PR
127
var pr PR
133
if err := c.postJSON(path, req, &pr); err != nil {
128
if err := c.PostJSON(path, req, &pr); err != nil {
134
return nil, err
129
return nil, err
135
}
130
}
136
return &pr, nil
131
return &pr, nil
137
}
132
}
138
133
139
// GetComments fetches comments on a PR.
134
// GetComments fetches comments on a PR.
140
func (c *Client) GetComments(
135
func (c *Client) GetComments(
141
repo string, number int,
136
repo string, number int,
142
) ([]Comment, error) {
137
) ([]Comment, error) {
143
path := fmt.Sprintf(
138
path := fmt.Sprintf(
144
"/%s/pr/%d/comments", repo, number)
139
"/%s/pr/%d/comments", repo, number)
145
var comments []Comment
140
var comments []Comment
146
if err := c.getJSON(path, &comments); err != nil {
141
if err := c.GetJSON(path, &comments); err != nil {
147
return nil, err
142
return nil, err
148
}
143
}
149
return comments, nil
144
return comments, nil
150
}
145
}
151
146
152
// AddComment adds a comment to a PR.
147
// AddComment adds a comment to a PR.
153
func (c *Client) AddComment(
148
func (c *Client) AddComment(
154
repo string, number int, req AddCommentRequest,
149
repo string, number int, req AddCommentRequest,
155
) (*Comment, error) {
150
) (*Comment, error) {
156
path := fmt.Sprintf(
151
path := fmt.Sprintf(
157
"/%s/pr/%d/comments", repo, number)
152
"/%s/pr/%d/comments", repo, number)
158
var comment Comment
153
var comment Comment
159
if err := c.postJSON(
154
if err := c.PostJSON(
160
path, req, &comment); err != nil {
155
path, req, &comment); err != nil {
161
return nil, err
156
return nil, err
162
}
157
}
163
return &comment, nil
158
return &comment, nil
164
}
159
}
165
160
166
// MergePR merges a PR.
161
// MergePR merges a PR.
167
func (c *Client) MergePR(
162
func (c *Client) MergePR(
168
repo string, number int,
163
repo string, number int,
169
) (*PR, error) {
164
) (*PR, error) {
170
path := fmt.Sprintf(
165
path := fmt.Sprintf(
171
"/%s/pr/%d/merge", repo, number)
166
"/%s/pr/%d/merge", repo, number)
172
var pr PR
167
var pr PR
173
if err := c.postJSON(path, nil, &pr); err != nil {
168
if err := c.PostJSON(path, nil, &pr); err != nil {
174
return nil, err
169
return nil, err
175
}
170
}
176
return &pr, nil
171
return &pr, nil
177
}
172
}
178
173
179
// SetPRState changes a PR's state (open/closed).
174
// SetPRState changes a PR's state (open/closed).
180
func (c *Client) SetPRState(
175
func (c *Client) SetPRState(
181
repo string, number int, state string,
176
repo string, number int, state string,
182
) (*PR, error) {
177
) (*PR, error) {
183
path := fmt.Sprintf(
178
path := fmt.Sprintf(
184
"/%s/pr/%d", repo, number)
179
"/%s/pr/%d", repo, number)
185
payload := map[string]string{"state": state}
180
payload := map[string]string{"state": state}
186
var pr PR
181
var pr PR
187
if err := c.patchJSON(
182
if err := c.PatchJSON(
188
path, payload, &pr); err != nil {
183
path, payload, &pr); err != nil {
189
return nil, err
184
return nil, err
190
}
185
}
191
return &pr, nil
186
return &pr, nil
192
}
187
}
193
188
194
// --- Repo operations ---
189
// --- Repo operations ---
195
190
196
// ListRepos lists repos accessible to the caller.
191
// ListRepos lists repos accessible to the caller.
197
func (c *Client) ListRepos() (
192
func (c *Client) ListRepos() (
198
*LsResponse, error,
193
*LsResponse, error,
199
) {
194
) {
200
var res LsResponse
195
var res LsResponse
201
if err := c.getJSON("/.ls", &res); err != nil {
196
if err := c.GetJSON("/.ls", &res); err != nil {
202
return nil, err
197
return nil, err
203
}
198
}
204
return &res, nil
199
return &res, nil
205
}
200
}
206
201
207
// CreateRepo creates a new repo on Klee.
202
// CreateRepo creates a new repo on Klee.
208
func (c *Client) CreateRepo(
203
func (c *Client) CreateRepo(
209
req CreateRepoRequest,
204
req CreateRepoRequest,
210
) error {
205
) error {
211
return c.postJSON("/.add-repo", req, nil)
206
return c.PostJSON("/.add-repo", req, nil)
212
}
213
214
// --- HTTP helpers ---
215
216
func (c *Client) do(
217
method, path string, body io.Reader,
218
) (*http.Response, error) {
219
url := c.Host + path
220
req, err := http.NewRequest(method, url, body)
221
if err != nil {
222
return nil, err
223
}
224
req.Header.Set("Accept", "application/json")
225
if c.APIKey != "" {
226
req.Header.Set("Authorization",
227
"Bearer "+c.APIKey)
228
}
229
if body != nil {
230
req.Header.Set(
231
"Content-Type", "application/json")
232
}
233
return c.HTTP.Do(req)
234
}
235
236
func (c *Client) getJSON(
237
path string, dst interface{},
238
) error {
239
resp, err := c.do("GET", path, nil)
240
if err != nil {
241
return err
242
}
243
defer resp.Body.Close()
244
if resp.StatusCode >= 400 {
245
b, _ := io.ReadAll(resp.Body)
246
return fmt.Errorf(
247
"HTTP %d: %s", resp.StatusCode, b)
248
}
249
return json.NewDecoder(resp.Body).Decode(dst)
250
}
251
252
func (c *Client) postJSON(
253
path string,
254
payload interface{},
255
dst interface{},
256
) error {
257
var body io.Reader
258
if payload != nil {
259
data, err := json.Marshal(payload)
260
if err != nil {
261
return err
262
}
263
body = strings.NewReader(string(data))
264
}
265
resp, err := c.do("POST", path, body)
266
if err != nil {
267
return err
268
}
269
defer resp.Body.Close()
270
if resp.StatusCode >= 400 {
271
b, _ := io.ReadAll(resp.Body)
272
return fmt.Errorf(
273
"HTTP %d: %s", resp.StatusCode, b)
274
}
275
if dst != nil {
276
return json.NewDecoder(
277
resp.Body).Decode(dst)
278
}
279
return nil
280
}
281
282
func (c *Client) patchJSON(
283
path string,
284
payload interface{},
285
dst interface{},
286
) error {
287
data, err := json.Marshal(payload)
288
if err != nil {
289
return err
290
}
291
body := strings.NewReader(string(data))
292
resp, err := c.do("PATCH", path, body)
293
if err != nil {
294
return err
295
}
296
defer resp.Body.Close()
297
if resp.StatusCode >= 400 {
298
b, _ := io.ReadAll(resp.Body)
299
return fmt.Errorf(
300
"HTTP %d: %s", resp.StatusCode, b)
301
}
302
if dst != nil {
303
return json.NewDecoder(
304
resp.Body).Decode(dst)
305
}
306
return nil
307
}
308
309
// PostJSON performs a POST with a JSON body and
310
// decodes the response. Useful for endpoints not
311
// covered by typed methods.
312
func (c *Client) PostJSON(
313
path string,
314
payload interface{},
315
dst interface{},
316
) error {
317
return c.postJSON(path, payload, dst)
318
}
207
}
319
208
320
// AgentName extracts the agent name from a Klee
209
// AgentName extracts the agent name from a Klee
321
// sub-user username. Given "operator.claude",
210
// sub-user username. Given "operator.claude",
322
// returns "claude". If no dot, returns the input.
211
// returns "claude". If no dot, returns the input.
323
func AgentName(klee_user string) string {
212
func AgentName(klee_user string) string {
324
i := strings.LastIndex(klee_user, ".")
213
i := strings.LastIndex(klee_user, ".")
325
if i < 0 {
214
if i < 0 {
326
return klee_user
215
return klee_user
327
}
216
}
328
return klee_user[i+1:]
217
return klee_user[i+1:]
329
}
218
}