code.oscarkilo.com/okg

Hash:
76d140db96999f7b03ac9f440260d87ef5db8ec4
Author:
Igor Naverniouk <[email protected]>
Date:
Sat Jun 6 11:54:09 2026 -0400
Message:
okg: add `okg exemplary` — few-shot batch runner Port the last `klex-git` binary into okg. Scans --dir for <case>.<ext> input files, groups paired .out files as few-shot examples, sends the rest to the configured LLM, and writes responses back as <case>.out. Renames: --dry_run → --dry-run (okg convention). Drops: --format flag was dead in the original. The signature shift from globals to a per-call function: scanForCases(dir) → ([]Case, error) copyRequest(req) → (*MessagesRequest, error) runExemplary(args) error No unit test in this commit — the upstream TestScanForCases depends on a 6-file example/ fixture dir (including binary images). Will port the fixtures + test in a follow-up if the extra payload's worth it. With this commit the three klex-git binaries (one, embed, exemplary) are all absorbed into okg. //klex-git is now a client-library-only repo, candidate for further consolidation.
diff --git a/README.md b/README.md
index fc267cd..d2315c1 100644
--- a/README.md
+++ b/README.md
@@ -89,15 +89,13 @@ okg one [--model NAME] [--system-file FILE] \
[--prompt-file FILE] [--attach FILE] \
[--format text|json|jsonindent] [--fast-fail]
(reads stdin as a JSON MessagesRequest)
+okg exemplary [--dir DIR] [--model NAME] [--dry-run] [--debug]
+ (few-shot batch runner; walks <case>.<ext>
+ files in DIR, writes responses to <case>.out)
```

-### Coming next
-
-- `okg exemplary` — few-shot batch runner (from
- `klex-git/exemplary`).
-
-Once that lands, the standalone `klex-git` binaries are
-deprecated.
+With `okg exemplary` in place, the standalone `klex-git`
+binaries (`one`, `embed`, `exemplary`) are deprecated.

### Flags

diff --git a/exemplary.go b/exemplary.go
new file mode 100644
index 0000000..6a8e6bc
--- /dev/null
+++ b/exemplary.go
@@ -0,0 +1,279 @@
+package main
+
+import "encoding/json"
+import "flag"
+import "fmt"
+import "log"
+import "os"
+import "path"
+import "sort"
+import "strings"
+
+import "oscarkilo.com/klex-git/api"
+
+// runExemplary runs few-shot LLM inference on a directory full
+// of "<case>.<ext>" input files. Files ending in .out are
+// treated as examples (paired with their non-.out siblings);
+// the remaining inputs are sent to the LLM and the responses
+// are written back as <case>.out files.
+//
+// Mirrors the (now-deprecated) `klex-git/exemplary` binary.
+func runExemplary(args []string) error {
+ fs := flag.NewFlagSet("exemplary", flag.ContinueOnError)
+ dir := fs.String("dir", ".",
+ "directory to scan for cases and write outputs to")
+ model := fs.String("model", "Gemini 3 Pro",
+ "LLM model name")
+ dryRun := fs.Bool("dry-run", false,
+ "scan + build requests but don't send them")
+ debug := fs.Bool("debug", false,
+ "log each request body to stderr before sending")
+ if err := fs.Parse(args); err != nil {
+ return err
+ }
+
+ cfg, err := loadConfig()
+ if err != nil {
+ return err
+ }
+ if cfg.ApiKey == "" {
+ return fmt.Errorf(
+ "no API key — run `okg auth login --key sk-...`")
+ }
+ client := newKlexClient(cfg)
+
+ // Build a request that starts with the system prompt and
+ // grows by one user message per case (plus an assistant
+ // message for examples). Inputs (cases without .out) get a
+ // snapshot of the request taken just before their user
+ // message would otherwise be added to the running prefix.
+ sysBytes, err := os.ReadFile(
+ path.Join(*dir, "system_prompt.txt"))
+ if err != nil {
+ return fmt.Errorf("read system_prompt.txt: %v", err)
+ }
+ req := api.MessagesRequest{
+ Model: *model,
+ System: string(sysBytes),
+ }
+
+ cases, err := scanForCases(*dir)
+ if err != nil {
+ return err
+ }
+
+ for i, c := range cases {
+ user := api.ChatMessage{Role: "user"}
+ text := "Case name: " + c.Name + "\n\n"
+ for _, suffix := range c.Before {
+ switch suffix {
+ case "txt":
+ b, err := os.ReadFile(
+ path.Join(*dir, c.Name+".txt"))
+ if err != nil {
+ return fmt.Errorf(
+ "read %s.txt: %v", c.Name, err)
+ }
+ text += string(b)
+ case "json":
+ b, err := os.ReadFile(
+ path.Join(*dir, c.Name+".json"))
+ if err != nil {
+ return fmt.Errorf(
+ "read %s.json: %v", c.Name, err)
+ }
+ text += "\n\n```json\n" + string(b) + "\n```\n"
+ case "jpg", "jpeg", "png", "webp":
+ b, err := os.ReadFile(
+ path.Join(*dir, c.Name+"."+suffix))
+ if err != nil {
+ return fmt.Errorf(
+ "read %s.%s: %v", c.Name, suffix, err)
+ }
+ user.Content = append(user.Content,
+ api.NewDocumentBlock(b))
+ default:
+ return fmt.Errorf(
+ "unsupported suffix %s in case %s",
+ suffix, c.Name)
+ }
+ }
+ user.Content = append(user.Content, api.ContentBlock{
+ Type: "text",
+ Text: text,
+ })
+ req.Messages = append(req.Messages, user)
+ if c.After != "" {
+ out, err := os.ReadFile(
+ path.Join(*dir, c.Name+".out"))
+ if err != nil {
+ return fmt.Errorf(
+ "read %s.out: %v", c.Name, err)
+ }
+ req.Messages = append(req.Messages,
+ api.ChatMessage{
+ Role: "assistant",
+ Content: []api.ContentBlock{
+ {Type: "text", Text: string(out)},
+ },
+ })
+ } else {
+ copy, err := copyRequest(req)
+ if err != nil {
+ return err
+ }
+ cases[i].Request = copy
+ req.Messages = req.Messages[:len(req.Messages)-1]
+ }
+ }
+
+ if *dryRun {
+ log.Printf("dry run; not sending requests")
+ return nil
+ }
+
+ // TODO: parallelize.
+ for _, c := range cases {
+ if c.Request == nil {
+ continue
+ }
+ if *debug {
+ log.Printf("Case %s: sending request:", c.Name)
+ enc := json.NewEncoder(os.Stderr)
+ enc.SetIndent("", " ")
+ enc.Encode(c.Request)
+ }
+ res, err := client.Messages(*c.Request)
+ if err != nil {
+ log.Printf(
+ "Case %s: request failed: %v", c.Name, err)
+ continue
+ }
+ if len(res.Content) != 1 {
+ log.Printf(
+ "Case %s: empty response", c.Name)
+ continue
+ }
+ c0 := res.Content[0]
+ if c0.Type != "text" {
+ log.Printf(
+ "Case %s: Content[0].Type = %s",
+ c.Name, c0.Type)
+ continue
+ }
+ out := path.Join(*dir, c.Name+".out")
+ err = os.WriteFile(
+ out, append([]byte(c0.Text), '\n'), 0644)
+ if err != nil {
+ log.Printf(
+ "Case %s: failed to write %s.out: %v",
+ c.Name, c.Name, err)
+ continue
+ }
+ log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
+ }
+ return nil
+}
+
+// Case is one input or one input-plus-example file group in
+// the directory exemplary scans.
+type Case struct {
+ Name string
+ Before []string // file extensions present: txt, json, jpg, ...
+ After string // "out" if a paired .out file exists
+ Request *api.MessagesRequest
+}
+
+// scanForCases walks dir and groups files into Cases. Files
+// named system_prompt.txt are skipped (consumed separately as
+// the request's System field). Cases with an .out file sort
+// before cases without, so the example-prefix building loop
+// can stop at the first non-example.
+func scanForCases(dir string) ([]Case, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, fmt.Errorf("read dir %s: %v", dir, err)
+ }
+
+ before := make(map[string][]string)
+ after := make(map[string]string)
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ if entry.Name() == "system_prompt.txt" {
+ continue
+ }
+ chunks := strings.Split(entry.Name(), ".")
+ if len(chunks) < 2 {
+ continue
+ }
+ name := strings.Join(
+ chunks[0:len(chunks)-1], ".")
+ suffix := chunks[len(chunks)-1]
+ switch suffix {
+ case "txt", "json", "jpg", "jpeg", "png", "webp":
+ before[name] = append(before[name], suffix)
+ case "out":
+ after[name] = suffix
+ }
+ }
+
+ var cases []Case
+ for name, b := range before {
+ sort.Slice(b, func(i, j int) bool {
+ if b[i] == "txt" && b[j] != "txt" {
+ return true
+ }
+ if b[i] != "txt" && b[j] == "txt" {
+ return false
+ }
+ return b[i] < b[j]
+ })
+ cases = append(cases, Case{
+ Name: name,
+ Before: b,
+ After: after[name],
+ })
+ }
+
+ sort.Slice(cases, func(i, j int) bool {
+ a, b := cases[i], cases[j]
+ if a.After != "" && b.After == "" {
+ return true
+ }
+ if a.After == "" && b.After != "" {
+ return false
+ }
+ return a.Name < b.Name
+ })
+
+ numExamples := 0
+ for ; numExamples < len(cases); numExamples++ {
+ if cases[numExamples].After == "" {
+ break
+ }
+ }
+ log.Printf(
+ "%s:\n num_examples = %d\n num_inputs = %d",
+ dir, numExamples, len(cases)-numExamples)
+
+ return cases, nil
+}
+
+// copyRequest deep-copies via JSON marshal/unmarshal so each
+// snapshot taken inside the build loop is independent of later
+// mutations.
+func copyRequest(
+ req api.MessagesRequest,
+) (*api.MessagesRequest, error) {
+ buf, err := json.Marshal(req)
+ if err != nil {
+ return nil, fmt.Errorf("copy request: %v", err)
+ }
+ out := &api.MessagesRequest{}
+ if err := json.Unmarshal(buf, out); err != nil {
+ return nil, fmt.Errorf("copy request: %v", err)
+ }
+ return out, nil
+}
diff --git a/main.go b/main.go
index 487818f..1f5d91c 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ var commands = map[string]func([]string) error{
"auth": runAuth,
"embed": runEmbed,
"one": runOne,
+ "exemplary": runExemplary,
"group": runGroup,
"authz": runAuthz,
"chat": runChat,
@@ -137,6 +138,13 @@ ARTIFICIAL INTELLIGENCE
(reads stdin as a JSON MessagesRequest; flags override
its fields)

+ okg exemplary
+ --dir DIR directory of <case>.<ext> files
+ (default: .)
+ --model NAME LLM model (default: "Gemini 3 Pro")
+ --dry-run build but don't send requests
+ --debug log each request to stderr
+
GLOBAL FLAGS
--repo REPO override auto-detected repo name
--json output raw JSON (where applicable)
@@ -175,5 +183,6 @@ EXAMPLES
echo 'hello world' | okg embed --dims 384
echo Hello? > /tmp/q.txt && \
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
+ okg exemplary --dir tagging_cases
`)
}
a/README.md
b/README.md
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
## Position
12
## Position
13
13
14
okg is world-public. It runs anywhere — on developer laptops,
14
okg is world-public. It runs anywhere — on developer laptops,
15
in cloud workers, in agent sandboxes — and authenticates
15
in cloud workers, in agent sandboxes — and authenticates
16
against OscarKilo services with a Klex API key saved to
16
against OscarKilo services with a Klex API key saved to
17
`~/.config/okg/config.json`. The wire formats it sends and
17
`~/.config/okg/config.json`. The wire formats it sends and
18
decodes are deliberately limited to fields that are safe to
18
decodes are deliberately limited to fields that are safe to
19
expose outside any data center.
19
expose outside any data center.
20
20
21
If you're writing an LLM agent, an automation script, or
21
If you're writing an LLM agent, an automation script, or
22
anything else that runs outside Oscar Kilo's infrastructure,
22
anything else that runs outside Oscar Kilo's infrastructure,
23
okg is the entry point.
23
okg is the entry point.
24
24
25
## Install
25
## Install
26
26
27
```bash
27
```bash
28
go install oscarkilo.com/okg@latest
28
go install oscarkilo.com/okg@latest
29
```
29
```
30
30
31
Or build from source:
31
Or build from source:
32
32
33
```bash
33
```bash
34
git clone https://code.oscarkilo.com/okg
34
git clone https://code.oscarkilo.com/okg
35
cd okg && go build .
35
cd okg && go build .
36
```
36
```
37
37
38
## Setup
38
## Setup
39
39
40
By default okg talks to the production klee host
40
By default okg talks to the production klee host
41
(`https://code.oscarkilo.com`); the only setup you need is your
41
(`https://code.oscarkilo.com`); the only setup you need is your
42
API key. `okg auth login` saves it to
42
API key. `okg auth login` saves it to
43
`~/.config/okg/config.json`.
43
`~/.config/okg/config.json`.
44
44
45
```bash
45
```bash
46
# Non-interactive (for agents and scripts).
46
# Non-interactive (for agents and scripts).
47
okg auth login --key sk-...
47
okg auth login --key sk-...
48
cat ~/.klex.key | okg auth login # equivalent, ps-safe
48
cat ~/.klex.key | okg auth login # equivalent, ps-safe
49
49
50
# Interactive (prompts for host then key).
50
# Interactive (prompts for host then key).
51
okg auth login
51
okg auth login
52
```
52
```
53
53
54
Override the host for dev/local work with `--host` on `auth login`.
54
Override the host for dev/local work with `--host` on `auth login`.
55
55
56
## Commands
56
## Commands
57
57
58
```
58
```
59
okg repo list
59
okg repo list
60
60
61
okg pr list [--state open|closed]
61
okg pr list [--state open|closed]
62
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
62
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
63
okg pr view NUMBER
63
okg pr view NUMBER
64
okg pr diff NUMBER
64
okg pr diff NUMBER
65
okg pr comment NUMBER --body BODY [--approve | --request-changes]
65
okg pr comment NUMBER --body BODY [--approve | --request-changes]
66
okg pr merge NUMBER
66
okg pr merge NUMBER
67
okg pr close NUMBER
67
okg pr close NUMBER
68
okg pr reopen NUMBER
68
okg pr reopen NUMBER
69
69
70
okg auth login [--key KEY] [--host HOST]
70
okg auth login [--key KEY] [--host HOST]
71
71
72
okg group list [--json]
72
okg group list [--json]
73
okg group create NAME [--full-name TEXT] [--owner USER]
73
okg group create NAME [--full-name TEXT] [--owner USER]
74
okg group add-member GROUP USER [USER ...]
74
okg group add-member GROUP USER [USER ...]
75
okg group remove-member GROUP USER [USER ...]
75
okg group remove-member GROUP USER [USER ...]
76
okg group members GROUP [--json]
76
okg group members GROUP [--json]
77
okg group delete NAME
77
okg group delete NAME
78
78
79
okg authz list [--json]
79
okg authz list [--json]
80
okg authz set URI OWNER READER
80
okg authz set URI OWNER READER
81
okg authz delete URI
81
okg authz delete URI
82
82
83
okg chat send TO TEXT
83
okg chat send TO TEXT
84
okg chat fetch [--to GROUP] [--json]
84
okg chat fetch [--to GROUP] [--json]
85
85
86
okg embed [--model NAME] [--dims N] [--full-path]
86
okg embed [--model NAME] [--dims N] [--full-path]
87
(reads stdin → vectors on stdout)
87
(reads stdin → vectors on stdout)
88
okg one [--model NAME] [--system-file FILE] \
88
okg one [--model NAME] [--system-file FILE] \
89
[--prompt-file FILE] [--attach FILE] \
89
[--prompt-file FILE] [--attach FILE] \
90
[--format text|json|jsonindent] [--fast-fail]
90
[--format text|json|jsonindent] [--fast-fail]
91
(reads stdin as a JSON MessagesRequest)
91
(reads stdin as a JSON MessagesRequest)
92
okg exemplary [--dir DIR] [--model NAME] [--dry-run] [--debug]
93
(few-shot batch runner; walks <case>.<ext>
94
files in DIR, writes responses to <case>.out)
92
```
95
```
93
96
94
### Coming next
97
With `okg exemplary` in place, the standalone `klex-git`
95
98
binaries (`one`, `embed`, `exemplary`) are deprecated.
96
- `okg exemplary` — few-shot batch runner (from
97
`klex-git/exemplary`).
98
99
Once that lands, the standalone `klex-git` binaries are
100
deprecated.
101
99
102
### Flags
100
### Flags
103
101
104
- `--repo REPO` overrides auto-detected repo name
102
- `--repo REPO` overrides auto-detected repo name
105
(normally parsed from `git remote get-url origin`)
103
(normally parsed from `git remote get-url origin`)
106
- `--json` outputs raw JSON for any command
104
- `--json` outputs raw JSON for any command
107
- `OKG_REPO` env var also overrides repo detection
105
- `OKG_REPO` env var also overrides repo detection
108
106
109
## Repo Detection
107
## Repo Detection
110
108
111
Like `gh`, okg detects the repo from the current directory's
109
Like `gh`, okg detects the repo from the current directory's
112
git remote:
110
git remote:
113
111
114
```
112
```
115
git remote get-url origin
113
git remote get-url origin
116
→ https://code.oscarkilo.com/widget.git
114
→ https://code.oscarkilo.com/widget.git
117
→ repo = "widget"
115
→ repo = "widget"
118
```
116
```
119
117
120
## Dependencies
118
## Dependencies
121
119
122
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
120
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
123
to be folded in once all `klex-git` binaries have moved here).
121
to be folded in once all `klex-git` binaries have moved here).
124
- Otherwise Go standard library only.
122
- Otherwise Go standard library only.
/dev/null
b/exemplary.go
1
package main
2
3
import "encoding/json"
4
import "flag"
5
import "fmt"
6
import "log"
7
import "os"
8
import "path"
9
import "sort"
10
import "strings"
11
12
import "oscarkilo.com/klex-git/api"
13
14
// runExemplary runs few-shot LLM inference on a directory full
15
// of "<case>.<ext>" input files. Files ending in .out are
16
// treated as examples (paired with their non-.out siblings);
17
// the remaining inputs are sent to the LLM and the responses
18
// are written back as <case>.out files.
19
//
20
// Mirrors the (now-deprecated) `klex-git/exemplary` binary.
21
func runExemplary(args []string) error {
22
fs := flag.NewFlagSet("exemplary", flag.ContinueOnError)
23
dir := fs.String("dir", ".",
24
"directory to scan for cases and write outputs to")
25
model := fs.String("model", "Gemini 3 Pro",
26
"LLM model name")
27
dryRun := fs.Bool("dry-run", false,
28
"scan + build requests but don't send them")
29
debug := fs.Bool("debug", false,
30
"log each request body to stderr before sending")
31
if err := fs.Parse(args); err != nil {
32
return err
33
}
34
35
cfg, err := loadConfig()
36
if err != nil {
37
return err
38
}
39
if cfg.ApiKey == "" {
40
return fmt.Errorf(
41
"no API key — run `okg auth login --key sk-...`")
42
}
43
client := newKlexClient(cfg)
44
45
// Build a request that starts with the system prompt and
46
// grows by one user message per case (plus an assistant
47
// message for examples). Inputs (cases without .out) get a
48
// snapshot of the request taken just before their user
49
// message would otherwise be added to the running prefix.
50
sysBytes, err := os.ReadFile(
51
path.Join(*dir, "system_prompt.txt"))
52
if err != nil {
53
return fmt.Errorf("read system_prompt.txt: %v", err)
54
}
55
req := api.MessagesRequest{
56
Model: *model,
57
System: string(sysBytes),
58
}
59
60
cases, err := scanForCases(*dir)
61
if err != nil {
62
return err
63
}
64
65
for i, c := range cases {
66
user := api.ChatMessage{Role: "user"}
67
text := "Case name: " + c.Name + "\n\n"
68
for _, suffix := range c.Before {
69
switch suffix {
70
case "txt":
71
b, err := os.ReadFile(
72
path.Join(*dir, c.Name+".txt"))
73
if err != nil {
74
return fmt.Errorf(
75
"read %s.txt: %v", c.Name, err)
76
}
77
text += string(b)
78
case "json":
79
b, err := os.ReadFile(
80
path.Join(*dir, c.Name+".json"))
81
if err != nil {
82
return fmt.Errorf(
83
"read %s.json: %v", c.Name, err)
84
}
85
text += "\n\n```json\n" + string(b) + "\n```\n"
86
case "jpg", "jpeg", "png", "webp":
87
b, err := os.ReadFile(
88
path.Join(*dir, c.Name+"."+suffix))
89
if err != nil {
90
return fmt.Errorf(
91
"read %s.%s: %v", c.Name, suffix, err)
92
}
93
user.Content = append(user.Content,
94
api.NewDocumentBlock(b))
95
default:
96
return fmt.Errorf(
97
"unsupported suffix %s in case %s",
98
suffix, c.Name)
99
}
100
}
101
user.Content = append(user.Content, api.ContentBlock{
102
Type: "text",
103
Text: text,
104
})
105
req.Messages = append(req.Messages, user)
106
if c.After != "" {
107
out, err := os.ReadFile(
108
path.Join(*dir, c.Name+".out"))
109
if err != nil {
110
return fmt.Errorf(
111
"read %s.out: %v", c.Name, err)
112
}
113
req.Messages = append(req.Messages,
114
api.ChatMessage{
115
Role: "assistant",
116
Content: []api.ContentBlock{
117
{Type: "text", Text: string(out)},
118
},
119
})
120
} else {
121
copy, err := copyRequest(req)
122
if err != nil {
123
return err
124
}
125
cases[i].Request = copy
126
req.Messages = req.Messages[:len(req.Messages)-1]
127
}
128
}
129
130
if *dryRun {
131
log.Printf("dry run; not sending requests")
132
return nil
133
}
134
135
// TODO: parallelize.
136
for _, c := range cases {
137
if c.Request == nil {
138
continue
139
}
140
if *debug {
141
log.Printf("Case %s: sending request:", c.Name)
142
enc := json.NewEncoder(os.Stderr)
143
enc.SetIndent("", " ")
144
enc.Encode(c.Request)
145
}
146
res, err := client.Messages(*c.Request)
147
if err != nil {
148
log.Printf(
149
"Case %s: request failed: %v", c.Name, err)
150
continue
151
}
152
if len(res.Content) != 1 {
153
log.Printf(
154
"Case %s: empty response", c.Name)
155
continue
156
}
157
c0 := res.Content[0]
158
if c0.Type != "text" {
159
log.Printf(
160
"Case %s: Content[0].Type = %s",
161
c.Name, c0.Type)
162
continue
163
}
164
out := path.Join(*dir, c.Name+".out")
165
err = os.WriteFile(
166
out, append([]byte(c0.Text), '\n'), 0644)
167
if err != nil {
168
log.Printf(
169
"Case %s: failed to write %s.out: %v",
170
c.Name, c.Name, err)
171
continue
172
}
173
log.Printf("Case %s: wrote %s.out", c.Name, c.Name)
174
}
175
return nil
176
}
177
178
// Case is one input or one input-plus-example file group in
179
// the directory exemplary scans.
180
type Case struct {
181
Name string
182
Before []string // file extensions present: txt, json, jpg, ...
183
After string // "out" if a paired .out file exists
184
Request *api.MessagesRequest
185
}
186
187
// scanForCases walks dir and groups files into Cases. Files
188
// named system_prompt.txt are skipped (consumed separately as
189
// the request's System field). Cases with an .out file sort
190
// before cases without, so the example-prefix building loop
191
// can stop at the first non-example.
192
func scanForCases(dir string) ([]Case, error) {
193
entries, err := os.ReadDir(dir)
194
if err != nil {
195
return nil, fmt.Errorf("read dir %s: %v", dir, err)
196
}
197
198
before := make(map[string][]string)
199
after := make(map[string]string)
200
for _, entry := range entries {
201
if entry.IsDir() {
202
continue
203
}
204
if entry.Name() == "system_prompt.txt" {
205
continue
206
}
207
chunks := strings.Split(entry.Name(), ".")
208
if len(chunks) < 2 {
209
continue
210
}
211
name := strings.Join(
212
chunks[0:len(chunks)-1], ".")
213
suffix := chunks[len(chunks)-1]
214
switch suffix {
215
case "txt", "json", "jpg", "jpeg", "png", "webp":
216
before[name] = append(before[name], suffix)
217
case "out":
218
after[name] = suffix
219
}
220
}
221
222
var cases []Case
223
for name, b := range before {
224
sort.Slice(b, func(i, j int) bool {
225
if b[i] == "txt" && b[j] != "txt" {
226
return true
227
}
228
if b[i] != "txt" && b[j] == "txt" {
229
return false
230
}
231
return b[i] < b[j]
232
})
233
cases = append(cases, Case{
234
Name: name,
235
Before: b,
236
After: after[name],
237
})
238
}
239
240
sort.Slice(cases, func(i, j int) bool {
241
a, b := cases[i], cases[j]
242
if a.After != "" && b.After == "" {
243
return true
244
}
245
if a.After == "" && b.After != "" {
246
return false
247
}
248
return a.Name < b.Name
249
})
250
251
numExamples := 0
252
for ; numExamples < len(cases); numExamples++ {
253
if cases[numExamples].After == "" {
254
break
255
}
256
}
257
log.Printf(
258
"%s:\n num_examples = %d\n num_inputs = %d",
259
dir, numExamples, len(cases)-numExamples)
260
261
return cases, nil
262
}
263
264
// copyRequest deep-copies via JSON marshal/unmarshal so each
265
// snapshot taken inside the build loop is independent of later
266
// mutations.
267
func copyRequest(
268
req api.MessagesRequest,
269
) (*api.MessagesRequest, error) {
270
buf, err := json.Marshal(req)
271
if err != nil {
272
return nil, fmt.Errorf("copy request: %v", err)
273
}
274
out := &api.MessagesRequest{}
275
if err := json.Unmarshal(buf, out); err != nil {
276
return nil, fmt.Errorf("copy request: %v", err)
277
}
278
return out, nil
279
}
a/main.go
b/main.go
1
package main
1
package main
2
2
3
import "fmt"
3
import "fmt"
4
import "os"
4
import "os"
5
5
6
// commands maps each top-level subcommand to its handler.
6
// commands maps each top-level subcommand to its handler.
7
// printUsage's sections need to stay in sync with this list.
7
// printUsage's sections need to stay in sync with this list.
8
var commands = map[string]func([]string) error{
8
var commands = map[string]func([]string) error{
9
"pr": runPR,
9
"pr": runPR,
10
"repo": runRepo,
10
"repo": runRepo,
11
"auth": runAuth,
11
"auth": runAuth,
12
"embed": runEmbed,
12
"embed": runEmbed,
13
"one": runOne,
13
"one": runOne,
14
"exemplary": runExemplary,
14
"group": runGroup,
15
"group": runGroup,
15
"authz": runAuthz,
16
"authz": runAuthz,
16
"chat": runChat,
17
"chat": runChat,
17
}
18
}
18
19
19
func main() {
20
func main() {
20
args := os.Args[1:]
21
args := os.Args[1:]
21
if len(args) == 0 {
22
if len(args) == 0 {
22
printUsage()
23
printUsage()
23
os.Exit(1)
24
os.Exit(1)
24
}
25
}
25
switch args[0] {
26
switch args[0] {
26
case "help", "--help", "-h":
27
case "help", "--help", "-h":
27
printUsage()
28
printUsage()
28
return
29
return
29
}
30
}
30
fn, ok := commands[args[0]]
31
fn, ok := commands[args[0]]
31
if !ok {
32
if !ok {
32
fmt.Fprintf(
33
fmt.Fprintf(
33
os.Stderr, "unknown command: %s\n", args[0])
34
os.Stderr, "unknown command: %s\n", args[0])
34
printUsage()
35
printUsage()
35
os.Exit(1)
36
os.Exit(1)
36
}
37
}
37
if err := fn(args[1:]); err != nil {
38
if err := fn(args[1:]); err != nil {
38
fmt.Fprintf(os.Stderr, "error: %v\n", err)
39
fmt.Fprintf(os.Stderr, "error: %v\n", err)
39
os.Exit(1)
40
os.Exit(1)
40
}
41
}
41
}
42
}
42
43
43
func printUsage() {
44
func printUsage() {
44
fmt.Fprintf(os.Stderr, `NAME
45
fmt.Fprintf(os.Stderr, `NAME
45
okg — Oscar Kilo Goodness
46
okg — Oscar Kilo Goodness
46
47
47
SETUP
48
SETUP
48
okg auth login
49
okg auth login
49
--key KEY API key (also accepted via stdin)
50
--key KEY API key (also accepted via stdin)
50
--host HOST klee host (default: production)
51
--host HOST klee host (default: production)
51
52
52
GIT REPOS
53
GIT REPOS
53
okg repo list
54
okg repo list
54
--json output raw JSON
55
--json output raw JSON
55
56
56
okg repo create NAME
57
okg repo create NAME
57
--reader USER grant read to USER (default: anyone)
58
--reader USER grant read to USER (default: anyone)
58
59
59
GROUPS
60
GROUPS
60
okg group list
61
okg group list
61
--json output raw JSON
62
--json output raw JSON
62
63
63
okg group create NAME
64
okg group create NAME
64
--full-name TEXT display name (default: NAME)
65
--full-name TEXT display name (default: NAME)
65
--owner USER owner username (default: caller)
66
--owner USER owner username (default: caller)
66
67
67
okg group add-member GROUP USER [USER ...]
68
okg group add-member GROUP USER [USER ...]
68
69
69
okg group remove-member GROUP USER [USER ...]
70
okg group remove-member GROUP USER [USER ...]
70
71
71
okg group members GROUP
72
okg group members GROUP
72
--json full DAG (Up/Down/Usernames) as raw JSON
73
--json full DAG (Up/Down/Usernames) as raw JSON
73
74
74
okg group delete NAME
75
okg group delete NAME
75
76
76
AUTHZ
77
AUTHZ
77
okg authz list
78
okg authz list
78
--json output raw JSON
79
--json output raw JSON
79
80
80
okg authz set URI OWNER READER
81
okg authz set URI OWNER READER
81
82
82
okg authz delete URI
83
okg authz delete URI
83
84
84
CHAT
85
CHAT
85
okg chat send TO TEXT
86
okg chat send TO TEXT
86
87
87
okg chat fetch
88
okg chat fetch
88
--to GROUP filter by destination group
89
--to GROUP filter by destination group
89
--json output raw JSON
90
--json output raw JSON
90
91
91
PULL REQUESTS
92
PULL REQUESTS
92
okg pr list
93
okg pr list
93
--state STATE open or closed (default: open)
94
--state STATE open or closed (default: open)
94
--json output raw JSON
95
--json output raw JSON
95
96
96
okg pr create
97
okg pr create
97
--head BRANCH source branch
98
--head BRANCH source branch
98
--base BRANCH target branch (default: master)
99
--base BRANCH target branch (default: master)
99
--title TITLE PR title
100
--title TITLE PR title
100
--body BODY PR body (optional)
101
--body BODY PR body (optional)
101
--json output raw JSON
102
--json output raw JSON
102
103
103
okg pr view NUMBER
104
okg pr view NUMBER
104
--json output raw JSON
105
--json output raw JSON
105
106
106
okg pr diff NUMBER
107
okg pr diff NUMBER
107
108
108
okg pr comment NUMBER
109
okg pr comment NUMBER
109
--body BODY comment body
110
--body BODY comment body
110
--approve also approve the PR
111
--approve also approve the PR
111
--request-changes also request changes
112
--request-changes also request changes
112
113
113
okg pr merge NUMBER
114
okg pr merge NUMBER
114
--json output raw JSON
115
--json output raw JSON
115
116
116
okg pr close NUMBER
117
okg pr close NUMBER
117
--json output raw JSON
118
--json output raw JSON
118
119
119
okg pr reopen NUMBER
120
okg pr reopen NUMBER
120
--json output raw JSON
121
--json output raw JSON
121
122
122
ARTIFICIAL INTELLIGENCE
123
ARTIFICIAL INTELLIGENCE
123
okg embed
124
okg embed
124
--model NAME embedding model
125
--model NAME embedding model
125
(default: openai:text-embedding-3-small)
126
(default: openai:text-embedding-3-small)
126
--dims N number of dimensions (default: 1536)
127
--dims N number of dimensions (default: 1536)
127
--full-path one vector per prefix of input
128
--full-path one vector per prefix of input
128
(reads stdin; writes vectors to stdout, one per line)
129
(reads stdin; writes vectors to stdout, one per line)
129
130
130
okg one
131
okg one
131
--model NAME override .Model in the request
132
--model NAME override .Model in the request
132
--system-file FILE override .System with contents of FILE
133
--system-file FILE override .System with contents of FILE
133
--prompt-file FILE append FILE as a user prompt
134
--prompt-file FILE append FILE as a user prompt
134
--attach FILE attach an image or PDF to the prompt
135
--attach FILE attach an image or PDF to the prompt
135
--format FORMAT text | json | jsonindent (default: text)
136
--format FORMAT text | json | jsonindent (default: text)
136
--fast-fail preflight attachment MIME (default: on)
137
--fast-fail preflight attachment MIME (default: on)
137
(reads stdin as a JSON MessagesRequest; flags override
138
(reads stdin as a JSON MessagesRequest; flags override
138
its fields)
139
its fields)
139
140
141
okg exemplary
142
--dir DIR directory of <case>.<ext> files
143
(default: .)
144
--model NAME LLM model (default: "Gemini 3 Pro")
145
--dry-run build but don't send requests
146
--debug log each request to stderr
147
140
GLOBAL FLAGS
148
GLOBAL FLAGS
141
--repo REPO override auto-detected repo name
149
--repo REPO override auto-detected repo name
142
--json output raw JSON (where applicable)
150
--json output raw JSON (where applicable)
143
151
144
EXAMPLES
152
EXAMPLES
145
Setup
153
Setup
146
okg auth login --key sk-...
154
okg auth login --key sk-...
147
cat ~/.klex.key | okg auth login
155
cat ~/.klex.key | okg auth login
148
156
149
Git repos
157
Git repos
150
okg repo list
158
okg repo list
151
okg repo create my-new-repo
159
okg repo create my-new-repo
152
160
153
Groups
161
Groups
154
okg group list
162
okg group list
155
okg group create chat-bots
163
okg group create chat-bots
156
okg group add-member chat-bots claude openclaw
164
okg group add-member chat-bots claude openclaw
157
okg group remove-member chat-bots openclaw
165
okg group remove-member chat-bots openclaw
158
okg group members chat-bots
166
okg group members chat-bots
159
okg group delete chat-bots
167
okg group delete chat-bots
160
168
161
Authz
169
Authz
162
okg authz set chat://chat-bots#post chat-bots chat-bots
170
okg authz set chat://chat-bots#post chat-bots chat-bots
163
okg authz list
171
okg authz list
164
172
165
Chat
173
Chat
166
okg chat send chat-bots 'hello team'
174
okg chat send chat-bots 'hello team'
167
okg chat fetch --to chat-bots
175
okg chat fetch --to chat-bots
168
176
169
Pull requests
177
Pull requests
170
okg pr list --state open
178
okg pr list --state open
171
okg pr view 42
179
okg pr view 42
172
okg pr comment 42 --body 'LGTM' --approve
180
okg pr comment 42 --body 'LGTM' --approve
173
181
174
Artificial intelligence
182
Artificial intelligence
175
echo 'hello world' | okg embed --dims 384
183
echo 'hello world' | okg embed --dims 384
176
echo Hello? > /tmp/q.txt && \
184
echo Hello? > /tmp/q.txt && \
177
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
185
okg one --model openai:gpt-4o-mini --prompt-file /tmp/q.txt
186
okg exemplary --dir tagging_cases
178
`)
187
`)
179
}
188
}