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]
59
okg group list [--json]
60
okg group create NAME [--full-name TEXT] [--owner USER]
60
okg group create NAME [--full-name TEXT] [--owner USER]
61
okg group add-member GROUP USER [USER ...]
61
okg group add-member GROUP USER [USER ...]
62
okg group remove-member GROUP USER [USER ...]
62
okg group remove-member GROUP USER [USER ...]
63
okg group members GROUP [--json]
63
okg group members GROUP [--json]
64
okg group delete NAME
64
okg group delete NAME
65
65
66
okg authz list [--json]
66
okg authz list [--json]
67
okg authz set URI OWNER READER
67
okg authz set URI OWNER READER
68
okg authz delete URI
68
okg authz delete URI
69
69
70
okg chat send TO TEXT
71
okg chat fetch [--to GROUP] [--json]
72
70
okg embed [--model NAME] [--dims N] [--full-path]
73
okg embed [--model NAME] [--dims N] [--full-path]
71
(reads stdin → vectors on stdout)
74
(reads stdin → vectors on stdout)
72
okg one [--model NAME] [--system-file FILE] \
75
okg one [--model NAME] [--system-file FILE] \
73
[--prompt-file FILE] [--attach FILE] \
76
[--prompt-file FILE] [--attach FILE] \
74
[--format text|json|jsonindent] [--fast-fail]
77
[--format text|json|jsonindent] [--fast-fail]
75
(reads stdin as a JSON MessagesRequest)
78
(reads stdin as a JSON MessagesRequest)
76
```
79
```
77
80
78
### Coming next
81
### Coming next
79
82
80
- `okg exemplary` — few-shot batch runner (from
83
- `okg exemplary` — few-shot batch runner (from
81
`klex-git/exemplary`).
84
`klex-git/exemplary`).
82
85
83
Once that lands, the standalone `klex-git` binaries are
86
Once that lands, the standalone `klex-git` binaries are
84
deprecated.
87
deprecated.
85
88
86
### Flags
89
### Flags
87
90
88
- `--repo REPO` overrides auto-detected repo name
91
- `--repo REPO` overrides auto-detected repo name
89
(normally parsed from `git remote get-url origin`)
92
(normally parsed from `git remote get-url origin`)
90
- `--json` outputs raw JSON for any command
93
- `--json` outputs raw JSON for any command
91
- `OKG_REPO` env var also overrides repo detection
94
- `OKG_REPO` env var also overrides repo detection
92
95
93
## Repo Detection
96
## Repo Detection
94
97
95
Like `gh`, okg detects the repo from the current directory's
98
Like `gh`, okg detects the repo from the current directory's
96
git remote:
99
git remote:
97
100
98
```
101
```
99
git remote get-url origin
102
git remote get-url origin
100
→ https://code.oscarkilo.com/widget.git
103
→ https://code.oscarkilo.com/widget.git
101
→ repo = "widget"
104
→ repo = "widget"
102
```
105
```
103
106
104
## Dependencies
107
## Dependencies
105
108
106
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
109
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
107
to be folded in once all `klex-git` binaries have moved here).
110
to be folded in once all `klex-git` binaries have moved here).
108
- Otherwise Go standard library only.
111
- Otherwise Go standard library only.
1
package main
2
3
import "encoding/json"
4
import "flag"
5
import "fmt"
6
import "os"
7
8
import "oscarkilo.com/okg/chat"
9
10
// ChatDefaultUrl is where //chat lives in production. Override
11
// via the CHAT_URL env var for dev/tests (typically
12
// http://localhost:9100, matching the //chat binary's default
13
// port).
14
//
15
// FUTURE: this URL is aspirational until //chat is deployed.
16
// Today, agents need to set CHAT_URL explicitly.
17
const ChatDefaultUrl = "https://oscarkilo.com"
18
19
func runChat(args []string) error {
20
if len(args) == 0 {
21
return fmt.Errorf(
22
"usage: okg chat SUBCOMMAND ... " +
23
"(try `okg --help`)")
24
}
25
switch args[0] {
26
case "send":
27
return runChatSend(args[1:])
28
case "fetch":
29
return runChatFetch(args[1:])
30
default:
31
return fmt.Errorf(
32
"unknown chat subcommand: %s", args[0])
33
}
34
}
35
36
func runChatSend(args []string) error {
37
fs := flag.NewFlagSet("chat send", flag.ContinueOnError)
38
if err := fs.Parse(args); err != nil {
39
return err
40
}
41
positional := fs.Args()
42
if len(positional) != 2 {
43
return fmt.Errorf("usage: okg chat send TO TEXT")
44
}
45
to := positional[0]
46
text := positional[1]
47
48
c, err := newChatClient()
49
if err != nil {
50
return err
51
}
52
msg, err := c.Send(chat.SendRequest{To: to, Text: text})
53
if err != nil {
54
return err
55
}
56
fmt.Printf(
57
"Sent (from=%s, to=%s, at=%s)\n",
58
msg.From, msg.To,
59
msg.CreatedAt.Format("2006-01-02 15:04:05"))
60
return nil
61
}
62
63
func runChatFetch(args []string) error {
64
fs := flag.NewFlagSet("chat fetch", flag.ContinueOnError)
65
to := fs.String("to", "",
66
"filter by destination group (default: all visible)")
67
asJSON := fs.Bool("json", false, "output raw JSON")
68
if err := fs.Parse(args); err != nil {
69
return err
70
}
71
72
c, err := newChatClient()
73
if err != nil {
74
return err
75
}
76
msgs, err := c.Search(chat.SearchRequest{To: *to})
77
if err != nil {
78
return err
79
}
80
81
if *asJSON {
82
buf, err := json.MarshalIndent(msgs, "", " ")
83
if err != nil {
84
return err
85
}
86
fmt.Println(string(buf))
87
return nil
88
}
89
90
for _, m := range msgs {
91
fmt.Printf(
92
"[%s] %s → %s: %s\n",
93
m.CreatedAt.Format("2006-01-02 15:04:05"),
94
m.From, m.To, m.Text)
95
}
96
return nil
97
}
98
99
// newChatClient builds a //chat client from saved config +
100
// the CHAT_URL env var.
101
func newChatClient() (*chat.HTTPClient, error) {
102
cfg, err := loadConfig()
103
if err != nil {
104
return nil, err
105
}
106
if cfg.ApiKey == "" {
107
return nil, fmt.Errorf(
108
"no API key — run `okg auth login --key sk-...`")
109
}
110
url := ChatDefaultUrl
111
if v := os.Getenv("CHAT_URL"); v != "" {
112
url = v
113
}
114
return chat.NewHTTPClient(url, cfg.ApiKey), nil
115
}
1
// Package chat is the public HTTP client for OscarKilo's //chat
2
// service.
3
//
4
// Wire types mirror //clients/chat's domain types (Message,
5
// Filter) and //chat/server's request/response wrappers
6
// (SendRequest, SearchRequest, SearchResponse). The
7
// duplication is deliberate — okg is world-public, //clients
8
// is intra-cluster — and will get the same dot-import
9
// treatment as //okg/who when both packages stabilize.
10
//
11
// The HTTP struct is named HTTPClient (not Client) so a future
12
// `import . "oscarkilo.com/okg/chat"` inside //clients/chat
13
// doesn't collide with that package's existing Client type.
14
package chat
15
16
import "time"
17
18
import "oscarkilo.com/okg/internal/rest"
19
20
// HTTPClient talks to a //chat server over HTTPS. Embeds the
21
// shared rest helpers; chat-specific methods live below.
22
type HTTPClient struct {
23
*rest.Client
24
}
25
26
func NewHTTPClient(host, apiKey string) *HTTPClient {
27
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
28
}
29
30
// Message mirrors //clients/chat.Message. No JSON tags — the
31
// server marshals with default Go field names (PascalCase).
32
// FUTURE: snake_case once //clients/chat gets JSON tags.
33
type Message struct {
34
From string
35
To string
36
Text string
37
CreatedAt time.Time
38
}
39
40
// SendRequest is the body for POST /chat/send.
41
type SendRequest struct {
42
To string `json:"to"`
43
Text string `json:"text"`
44
}
45
46
// Send posts a message to a //chat group. The caller must have
47
// post rights on chat://<to>#post.
48
func (c *HTTPClient) Send(req SendRequest) (*Message, error) {
49
var msg Message
50
if err := c.PostJSON("/chat/send", req, &msg); err != nil {
51
return nil, err
52
}
53
return &msg, nil
54
}
55
56
// SearchRequest is the body for POST /chat/search.
57
type SearchRequest struct {
58
To string `json:"to,omitempty"`
59
}
60
61
// SearchResponse mirrors //chat/server.SearchResponse.
62
type SearchResponse struct {
63
Messages []*Message `json:"messages"`
64
}
65
66
// Search returns messages matching the filter. The caller must
67
// have read rights on chat://<To>#read (otherwise the result
68
// is empty, not an error — per //chat's policy).
69
func (c *HTTPClient) Search(
70
req SearchRequest,
71
) ([]*Message, error) {
72
var res SearchResponse
73
if err := c.PostJSON(
74
"/chat/search", req, &res); err != nil {
75
return nil, err
76
}
77
return res.Messages, nil
78
}
1
package chat
2
3
import "encoding/json"
4
import "net/http"
5
import "net/http/httptest"
6
import "testing"
7
import "time"
8
9
func TestSend(t *testing.T) {
10
var seen SendRequest
11
srv := httptest.NewServer(http.HandlerFunc(
12
func(w http.ResponseWriter, r *http.Request) {
13
if r.URL.Path != "/chat/send" {
14
t.Errorf(
15
"path: got %q, want /chat/send", r.URL.Path)
16
}
17
json.NewDecoder(r.Body).Decode(&seen)
18
json.NewEncoder(w).Encode(Message{
19
From: "alice",
20
To: seen.To,
21
Text: seen.Text,
22
CreatedAt: time.Now(),
23
})
24
}))
25
defer srv.Close()
26
27
c := NewHTTPClient(srv.URL, "test-key")
28
msg, err := c.Send(SendRequest{
29
To: "team", Text: "hello",
30
})
31
if err != nil {
32
t.Fatal(err)
33
}
34
if msg.To != "team" || msg.Text != "hello" {
35
t.Errorf("msg: %+v", msg)
36
}
37
if seen.To != "team" {
38
t.Errorf("seen.To: got %q", seen.To)
39
}
40
}
41
42
func TestSearch(t *testing.T) {
43
srv := httptest.NewServer(http.HandlerFunc(
44
func(w http.ResponseWriter, r *http.Request) {
45
if r.URL.Path != "/chat/search" {
46
t.Errorf(
47
"path: got %q, want /chat/search", r.URL.Path)
48
}
49
json.NewEncoder(w).Encode(SearchResponse{
50
Messages: []*Message{
51
{From: "alice", To: "team", Text: "hello"},
52
{From: "bob", To: "team", Text: "world"},
53
},
54
})
55
}))
56
defer srv.Close()
57
58
c := NewHTTPClient(srv.URL, "test-key")
59
msgs, err := c.Search(SearchRequest{To: "team"})
60
if err != nil {
61
t.Fatal(err)
62
}
63
if len(msgs) != 2 {
64
t.Fatalf("len: got %d, want 2", len(msgs))
65
}
66
if msgs[0].Text != "hello" {
67
t.Errorf("msgs[0].Text: got %q", msgs[0].Text)
68
}
69
}
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":
25
case "group":
26
err = runGroup(args[1:])
26
err = runGroup(args[1:])
27
case "authz":
27
case "authz":
28
err = runAuthz(args[1:])
28
err = runAuthz(args[1:])
29
case "chat":
30
err = runChat(args[1:])
29
case "help", "--help", "-h":
31
case "help", "--help", "-h":
30
printUsage()
32
printUsage()
31
return
33
return
32
default:
34
default:
33
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
35
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
34
printUsage()
36
printUsage()
35
os.Exit(1)
37
os.Exit(1)
36
}
38
}
37
39
38
if err != nil {
40
if err != nil {
39
fmt.Fprintf(os.Stderr, "error: %v\n", err)
41
fmt.Fprintf(os.Stderr, "error: %v\n", err)
40
os.Exit(1)
42
os.Exit(1)
41
}
43
}
42
}
44
}
43
45
44
func printUsage() {
46
func printUsage() {
45
fmt.Fprintf(os.Stderr, `NAME
47
fmt.Fprintf(os.Stderr, `NAME
46
okg — Oscar Kilo Goodness
48
okg — Oscar Kilo Goodness
47
49
48
SETUP
50
SETUP
49
okg auth login
51
okg auth login
50
--key KEY API key (also accepted via stdin)
52
--key KEY API key (also accepted via stdin)
51
--host HOST klee host (default: production)
53
--host HOST klee host (default: production)
52
54
53
GIT REPOS
55
GIT REPOS
54
okg repo list
56
okg repo list
55
--json output raw JSON
57
--json output raw JSON
56
58
57
okg repo create NAME
59
okg repo create NAME
58
--reader USER grant read to USER (default: anyone)
60
--reader USER grant read to USER (default: anyone)
59
61
60
GROUPS
62
GROUPS
61
okg group list
63
okg group list
62
--json output raw JSON
64
--json output raw JSON
63
65
64
okg group create NAME
66
okg group create NAME
65
--full-name TEXT display name (default: NAME)
67
--full-name TEXT display name (default: NAME)
66
--owner USER owner username (default: caller)
68
--owner USER owner username (default: caller)
67
69
68
okg group add-member GROUP USER [USER ...]
70
okg group add-member GROUP USER [USER ...]
69
71
70
okg group remove-member GROUP USER [USER ...]
72
okg group remove-member GROUP USER [USER ...]
71
73
72
okg group members GROUP
74
okg group members GROUP
73
--json full DAG (Up/Down/Usernames) as raw JSON
75
--json full DAG (Up/Down/Usernames) as raw JSON
74
76
75
okg group delete NAME
77
okg group delete NAME
76
78
77
AUTHZ
79
AUTHZ
78
okg authz list
80
okg authz list
79
--json output raw JSON
81
--json output raw JSON
80
82
81
okg authz set URI OWNER READER
83
okg authz set URI OWNER READER
82
84
83
okg authz delete URI
85
okg authz delete URI
84
86
87
CHAT
88
okg chat send TO TEXT
89
90
okg chat fetch
91
--to GROUP filter by destination group
92
--json output raw JSON
93
85
PULL REQUESTS
94
PULL REQUESTS
86
okg pr list
95
okg pr list
87
--state STATE open or closed (default: open)
96
--state STATE open or closed (default: open)
88
--json output raw JSON
97
--json output raw JSON
89
98
90
okg pr create
99
okg pr create
91
--head BRANCH source branch
100
--head BRANCH source branch
92
--base BRANCH target branch (default: master)
101
--base BRANCH target branch (default: master)
93
--title TITLE PR title
102
--title TITLE PR title
94
--body BODY PR body (optional)
103
--body BODY PR body (optional)
95
--json output raw JSON
104
--json output raw JSON
96
105
97
okg pr view NUMBER
106
okg pr view NUMBER
98
--json output raw JSON
107
--json output raw JSON
99
108
100
okg pr diff NUMBER
109
okg pr diff NUMBER
101
110
102
okg pr comment NUMBER
111
okg pr comment NUMBER
103
--body BODY comment body
112
--body BODY comment body
104
--approve also approve the PR
113
--approve also approve the PR
105
--request-changes also request changes
114
--request-changes also request changes
106
115
107
okg pr merge NUMBER
116
okg pr merge NUMBER
108
--json output raw JSON
117
--json output raw JSON
109
118
110
okg pr close NUMBER
119
okg pr close NUMBER
111
--json output raw JSON
120
--json output raw JSON
112
121
113
okg pr reopen NUMBER
122
okg pr reopen NUMBER
114
--json output raw JSON
123
--json output raw JSON
115
124
116
ARTIFICIAL INTELLIGENCE
125
ARTIFICIAL INTELLIGENCE
117
okg embed
126
okg embed
118
--model NAME embedding model
127
--model NAME embedding model
119
(default: openai:text-embedding-3-small)
128
(default: openai:text-embedding-3-small)
120
--dims N number of dimensions (default: 1536)
129
--dims N number of dimensions (default: 1536)
121
--full-path one vector per prefix of input
130
--full-path one vector per prefix of input
122
(reads stdin; writes vectors to stdout, one per line)
131
(reads stdin; writes vectors to stdout, one per line)
123
132
124
okg one
133
okg one
125
--model NAME override .Model in the request
134
--model NAME override .Model in the request
126
--system-file FILE override .System with contents of FILE
135
--system-file FILE override .System with contents of FILE
127
--prompt-file FILE append FILE as a user prompt
136
--prompt-file FILE append FILE as a user prompt
128
--attach FILE attach an image or PDF to the prompt
137
--attach FILE attach an image or PDF to the prompt
129
--format FORMAT text | json | jsonindent (default: text)
138
--format FORMAT text | json | jsonindent (default: text)
130
--fast-fail preflight attachment MIME (default: on)
139
--fast-fail preflight attachment MIME (default: on)
131
(reads stdin as a JSON MessagesRequest; flags override
140
(reads stdin as a JSON MessagesRequest; flags override
132
its fields)
141
its fields)
133
142
134
GLOBAL FLAGS
143
GLOBAL FLAGS
135
--repo REPO override auto-detected repo name
144
--repo REPO override auto-detected repo name
136
--json output raw JSON (where applicable)
145
--json output raw JSON (where applicable)
137
146
138
EXAMPLES
147
EXAMPLES
139
Setup
148
Setup
140
okg auth login --key sk-...
149
okg auth login --key sk-...
141
cat ~/.klex.key | okg auth login
150
cat ~/.klex.key | okg auth login
142
151
143
Git repos
152
Git repos
144
okg repo list
153
okg repo list
145
okg repo create my-new-repo
154
okg repo create my-new-repo
146
155
147
Groups
156
Groups
148
okg group list
157
okg group list
149
okg group create chat-bots
158
okg group create chat-bots
150
okg group add-member chat-bots claude openclaw
159
okg group add-member chat-bots claude openclaw
151
okg group remove-member chat-bots openclaw
160
okg group remove-member chat-bots openclaw
152
okg group members chat-bots
161
okg group members chat-bots
153
okg group delete chat-bots
162
okg group delete chat-bots
154
163
155
Authz
164
Authz
156
okg authz set chat://chat-bots#post chat-bots chat-bots
165
okg authz set chat://chat-bots#post chat-bots chat-bots
157
okg authz list
166
okg authz list
158
167
168
Chat
169
okg chat send chat-bots 'hello team'
170
okg chat fetch --to chat-bots
171
159
Pull requests
172
Pull requests
160
okg pr list --state open
173
okg pr list --state open
161
okg pr view 42
174
okg pr view 42
162
okg pr comment 42 --body 'LGTM' --approve
175
okg pr comment 42 --body 'LGTM' --approve
163
176
164
Artificial intelligence
177
Artificial intelligence
165
echo 'hello world' | okg embed --dims 384
178
echo 'hello world' | okg embed --dims 384
166
echo Hello? > /tmp/q.txt && \
179
echo Hello? > /tmp/q.txt && \
167
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
180
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
168
`)
181
`)
169
}
182
}