// Package klee provides a Go client for the Klee
// git server API (code.oscarkilo.com).
package klee
import "fmt"
import "strings"
import "time"
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 {
*rest.Client
}
// NewClient creates a Klee client.
func NewClient(host, api_key string) *Client {
return &Client{Client: rest.NewClient(host, api_key)}
}
// PR is a pull request on Klee.
type PR struct {
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
Merged bool `json:"merged"`
Head string `json:"head"`
Base string `json:"base"`
Author string `json:"author"`
Files []string `json:"files"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
MergedBy string `json:"merged_by,omitempty"`
MergedAt time.Time `json:"merged_at,omitempty"`
}
// Comment is a PR comment on Klee.
type Comment struct {
ID int `json:"id"`
Author string `json:"author"`
Body string `json:"body"`
Verdict string `json:"verdict,omitempty"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
Created time.Time `json:"created"`
}
// CreatePRRequest is the body for creating a PR.
type CreatePRRequest struct {
Head string `json:"head"`
Base string `json:"base"`
Title string `json:"title"`
Body string `json:"body"`
}
// AddCommentRequest is the body for adding a
// comment to a PR.
type AddCommentRequest struct {
Body string `json:"body"`
Verdict string `json:"verdict,omitempty"`
}
// CreateRepoRequest is the body for creating a
// repo on Klee.
type CreateRepoRequest struct {
RepoName string `json:"repo_name"`
ReaderUsername string `json:"reader_username"`
}
// RepoInfo describes a repository.
type RepoInfo struct {
Name string `json:"name"`
IsPublic bool `json:"is_public"`
Authz *RepoInfoAuthz `json:"authz"`
}
// RepoInfoAuthz describes repo access.
type RepoInfoAuthz struct {
IsOwner bool `json:"is_owner"`
IsReader bool `json:"is_reader"`
OwnerUsername string `json:"owner_username"`
ReaderUsername string `json:"reader_username"`
}
// LsResponse is the response from /.ls.
type LsResponse struct {
Repos []RepoInfo `json:"repos"`
CanCreateRepos bool `json:"can_create_repos"`
}
// --- PR operations ---
// ListPRs lists PRs for a repo by state.
func (c *Client) ListPRs(
repo, state string,
) ([]PR, error) {
path := fmt.Sprintf(
"/%s/prs?state=%s", repo, state)
var prs []PR
if err := c.GetJSON(path, &prs); err != nil {
return nil, err
}
return prs, nil
}
// GetPR fetches a single PR.
func (c *Client) GetPR(
repo string, number int,
) (*PR, error) {
path := fmt.Sprintf(
"/%s/pr/%d", repo, number)
var pr PR
if err := c.GetJSON(path, &pr); err != nil {
return nil, err
}
return &pr, nil
}
// CreatePR creates a new PR.
func (c *Client) CreatePR(
repo string, req CreatePRRequest,
) (*PR, error) {
path := fmt.Sprintf("/%s/prs", repo)
var pr PR
if err := c.PostJSON(path, req, &pr); err != nil {
return nil, err
}
return &pr, nil
}
// GetComments fetches comments on a PR.
func (c *Client) GetComments(
repo string, number int,
) ([]Comment, error) {
path := fmt.Sprintf(
"/%s/pr/%d/comments", repo, number)
var comments []Comment
if err := c.GetJSON(path, &comments); err != nil {
return nil, err
}
return comments, nil
}
// AddComment adds a comment to a PR.
func (c *Client) AddComment(
repo string, number int, req AddCommentRequest,
) (*Comment, error) {
path := fmt.Sprintf(
"/%s/pr/%d/comments", repo, number)
var comment Comment
if err := c.PostJSON(
path, req, &comment); err != nil {
return nil, err
}
return &comment, nil
}
// MergePR merges a PR.
func (c *Client) MergePR(
repo string, number int,
) (*PR, error) {
path := fmt.Sprintf(
"/%s/pr/%d/merge", repo, number)
var pr PR
if err := c.PostJSON(path, nil, &pr); err != nil {
return nil, err
}
return &pr, nil
}
// SetPRState changes a PR's state (open/closed).
func (c *Client) SetPRState(
repo string, number int, state string,
) (*PR, error) {
path := fmt.Sprintf(
"/%s/pr/%d", repo, number)
payload := map[string]string{"state": state}
var pr PR
if err := c.PatchJSON(
path, payload, &pr); err != nil {
return nil, err
}
return &pr, nil
}
// --- Repo operations ---
// ListRepos lists repos accessible to the caller.
func (c *Client) ListRepos() (
*LsResponse, error,
) {
var res LsResponse
if err := c.GetJSON("/.ls", &res); err != nil {
return nil, err
}
return &res, nil
}
// CreateRepo creates a new repo on Klee.
func (c *Client) CreateRepo(
req CreateRepoRequest,
) error {
return c.PostJSON("/.add-repo", req, nil)
}
// AgentName extracts the agent name from a Klee
// sub-user username. Given "operator.claude",
// returns "claude". If no dot, returns the input.
func AgentName(klee_user string) string {
i := strings.LastIndex(klee_user, ".")
if i < 0 {
return klee_user
}
return klee_user[i+1:]
}