1
# okg — Oscar Kilo Goodness
1
# okg — Oscar Kilo Goodness
2
2
3
A command-line tool that bundles Oscar Kilo's externally usable
3
A command-line tool that bundles Oscar Kilo's externally usable
4
services into one binary. Today: git ops against
4
services into one binary. Today: git ops against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
8
8
9
Designed for both humans and AI agents (Claude, OpenClaw) to
9
Designed for both humans and AI agents (Claude, OpenClaw) to
10
use directly from the command line.
10
use directly from the command line.
11
11
12
## Install
12
## Install
13
13
14
```bash
14
```bash
15
go install oscarkilo.com/okg@latest
15
go install oscarkilo.com/okg@latest
16
```
16
```
17
17
18
Or build from source:
18
Or build from source:
19
19
20
```bash
20
```bash
21
git clone https://code.oscarkilo.com/okg
21
git clone https://code.oscarkilo.com/okg
22
cd okg && go build .
22
cd okg && go build .
23
```
23
```
24
24
25
## Setup
25
## Setup
26
26
27
By default okg talks to the production klee host
27
By default okg talks to the production klee host
28
(`https://code.oscarkilo.com`); the only setup you need is your
28
(`https://code.oscarkilo.com`); the only setup you need is your
29
API key. `okg auth login` saves it to
29
API key. `okg auth login` saves it to
30
`~/.config/okg/config.json`.
30
`~/.config/okg/config.json`.
31
31
32
```bash
32
```bash
33
# Non-interactive (for agents and scripts).
33
# Non-interactive (for agents and scripts).
34
okg auth login --key sk-...
34
okg auth login --key sk-...
35
cat ~/.klex.key | okg auth login # equivalent, ps-safe
35
cat ~/.klex.key | okg auth login # equivalent, ps-safe
36
36
37
# Interactive (prompts for host then key).
37
# Interactive (prompts for host then key).
38
okg auth login
38
okg auth login
39
```
39
```
40
40
41
Override the host for dev/local work with `--host` on `auth login`.
41
Override the host for dev/local work with `--host` on `auth login`.
42
42
43
## Commands
43
## Commands
44
44
45
```
45
```
46
okg repo list
46
okg repo list
47
47
48
okg pr list [--state open|closed]
48
okg pr list [--state open|closed]
49
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
49
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
50
okg pr view NUMBER
50
okg pr view NUMBER
51
okg pr diff NUMBER
51
okg pr diff NUMBER
52
okg pr comment NUMBER --body BODY [--approve | --request-changes]
52
okg pr comment NUMBER --body BODY [--approve | --request-changes]
53
okg pr merge NUMBER
53
okg pr merge NUMBER
54
okg pr close NUMBER
54
okg pr close NUMBER
55
okg pr reopen NUMBER
55
okg pr reopen NUMBER
56
56
57
okg auth login [--key KEY] [--host HOST]
57
okg auth login [--key KEY] [--host HOST]
58
58
59
okg group list [--json]
60
59
okg embed [--model NAME] [--dims N] [--full-path]
61
okg embed [--model NAME] [--dims N] [--full-path]
60
(reads stdin → vectors on stdout)
62
(reads stdin → vectors on stdout)
61
okg one [--model NAME] [--system-file FILE] \
63
okg one [--model NAME] [--system-file FILE] \
62
[--prompt-file FILE] [--attach FILE] \
64
[--prompt-file FILE] [--attach FILE] \
63
[--format text|json|jsonindent] [--fast-fail]
65
[--format text|json|jsonindent] [--fast-fail]
64
(reads stdin as a JSON MessagesRequest)
66
(reads stdin as a JSON MessagesRequest)
65
```
67
```
66
68
67
### Coming next
69
### Coming next
68
70
69
- `okg exemplary` — few-shot batch runner (from
71
- `okg exemplary` — few-shot batch runner (from
70
`klex-git/exemplary`).
72
`klex-git/exemplary`).
71
73
72
Once that lands, the standalone `klex-git` binaries are
74
Once that lands, the standalone `klex-git` binaries are
73
deprecated.
75
deprecated.
74
76
75
### Flags
77
### Flags
76
78
77
- `--repo REPO` overrides auto-detected repo name
79
- `--repo REPO` overrides auto-detected repo name
78
(normally parsed from `git remote get-url origin`)
80
(normally parsed from `git remote get-url origin`)
79
- `--json` outputs raw JSON for any command
81
- `--json` outputs raw JSON for any command
80
- `OKG_REPO` env var also overrides repo detection
82
- `OKG_REPO` env var also overrides repo detection
81
83
82
## Repo Detection
84
## Repo Detection
83
85
84
Like `gh`, okg detects the repo from the current directory's
86
Like `gh`, okg detects the repo from the current directory's
85
git remote:
87
git remote:
86
88
87
```
89
```
88
git remote get-url origin
90
git remote get-url origin
89
→ https://code.oscarkilo.com/widget.git
91
→ https://code.oscarkilo.com/widget.git
90
→ repo = "widget"
92
→ repo = "widget"
91
```
93
```
92
94
93
## Dependencies
95
## Dependencies
94
96
95
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
97
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
96
to be folded in once all `klex-git` binaries have moved here).
98
to be folded in once all `klex-git` binaries have moved here).
97
- Otherwise Go standard library only.
99
- Otherwise Go standard library only.
1
package main
2
3
import "encoding/json"
4
import "flag"
5
import "fmt"
6
import "os"
7
import "text/tabwriter"
8
9
import "oscarkilo.com/okg/who"
10
11
func runGroup(args []string) error {
12
if len(args) == 0 {
13
return fmt.Errorf(
14
"usage: okg group SUBCOMMAND ... (try `okg --help`)")
15
}
16
switch args[0] {
17
case "list":
18
return runGroupList(args[1:])
19
default:
20
return fmt.Errorf(
21
"unknown group subcommand: %s", args[0])
22
}
23
}
24
25
func runGroupList(args []string) error {
26
fs := flag.NewFlagSet("group list", flag.ContinueOnError)
27
asJSON := fs.Bool("json", false, "output raw JSON")
28
if err := fs.Parse(args); err != nil {
29
return err
30
}
31
32
cfg, err := loadConfig()
33
if err != nil {
34
return err
35
}
36
if cfg.ApiKey == "" {
37
return fmt.Errorf(
38
"no API key — run `okg auth login --key sk-...`")
39
}
40
41
c := who.NewHTTPClient(cfg.Host, cfg.ApiKey)
42
groups, err := c.ListGroups()
43
if err != nil {
44
return err
45
}
46
47
if *asJSON {
48
buf, err := json.MarshalIndent(groups, "", " ")
49
if err != nil {
50
return err
51
}
52
fmt.Println(string(buf))
53
return nil
54
}
55
56
tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
57
fmt.Fprintln(tw, "USERNAME\tFULL NAME\tOWNER")
58
for _, g := range groups {
59
fmt.Fprintf(tw, "%s\t%s\t%s\n",
60
g.Username, g.Name, g.OwnerUsername)
61
}
62
return tw.Flush()
63
}
1
package main
1
package main
2
2
3
import "fmt"
3
import "fmt"
4
import "os"
4
import "os"
5
5
6
func main() {
6
func main() {
7
args := os.Args[1:]
7
args := os.Args[1:]
8
if len(args) == 0 {
8
if len(args) == 0 {
9
printUsage()
9
printUsage()
10
os.Exit(1)
10
os.Exit(1)
11
}
11
}
12
12
13
var err error
13
var err error
14
switch args[0] {
14
switch args[0] {
15
case "pr":
15
case "pr":
16
err = runPR(args[1:])
16
err = runPR(args[1:])
17
case "repo":
17
case "repo":
18
err = runRepo(args[1:])
18
err = runRepo(args[1:])
19
case "auth":
19
case "auth":
20
err = runAuth(args[1:])
20
err = runAuth(args[1:])
21
case "embed":
21
case "embed":
22
err = runEmbed(args[1:])
22
err = runEmbed(args[1:])
23
case "one":
23
case "one":
24
err = runOne(args[1:])
24
err = runOne(args[1:])
25
case "group":
26
err = runGroup(args[1:])
25
case "help", "--help", "-h":
27
case "help", "--help", "-h":
26
printUsage()
28
printUsage()
27
return
29
return
28
default:
30
default:
29
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
31
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
30
printUsage()
32
printUsage()
31
os.Exit(1)
33
os.Exit(1)
32
}
34
}
33
35
34
if err != nil {
36
if err != nil {
35
fmt.Fprintf(os.Stderr, "error: %v\n", err)
37
fmt.Fprintf(os.Stderr, "error: %v\n", err)
36
os.Exit(1)
38
os.Exit(1)
37
}
39
}
38
}
40
}
39
41
40
func printUsage() {
42
func printUsage() {
41
fmt.Fprintf(os.Stderr, `NAME
43
fmt.Fprintf(os.Stderr, `NAME
42
okg — Oscar Kilo Goodness
44
okg — Oscar Kilo Goodness
43
45
44
SETUP
46
SETUP
45
okg auth login
47
okg auth login
46
--key KEY API key (also accepted via stdin)
48
--key KEY API key (also accepted via stdin)
47
--host HOST klee host (default: production)
49
--host HOST klee host (default: production)
48
50
49
GIT REPOS
51
GIT REPOS
50
okg repo list
52
okg repo list
51
--json output raw JSON
53
--json output raw JSON
52
54
53
okg repo create NAME
55
okg repo create NAME
54
--reader USER grant read to USER (default: anyone)
56
--reader USER grant read to USER (default: anyone)
55
57
58
GROUPS
59
okg group list
60
--json output raw JSON
61
56
PULL REQUESTS
62
PULL REQUESTS
57
okg pr list
63
okg pr list
58
--state STATE open or closed (default: open)
64
--state STATE open or closed (default: open)
59
--json output raw JSON
65
--json output raw JSON
60
66
61
okg pr create
67
okg pr create
62
--head BRANCH source branch
68
--head BRANCH source branch
63
--base BRANCH target branch (default: master)
69
--base BRANCH target branch (default: master)
64
--title TITLE PR title
70
--title TITLE PR title
65
--body BODY PR body (optional)
71
--body BODY PR body (optional)
66
--json output raw JSON
72
--json output raw JSON
67
73
68
okg pr view NUMBER
74
okg pr view NUMBER
69
--json output raw JSON
75
--json output raw JSON
70
76
71
okg pr diff NUMBER
77
okg pr diff NUMBER
72
78
73
okg pr comment NUMBER
79
okg pr comment NUMBER
74
--body BODY comment body
80
--body BODY comment body
75
--approve also approve the PR
81
--approve also approve the PR
76
--request-changes also request changes
82
--request-changes also request changes
77
83
78
okg pr merge NUMBER
84
okg pr merge NUMBER
79
--json output raw JSON
85
--json output raw JSON
80
86
81
okg pr close NUMBER
87
okg pr close NUMBER
82
--json output raw JSON
88
--json output raw JSON
83
89
84
okg pr reopen NUMBER
90
okg pr reopen NUMBER
85
--json output raw JSON
91
--json output raw JSON
86
92
87
ARTIFICIAL INTELLIGENCE
93
ARTIFICIAL INTELLIGENCE
88
okg embed
94
okg embed
89
--model NAME embedding model
95
--model NAME embedding model
90
(default: openai:text-embedding-3-small)
96
(default: openai:text-embedding-3-small)
91
--dims N number of dimensions (default: 1536)
97
--dims N number of dimensions (default: 1536)
92
--full-path one vector per prefix of input
98
--full-path one vector per prefix of input
93
(reads stdin; writes vectors to stdout, one per line)
99
(reads stdin; writes vectors to stdout, one per line)
94
100
95
okg one
101
okg one
96
--model NAME override .Model in the request
102
--model NAME override .Model in the request
97
--system-file FILE override .System with contents of FILE
103
--system-file FILE override .System with contents of FILE
98
--prompt-file FILE append FILE as a user prompt
104
--prompt-file FILE append FILE as a user prompt
99
--attach FILE attach an image or PDF to the prompt
105
--attach FILE attach an image or PDF to the prompt
100
--format FORMAT text | json | jsonindent (default: text)
106
--format FORMAT text | json | jsonindent (default: text)
101
--fast-fail preflight attachment MIME (default: on)
107
--fast-fail preflight attachment MIME (default: on)
102
(reads stdin as a JSON MessagesRequest; flags override
108
(reads stdin as a JSON MessagesRequest; flags override
103
its fields)
109
its fields)
104
110
105
GLOBAL FLAGS
111
GLOBAL FLAGS
106
--repo REPO override auto-detected repo name
112
--repo REPO override auto-detected repo name
107
--json output raw JSON (where applicable)
113
--json output raw JSON (where applicable)
108
114
109
EXAMPLES
115
EXAMPLES
110
Setup
116
Setup
111
okg auth login --key sk-...
117
okg auth login --key sk-...
112
cat ~/.klex.key | okg auth login
118
cat ~/.klex.key | okg auth login
113
119
114
Git repos
120
Git repos
115
okg repo list
121
okg repo list
116
okg repo create my-new-repo
122
okg repo create my-new-repo
117
123
124
Groups
125
okg group list
126
118
Pull requests
127
Pull requests
119
okg pr list --state open
128
okg pr list --state open
120
okg pr view 42
129
okg pr view 42
121
okg pr comment 42 --body 'LGTM' --approve
130
okg pr comment 42 --body 'LGTM' --approve
122
131
123
Artificial intelligence
132
Artificial intelligence
124
echo 'hello world' | okg embed --dims 384
133
echo 'hello world' | okg embed --dims 384
125
echo Hello? > /tmp/q.txt && \
134
echo Hello? > /tmp/q.txt && \
126
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
135
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
127
`)
136
`)
128
}
137
}
1
// Package who is the public HTTP client for OscarKilo's //who
2
// service. It is the world-public side of //who, used by
3
// `okg` and any other tool that talks to oscarkilo.com over
4
// the network.
5
//
6
// //clients/who (intra-cluster, has the in-process MockClient,
7
// the /internal/* calls, the dev/prod factory) will eventually
8
// import this package via `import . "oscarkilo.com/okg/who"`
9
// so the two share types and request paths. To make that dot
10
// import work, the struct here is named HTTPClient — leaving
11
// the unqualified `Client` name free for //clients/who's
12
// existing interface.
13
package who
14
15
import "oscarkilo.com/okg/internal/rest"
16
17
// HTTPClient talks to a //who server over HTTPS. Embeds the
18
// shared rest helpers; who-specific methods live below.
19
type HTTPClient struct {
20
*rest.Client
21
}
22
23
func NewHTTPClient(host, apiKey string) *HTTPClient {
24
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
25
}
26
27
// Group describes a //who group (a named entity with members).
28
// Mirrors the listedGroup shape in //who/server/groups_list.go.
29
type Group struct {
30
Owid string `json:"owid"`
31
Username string `json:"username"`
32
Name string `json:"name"`
33
OwnerOwid string `json:"owner_owid"`
34
OwnerUsername string `json:"owner_username"`
35
}
36
37
type listGroupsResponse struct {
38
Groups []Group `json:"groups"`
39
}
40
41
// ListGroups returns the groups visible to the caller.
42
func (c *HTTPClient) ListGroups() ([]Group, error) {
43
var res listGroupsResponse
44
if err := c.GetJSON("/groups/list", &res); err != nil {
45
return nil, err
46
}
47
return res.Groups, nil
48
}
1
package who
2
3
import "encoding/json"
4
import "net/http"
5
import "net/http/httptest"
6
import "testing"
7
8
func TestListGroups(t *testing.T) {
9
srv := httptest.NewServer(http.HandlerFunc(
10
func(w http.ResponseWriter, r *http.Request) {
11
if r.URL.Path != "/groups/list" {
12
t.Errorf("path: got %q, want /groups/list",
13
r.URL.Path)
14
}
15
if r.Header.Get("Authorization") !=
16
"Bearer test-key" {
17
t.Errorf("Authorization: got %q",
18
r.Header.Get("Authorization"))
19
}
20
w.Header().Set(
21
"Content-Type", "application/json")
22
json.NewEncoder(w).Encode(listGroupsResponse{
23
Groups: []Group{
24
{
25
Username: "team",
26
Name: "Team",
27
OwnerUsername: "alice",
28
},
29
{
30
Username: "admins",
31
Name: "Admins",
32
OwnerUsername: "alice",
33
},
34
},
35
})
36
}))
37
defer srv.Close()
38
39
c := NewHTTPClient(srv.URL, "test-key")
40
groups, err := c.ListGroups()
41
if err != nil {
42
t.Fatal(err)
43
}
44
if len(groups) != 2 {
45
t.Fatalf("len: got %d, want 2", len(groups))
46
}
47
if groups[0].Username != "team" {
48
t.Errorf(
49
"groups[0].Username: got %q, want team",
50
groups[0].Username)
51
}
52
if groups[1].OwnerUsername != "alice" {
53
t.Errorf(
54
"groups[1].OwnerUsername: got %q, want alice",
55
groups[1].OwnerUsername)
56
}
57
}
58
59
func TestListGroupsHTTPError(t *testing.T) {
60
srv := httptest.NewServer(http.HandlerFunc(
61
func(w http.ResponseWriter, r *http.Request) {
62
w.WriteHeader(401)
63
w.Write([]byte("not authenticated"))
64
}))
65
defer srv.Close()
66
67
c := NewHTTPClient(srv.URL, "bad-key")
68
_, err := c.ListGroups()
69
if err == nil {
70
t.Fatal("want error on 401, got nil")
71
}
72
}