ScuttleBot
Add codex relay broker and install primers
Commit
50baf1a5833ef0a2c3f18771073d708ed9e2bcda72f713678804a5f66c41cf43
Parent
a05a5a46e2a2414…
14 files changed
+28
+201
+262
+84
+287
+114
+30
+28
+1
+18
+228
+132
+142
+125
+
cmd/codex-relay/main.go
+
cmd/codex-relay/main_test.go
+
skills/irc-agent/README.md
+
skills/openai-relay/FLEET.md
+
skills/openai-relay/SKILL.md
+
skills/openai-relay/hooks/README.md
+
skills/openai-relay/hooks/scuttlebot-check.sh
+
skills/openai-relay/hooks/scuttlebot-post.sh
+
skills/openai-relay/install.md
+
skills/openai-relay/scripts/codex-relay.sh
+
skills/openai-relay/scripts/install-codex-relay.sh
+
skills/openai-relay/scripts/node-openai-relay.mjs
+
skills/openai-relay/scripts/python-openai-relay.py
+
skills/scuttlebot-relay/ADDING_AGENTS.md
+28
| --- a/cmd/codex-relay/main.go | ||
| +++ b/cmd/codex-relay/main.go | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bufio" | |
| 5 | + "bytes" | |
| 6 | + "context" | |
| 7 | + "encoding/json" | |
| 8 | + "errors" | |
| 9 | + "fmt" | |
| 10 | + "hash/crc32" | |
| 11 | + "io" | |
| 12 | + "net/http" | |
| 13 | + "os" | |
| 14 | + "os/exec" | |
| 15 | + "os/signal" | |
| 16 | + "path/filepath" | |
| 17 | + "regexp" | |
| 18 | + "sort" | |
| 19 | + "strings" | |
| 20 | + "sync" | |
| 21 | + "syscall" | |
| 22 | + "time" | |
| 23 | + | |
| 24 | + "github.com/conflicthq/scuttlebot/pessionrelay" | |
| 25 | + "github.com/creack/pty" | |
| 26 | + "golang.org/x/term" | |
| 27 | +/term" | |
| 28 | + "gopkg.in/yam |
| --- a/cmd/codex-relay/main.go | |
| +++ b/cmd/codex-relay/main.go | |
| @@ -0,0 +1,28 @@ | |
| --- a/cmd/codex-relay/main.go | |
| +++ b/cmd/codex-relay/main.go | |
| @@ -0,0 +1,28 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "bytes" |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | "hash/crc32" |
| 11 | "io" |
| 12 | "net/http" |
| 13 | "os" |
| 14 | "os/exec" |
| 15 | "os/signal" |
| 16 | "path/filepath" |
| 17 | "regexp" |
| 18 | "sort" |
| 19 | "strings" |
| 20 | "sync" |
| 21 | "syscall" |
| 22 | "time" |
| 23 | |
| 24 | "github.com/conflicthq/scuttlebot/pessionrelay" |
| 25 | "github.com/creack/pty" |
| 26 | "golang.org/x/term" |
| 27 | /term" |
| 28 | "gopkg.in/yam |
+201
| --- a/cmd/codex-relay/main_test.go | ||
| +++ b/cmd/codex-relay/main_test.go | ||
| @@ -0,0 +1,201 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytesf err != nil { | |
| 5 | + t.Fat want { | |
| 6 | + t.Fatalf("target, | |
| 7 | + }ages, since, nick, "worker") | |
| 8 | + if len(got) != 2 { | |
| 9 | + t.Fatalf("len(filterMessages) = %d, want 2", len(got)) | |
| 10 | + } | |
| 11 | + if got[0].Text != nick+": check README.md" { | |
| 12 | + t.Fatalf("first injected message = %q", got[0].Text) | |
| 13 | + } | |
| 14 | + if got[1].Text != nick+": and inspect bridge.go" { | |
| 15 | + t.Time: base | |
| 16 | + got, err := targetCWD([]string"ambient chat", Time !newest.Equal(base.Add(4 * time.Second)) { | |
| 17 | + t.Fatalf("newest = %s", newest) | |
| 18 | + }Time"newest = %s", newest) | |
| 19 | + } | |
| 20 | +} | |
| 21 | + | |
| 22 | +func TestTargetCWD(t *testing.T) { | |
| 23 | + t.Helper() | |
| 24 | + | |
| 25 | + cwd, err Time!= nil { | |
| 26 | + t.Fatal(err) | |
| 27 | + } | |
| 28 | + | |
| 29 | + got, err := targetCWD([]string{"--cd", "../.."}) | |
| 30 | + if err != nil Time: base.Add(4.Fatal(err) | |
| 31 | + } | |
| 32 | + | |
| 33 | + gorrepo-999= filepath.Clean(filepath.Join(cwd, "../..")) | |
| 34 | + if got ! | |
| 35 | + "bytes" | |
| 36 | + " ( | |
| 37 | + "bytes" | |
| 38 | + "fmt" | |
| 39 | + "os" | |
| 40 | + "path/filepath" | |
| 41 | + "strings" | |
| 42 | + "testing" | |
| 43 | + "time" | |
| 44 | +) | |
| 45 | + | |
| 46 | +func TestFilterMessages(t *testing.T) { | |
| 47 | + t.Helper() | |
| 48 | + | |
| 49 | + base := time.Date(2026, 3, 31, 21, 0, 0, 0, time.FixedZone("CST", -6*60*60)) | |
| 50 | + since := base.Add(-time.Second) | |
| 51 | + nick := "codex-scuttlebot-1234" | |
| 52 | + | |
| 53 | + messages := []message{ | |
| 54 | + {Nick: "bridge", Text: "[glengoolie] hello", At: base}, | |
| 55 | + {Nick: "glengoolie", Text: "ambient chat", At: base.Add(time.Second)}, | |
| 56 | + {Nick: "codex-otherrepo-9999", Text: "status post", At: base.Add(2 * time.Second)}, | |
| 57 | + {Nick: "glengoolie", Text: nick + ": check README.md", At: base.Add(3 * time.Second)}, | |
| 58 | + {Nick: "glengoolie", Text: nick + ": and inspect bridge.go", At: base.Add(4 * time.Second)}, | |
| 59 | + }ages, since, nick, "worker") | |
| 60 | + if len(got) != 2 { | |
| 61 | + t.Fatalf("len(filterMessages) = %d, want 2", len(got)) | |
| 62 | + } | |
| 63 | + if got[0].Text != nick+": check README.md" { | |
| 64 | + t.Fatalf("first injected message = %q", got[0].Text) | |
| 65 | + } | |
| 66 | + if got[1].Text != nick+": and inspect bridge.go" { | |
| 67 | + t.Fatalf("second injected message = %q", got[1].Text) | |
| 68 | + } | |
| 69 | + if !newest.Equal(base.Add(4 * time.Second)) { | |
| 70 | + t.Fatalf("newest = %s", newest) | |
| 71 | + } | |
| 72 | +} | |
| 73 | + | |
| 74 | +func TestTargetCWD(t *testing.T) { | |
| 75 | + t.Helper() | |
| 76 | + | |
| 77 | + cwd, err := filepath.Abs(".") | |
| 78 | + if err != nil { | |
| 79 | + t.Fatal(err) | |
| 80 | + } | |
| 81 | + | |
| 82 | + got, err := targetCWD([]string{"--cd", "../.."}) | |
| 83 | + if err != nil { | |
| 84 | + t.Fatal(err) | |
| 85 | + } | |
| 86 | + want := filepath.Clean(filepath.Join(cwd, "../..")) | |
| 87 | + if got != want { | |
| 88 | + t.Fatalf("targetCWD = %q, want %q", got, want) | |
| 89 | + } | |
| 90 | +} | |
| 91 | + | |
| 92 | +func TestRelayStateShouldInterruptOnlyWhenRecentlyBusy(t *testing.T) { | |
| 93 | + t.Helper() | |
| 94 | + | |
| 95 | + var state relayState | |
| 96 | + now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) | |
| 97 | + state.observeOutput([]byte("Working (1s • esc to interrupt)"), now) | |
| 98 | + | |
| 99 | + if !state.shouldInterrupt(now.Add(defaultBusyWindow / 2)) { | |
| 100 | + t.Fatal("shouldInterrupt = false, want true for recent busy session") | |
| 101 | + } | |
| 102 | + if state.shouldInterrupt(now.Add(defaultBusyWindow + time.Millisecond)) { | |
| 103 | + t.Fatal("shouldInterrupt = true, want false after busy window expires") | |
| 104 | + } | |
| 105 | +} | |
| 106 | + | |
| 107 | +func TestInjectMessagesIdleSkipsCtrlCAndSubmits(t *testing.T) { | |
| 108 | + t.Helper() | |
| 109 | + | |
| 110 | + var writer bytes.Buffer | |
| 111 | + cfg := config{ | |
| 112 | + Nick: "codex-scuttlebpackage main | |
| 113 | + | |
| 114 | +import ( | |
| 115 | + "bytes" | |
| 116 | + " ( | |
| 117 | + "bytes" | |
| 118 | + "fmt" | |
| 119 | + "os" | |
| 120 | + "path/filepath" | |
| 121 | + "strings" | |
| 122 | + "testing" | |
| 123 | + "time" | |
| 124 | +) | |
| 125 | + | |
| 126 | +func TestFilterMessages(t *testing.T) { | |
| 127 | + t.Helper() | |
| 128 | + | |
| 129 | + base := time.Date(2026, 3, 31, 21, 0, 0, 0, time.FixedZone("CST", -6*60*60)) | |
| 130 | + since := base.Add(-time.Second) | |
| 131 | + nick := "codex-scuttlebot-1234" | |
| 132 | + | |
| 133 | + messages := []message{ | |
| 134 | + {Nick: "bridge", Text: "[glengoolie] hello", At: base}, | |
| 135 | + {Nick: "glengoolie", Text: "ambient chat", At: base.Add(time.Second)}, | |
| 136 | + {Nick: "codex-otherrepo-9999", Text: "status post", At: base.Add(2 * time.Second)}, | |
| 137 | + {Nick: "glengoolie", Text: nick + ": check README.md", At: base.Add(3 * time.Second)}, | |
| 138 | + {Nick: "glengoolie", Text: nick + ": and inspect bridge.go", At: base.Add(4 * time.Second)}, | |
| 139 | + }ages, since, nick, "worker") | |
| 140 | + if len(got) != 2 { | |
| 141 | + t.Fatalf("len(filterMessages) = %d, want 2", len(got)) | |
| 142 | + } | |
| 143 | + if got[0].Text != nick+": check README.md" { | |
| 144 | + t.Fatalf("first injected message = %q", got[0].Text) | |
| 145 | + } | |
| 146 | + if got[1].Text != nick+": and inspect bridge.go" { | |
| 147 | + t.Fatalf("second injected message = %q", got[1].Text) | |
| 148 | + } | |
| 149 | + if !newest.Equal(base.Add(4 * time.Second)) { | |
| 150 | + t.Fatalf("newest = %s", newest) | |
| 151 | + } | |
| 152 | +} | |
| 153 | + | |
| 154 | +func TestTargetCWD(t *testing.T) { | |
| 155 | + t.Helper() | |
| 156 | + | |
| 157 | + cwd, err := filepath.Abs(".") | |
| 158 | + if err != nil { | |
| 159 | + t.Fatal(err) | |
| 160 | + } | |
| 161 | + | |
| 162 | + got, err := targetCWD([]string{"--cd", "../.."}) | |
| 163 | + if err != nil { | |
| 164 | + t.Fatal(err) | |
| 165 | + } | |
| 166 | + want := filepath.Clean(filepath.Join(cwd, "../..")) | |
| 167 | + if got != want { | |
| 168 | + t.Fatalf("targetCWD = %q, want %q", got, want) | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +func TestRelayStateShouldInterruptOnlyWhenRecentlyBusy(t *testing.T) { | |
| 173 | + t.Helper() | |
| 174 | + | |
| 175 | + var state relayState | |
| 176 | + now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) | |
| 177 | + state.observeOutput([]byte("Working (1s • esc to interrupt)"), now) | |
| 178 | + | |
| 179 | + if !state.shouldInterrupt(now.Add(defaultBusyWindow / 2)) { | |
| 180 | + t.Fatal("shouldInterrupt = false, want true for recent busy session") | |
| 181 | + } | |
| 182 | + if state.shouldInterrupt(now.Add(defaultBusyWindow + time.Millisecond)) { | |
| 183 | + t.Fatal("shouldInterrupt = true, want false after busy window expires") | |
| 184 | + } | |
| 185 | +} | |
| 186 | + | |
| 187 | +func TestInjectMessagesIdleSkipsCtrlCAndSubmits(t *testing.T) { | |
| 188 | + t.Helper() | |
| 189 | + | |
| 190 | + var writer bytes.Buffer | |
| 191 | + cfg := config{ | |
| 192 | + Nick: "codex-scuttlebot-1234", | |
| 193 | + InterruptOnMessage: true, | |
| 194 | + } | |
| 195 | + state := &relayState{} | |
| 196 | + batch := []message{{ | |
| 197 | + Nick: "glengoolie", | |
| 198 | + Text: "codex-scuttlebot-1234: check README.md", | |
| 199 | + }} | |
| 200 | + | |
| 201 | + if err := injectMessages(&writer, cfg, stat |
| --- a/cmd/codex-relay/main_test.go | |
| +++ b/cmd/codex-relay/main_test.go | |
| @@ -0,0 +1,201 @@ | |
| --- a/cmd/codex-relay/main_test.go | |
| +++ b/cmd/codex-relay/main_test.go | |
| @@ -0,0 +1,201 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytesf err != nil { |
| 5 | t.Fat want { |
| 6 | t.Fatalf("target, |
| 7 | }ages, since, nick, "worker") |
| 8 | if len(got) != 2 { |
| 9 | t.Fatalf("len(filterMessages) = %d, want 2", len(got)) |
| 10 | } |
| 11 | if got[0].Text != nick+": check README.md" { |
| 12 | t.Fatalf("first injected message = %q", got[0].Text) |
| 13 | } |
| 14 | if got[1].Text != nick+": and inspect bridge.go" { |
| 15 | t.Time: base |
| 16 | got, err := targetCWD([]string"ambient chat", Time !newest.Equal(base.Add(4 * time.Second)) { |
| 17 | t.Fatalf("newest = %s", newest) |
| 18 | }Time"newest = %s", newest) |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | func TestTargetCWD(t *testing.T) { |
| 23 | t.Helper() |
| 24 | |
| 25 | cwd, err Time!= nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | |
| 29 | got, err := targetCWD([]string{"--cd", "../.."}) |
| 30 | if err != nil Time: base.Add(4.Fatal(err) |
| 31 | } |
| 32 | |
| 33 | gorrepo-999= filepath.Clean(filepath.Join(cwd, "../..")) |
| 34 | if got ! |
| 35 | "bytes" |
| 36 | " ( |
| 37 | "bytes" |
| 38 | "fmt" |
| 39 | "os" |
| 40 | "path/filepath" |
| 41 | "strings" |
| 42 | "testing" |
| 43 | "time" |
| 44 | ) |
| 45 | |
| 46 | func TestFilterMessages(t *testing.T) { |
| 47 | t.Helper() |
| 48 | |
| 49 | base := time.Date(2026, 3, 31, 21, 0, 0, 0, time.FixedZone("CST", -6*60*60)) |
| 50 | since := base.Add(-time.Second) |
| 51 | nick := "codex-scuttlebot-1234" |
| 52 | |
| 53 | messages := []message{ |
| 54 | {Nick: "bridge", Text: "[glengoolie] hello", At: base}, |
| 55 | {Nick: "glengoolie", Text: "ambient chat", At: base.Add(time.Second)}, |
| 56 | {Nick: "codex-otherrepo-9999", Text: "status post", At: base.Add(2 * time.Second)}, |
| 57 | {Nick: "glengoolie", Text: nick + ": check README.md", At: base.Add(3 * time.Second)}, |
| 58 | {Nick: "glengoolie", Text: nick + ": and inspect bridge.go", At: base.Add(4 * time.Second)}, |
| 59 | }ages, since, nick, "worker") |
| 60 | if len(got) != 2 { |
| 61 | t.Fatalf("len(filterMessages) = %d, want 2", len(got)) |
| 62 | } |
| 63 | if got[0].Text != nick+": check README.md" { |
| 64 | t.Fatalf("first injected message = %q", got[0].Text) |
| 65 | } |
| 66 | if got[1].Text != nick+": and inspect bridge.go" { |
| 67 | t.Fatalf("second injected message = %q", got[1].Text) |
| 68 | } |
| 69 | if !newest.Equal(base.Add(4 * time.Second)) { |
| 70 | t.Fatalf("newest = %s", newest) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestTargetCWD(t *testing.T) { |
| 75 | t.Helper() |
| 76 | |
| 77 | cwd, err := filepath.Abs(".") |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | |
| 82 | got, err := targetCWD([]string{"--cd", "../.."}) |
| 83 | if err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | want := filepath.Clean(filepath.Join(cwd, "../..")) |
| 87 | if got != want { |
| 88 | t.Fatalf("targetCWD = %q, want %q", got, want) |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | func TestRelayStateShouldInterruptOnlyWhenRecentlyBusy(t *testing.T) { |
| 93 | t.Helper() |
| 94 | |
| 95 | var state relayState |
| 96 | now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) |
| 97 | state.observeOutput([]byte("Working (1s • esc to interrupt)"), now) |
| 98 | |
| 99 | if !state.shouldInterrupt(now.Add(defaultBusyWindow / 2)) { |
| 100 | t.Fatal("shouldInterrupt = false, want true for recent busy session") |
| 101 | } |
| 102 | if state.shouldInterrupt(now.Add(defaultBusyWindow + time.Millisecond)) { |
| 103 | t.Fatal("shouldInterrupt = true, want false after busy window expires") |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | func TestInjectMessagesIdleSkipsCtrlCAndSubmits(t *testing.T) { |
| 108 | t.Helper() |
| 109 | |
| 110 | var writer bytes.Buffer |
| 111 | cfg := config{ |
| 112 | Nick: "codex-scuttlebpackage main |
| 113 | |
| 114 | import ( |
| 115 | "bytes" |
| 116 | " ( |
| 117 | "bytes" |
| 118 | "fmt" |
| 119 | "os" |
| 120 | "path/filepath" |
| 121 | "strings" |
| 122 | "testing" |
| 123 | "time" |
| 124 | ) |
| 125 | |
| 126 | func TestFilterMessages(t *testing.T) { |
| 127 | t.Helper() |
| 128 | |
| 129 | base := time.Date(2026, 3, 31, 21, 0, 0, 0, time.FixedZone("CST", -6*60*60)) |
| 130 | since := base.Add(-time.Second) |
| 131 | nick := "codex-scuttlebot-1234" |
| 132 | |
| 133 | messages := []message{ |
| 134 | {Nick: "bridge", Text: "[glengoolie] hello", At: base}, |
| 135 | {Nick: "glengoolie", Text: "ambient chat", At: base.Add(time.Second)}, |
| 136 | {Nick: "codex-otherrepo-9999", Text: "status post", At: base.Add(2 * time.Second)}, |
| 137 | {Nick: "glengoolie", Text: nick + ": check README.md", At: base.Add(3 * time.Second)}, |
| 138 | {Nick: "glengoolie", Text: nick + ": and inspect bridge.go", At: base.Add(4 * time.Second)}, |
| 139 | }ages, since, nick, "worker") |
| 140 | if len(got) != 2 { |
| 141 | t.Fatalf("len(filterMessages) = %d, want 2", len(got)) |
| 142 | } |
| 143 | if got[0].Text != nick+": check README.md" { |
| 144 | t.Fatalf("first injected message = %q", got[0].Text) |
| 145 | } |
| 146 | if got[1].Text != nick+": and inspect bridge.go" { |
| 147 | t.Fatalf("second injected message = %q", got[1].Text) |
| 148 | } |
| 149 | if !newest.Equal(base.Add(4 * time.Second)) { |
| 150 | t.Fatalf("newest = %s", newest) |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | func TestTargetCWD(t *testing.T) { |
| 155 | t.Helper() |
| 156 | |
| 157 | cwd, err := filepath.Abs(".") |
| 158 | if err != nil { |
| 159 | t.Fatal(err) |
| 160 | } |
| 161 | |
| 162 | got, err := targetCWD([]string{"--cd", "../.."}) |
| 163 | if err != nil { |
| 164 | t.Fatal(err) |
| 165 | } |
| 166 | want := filepath.Clean(filepath.Join(cwd, "../..")) |
| 167 | if got != want { |
| 168 | t.Fatalf("targetCWD = %q, want %q", got, want) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | func TestRelayStateShouldInterruptOnlyWhenRecentlyBusy(t *testing.T) { |
| 173 | t.Helper() |
| 174 | |
| 175 | var state relayState |
| 176 | now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) |
| 177 | state.observeOutput([]byte("Working (1s • esc to interrupt)"), now) |
| 178 | |
| 179 | if !state.shouldInterrupt(now.Add(defaultBusyWindow / 2)) { |
| 180 | t.Fatal("shouldInterrupt = false, want true for recent busy session") |
| 181 | } |
| 182 | if state.shouldInterrupt(now.Add(defaultBusyWindow + time.Millisecond)) { |
| 183 | t.Fatal("shouldInterrupt = true, want false after busy window expires") |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | func TestInjectMessagesIdleSkipsCtrlCAndSubmits(t *testing.T) { |
| 188 | t.Helper() |
| 189 | |
| 190 | var writer bytes.Buffer |
| 191 | cfg := config{ |
| 192 | Nick: "codex-scuttlebot-1234", |
| 193 | InterruptOnMessage: true, |
| 194 | } |
| 195 | state := &relayState{} |
| 196 | batch := []message{{ |
| 197 | Nick: "glengoolie", |
| 198 | Text: "codex-scuttlebot-1234: check README.md", |
| 199 | }} |
| 200 | |
| 201 | if err := injectMessages(&writer, cfg, stat |
+262
| --- a/skills/irc-agent/README.md | ||
| +++ b/skills/irc-agent/README.md | ||
| @@ -0,0 +1,262 @@ | ||
| 1 | +# Building an IRC agent on scuttlebot | |
| 2 | + | |
| 3 | +How to connect any agent — LLM-powered chat bot, task runner, monitoring agent, | |
| 4 | +or anything else — to scuttlebot's IRC backplane. Language-agnostic. The Go | |
| 5 | +reference runtime in this repo is `pkg/ircagent`; `cmd/claude-agent`, | |
| 6 | +`cmd/codex-agent`, and `cmd/gemini-agent` are thin wrappers with different defaults. | |
| 7 | + | |
| 8 | +This document is for IRC-resident agents. Live terminal runtimes such as | |
| 9 | +`codex-relay` use a different pattern: a broker owns session presence, | |
| 10 | +continuous operator input injection, and outbound activity mirroring while the | |
| 11 | +runtime stays local.TTskills/{runtime}-relay/`. | |
| 12 | + | |
| 13 | +--- | |
| 14 | + | |
| 15 | +## What scuttlebot gives you | |
| 16 | + | |
| 17 | +- An Ergo IRC server with NickServ account-per-agent (SASL auth) | |
| 18 | +- A bridge bot that relays web UI messages into IRC and back | |
| 19 | +- An HTTP API for agent registration, credential management, and LLM proxying | |
| 20 | +- Human-observable coordination: everything that happens is visible in IRC | |
| 21 | + | |
| 22 | +--- | |
| 23 | + | |
| 24 | +## Architecture | |
| 25 | + | |
| 26 | +``` | |
| 27 | +Web UI / IRC client | |
| 28 | + │ | |
| 29 | + ▼ | |
| 30 | + scuttlebot (bridge bot) | |
| 31 | + │ PRIVMSG via girc | |
| 32 | + ▼ | |
| 33 | + Ergo IRC server (6667) | |
| 34 | + │ PRIVMSG event | |
| 35 | + ▼ | |
| 36 | + claude-agent / codex-agent | |
| 37 | + │ pkg/ircagent.Run(...) | |
| 38 | + │ buildPrompt() → completer.complete() | |
| 39 | + ▼ | |
| 40 | + LLM (direct or gateway) | |
| 41 | + │ reply text | |
| 42 | + ▼ | |
| 43 | + claude-agent → cl.Cmd.Message(channel, reply) | |
| 44 | + │ | |
| 45 | + ▼ | |
| 46 | + Ergo → bridge PRIVMSG → web UI renders it | |
| 47 | +``` | |
| 48 | + | |
| 49 | +### Two operation modes | |
| 50 | + | |
| 51 | +**Direct mode** — the agent calls the LLM provider directly. Needs the API key: | |
| 52 | +``` | |
| 53 | +./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> --api-key sk-ant-... | |
| 54 | +``` | |
| 55 | + | |
| 56 | +**Gateway mode** — proxies through scuttlebot's `/v1/llm/complete` endpoint. | |
| 57 | +The key never leaves the server. Preferred for production: | |
| 58 | +``` | |
| 59 | + | |
| 60 | +### IRC-resident agent vs terminal-session broker | |
| 61 | + | |
| 62 | +- IRC-resident agent: logs into Ergo directly, lives in-channel, responds like a bot | |
| 63 | +- terminal-session broker: wraps a local tool loop, posts `online` / `offline`, | |
| 64 | + mirrors session activity, and injects addressed operator messages back into the | |
| 65 | + live terminal session | |
| 66 | + | |
| 67 | +Use `pkg/ircagent` when the process itself should be an IRC user. Use a broker | |
| 68 | +such as `cmd/codex-relay` when the process should remain a local interactive | |
| 69 | +session but still be operator-addressable from IRC. | |
| 70 | +./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> \ | |
| 71 | + --api-url http://localhost:8080 --token <bearer> --backend anthro | |
| 72 | +``` | |
| 73 | + | |
| 74 | +--- | |
| 75 | + | |
| 76 | +## Key design decisions | |
| 77 | + | |
| 78 | +### Nick registration | |
| 79 | +The agent's IRC nick must be pre-registered as a NickServ account (scuttlebot | |
| 80 | +does this when you register an agent via the UI or API). The agent authenticates | |
| 81 | +via SASL PLAIN on connect. | |
| 82 | + | |
| 83 | +### Message routing | |
| 84 | +- **Channel messages**: the agent only responds when its nick is mentioned. | |
| 85 | + Mention detection uses word-boundary matching. Adjacent characters that | |
| 86 | + suppress a match: letters, digits, `-`, `_`, `.`, `/`, `\`. This means | |
| 87 | + `.claude/hooks/` does NOT trigger a response, but neither does `claude.` | |
| 88 | + at the end of a sentence. Address the agent with `claude:` or `claude,`. | |
| 89 | +- **DMs**: the agent always responds. | |
| 90 | +- **activity-post senders**: hook/session nicks like `claude-*` and | |
| 91 | + `codex-*` are silently observed (added to history) but never responded to. | |
| 92 | + They're status logs, not chat. | |
| 93 | + | |
| 94 | +### Session nick format | |
| 95 | + | |
| 96 | +Hook nicks follow the pattern `{agent}-{basename}-{session_id[:8]}`: | |
| 97 | + | |
| 98 | +- `claude-scuttlebot-a1b2c3d4` | |
| 99 | +- `gemini-myapp-e5f6a7b8` | |
| 100 | +- `codex-api-9c0d1e2f` | |
| 101 | + | |
| 102 | +The 8-char session ID suffix is extracted from the hook input JSON (`session_id` field for Claude/Codex, `GEMINI_SESSION_ID` env for Gemini, `$PPID` as fallback). This ensures uniqueness across a fleet of agents all working on the same repo — same basename, different session IDs. | |
| 103 | + | |
| 104 | +### Bridge prefix stripping | |
| 105 | +Messages from web UI users arrive via the bridge bot as: | |
| 106 | +``` | |
| 107 | +[realNick] message text | |
| 108 | +``` | |
| 109 | +The agent unwraps this before processing, so `senderNick` is the real web user | |
| 110 | +and `text` is the clean message. The response prefix (`senderNick: reply`) then | |
| 111 | +correctly addresses the human, not the bridge infrastructure nick. | |
| 112 | + | |
| 113 | +### Conversation history | |
| 114 | +Per-conversation history (keyed by channel or DM partner nick) is kept in | |
| 115 | +memory, capped at 20 entries. Older entries are dropped. History is shared | |
| 116 | +across all sessions using the same `convKey` — everyone in a channel sees a | |
| 117 | +single running conversation. | |
| 118 | + | |
| 119 | +### Response format | |
| 120 | +- Channel: `senderNick: first line of reply` (subsequent lines unindented) | |
| 121 | +- DM: plain reply (no prefix) | |
| 122 | +- No markdown, no bold/italic, no code blocks — IRC renders plain text only. | |
| 123 | + | |
| 124 | +--- | |
| 125 | + | |
| 126 | +## Starting the agent | |
| 127 | + | |
| 128 | +### 1. Register the agent in scuttlebot | |
| 129 | +Via the admin UI → Agents → Register Agent, or via API: | |
| 130 | +```bash | |
| 131 | +curl -X POST http://localhost:8080/v1/agents \ | |
| 132 | + -H "Authorization: Bearer $TOKEN" \ | |
| 133 | + -H "Content-Type: application/json" \ | |
| 134 | + -d '{"nick":"claude","type":"worker","channels":["#general"]}' | |
| 135 | +``` | |
| 136 | +The response contains a one-time password. Save it. | |
| 137 | + | |
| 138 | +### 2. Configure an LLM backend (gateway mode) | |
| 139 | +Via admin UI → AI → Add Backend, or in `scuttlebot.yaml`: | |
| 140 | +```yaml | |
| 141 | +llm: | |
| 142 | + backends: | |
| 143 | + - name: anthro | |
| 144 | + backend: anthropic | |
| 145 | + api_key: sk-ant-... | |
| 146 | + model: claude-sonnet-4-6 | |
| 147 | +``` | |
| 148 | + | |
| 149 | +### 3. Launch | |
| 150 | +```bash | |
| 151 | +./claude-agent \ | |
| 152 | + --irc 127.0.0.1:6667 \ | |
| 153 | + --nick claude \ | |
| 154 | + --pass <one-time-password> \ | |
| 155 | + --channels "#general" \ | |
| 156 | + --api-url http://localhost:8080 \ | |
| 157 | + --token $SCUTTLEBOT_TOKEN \ | |
| 158 | + --backend anthro | |
| 159 | +``` | |
| 160 | + | |
| 161 | +Run as a background process or under a process supervisor. | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 165 | +## Shared Go runtime | |
| 166 | + | |
| 167 | +`pkg/ircagent` owns the common IRC agent behavior. `ircagent.Run(ctx, cfg)` | |
| 168 | +blocks until the context is cancelled or the IRC connection fails. | |
| 169 | + | |
| 170 | +Key `Config` fields: | |
| 171 | + | |
| 172 | +| Field | Purpose | Default | | |
| 173 | +|---|---|---| | |
| 174 | +| `IRCAddr` | `host:port` of the Ergo server | — (required) | | |
| 175 | +| `Nick` | IRC nick and SASL username | — (required) | | |
| 176 | +| `Pass` | SASL password | — (required) | | |
| 177 | +| `Channels` | channels to join on connect | `["#general"]` | | |
| 178 | +| `SystemPrompt` | LLM system prompt | — (required) | | |
| 179 | +| `HistoryLen` | per-conversation history cap | 20 | | |
| 180 | +| `TypingDelay` | pause before responding | 400ms | | |
| 181 | +| `ActivityPrefixes` | nick prefixes treated as status logs | `["claude-", "codex-", "gemini-"]` | | |
| 182 | +| `Direct` | direct LLM mode (needs `APIKey`) | nil | | |
| 183 | +| `Gateway` | gateway mode via `/v1/llm/complete` | nil | | |
| 184 | + | |
| 185 | +**Extending `ActivityPrefixes`**: add any prefix whose messages should be | |
| 186 | +observed (added to history for context) but never trigger a reply. E.g. adding | |
| 187 | +`"sentinel-"` means sentinel bots shout into the void without getting an answer. | |
| 188 | + | |
| 189 | +The two binaries in `cmd/` differ only in defaults: system prompt, direct | |
| 190 | +backend name (`anthropic` vs `openai`), and gateway backend default | |
| 191 | +(`anthro` vs `openai`). | |
| 192 | + | |
| 193 | +## Porting to another language | |
| 194 | + | |
| 195 | +The agent needs three things: | |
| 196 | + | |
| 197 | +1. **IRC connection with SASL PLAIN** — connect to port 6667, auth with nick+pass. | |
| 198 | + Any IRC library works: python-ircclient, node-irc, etc. | |
| 199 | + | |
| 200 | +2. **Message handler** — on PRIVMSG: | |
| 201 | + - Strip `[realNick] ` prefix if present (bridge messages) | |
| 202 | + - Skip if sender starts with an activity prefix like `claude-`, `codex-`, or `gemini-` | |
| 203 | + - Check for mention (word boundary) or DM | |
| 204 | + - Build prompt from history + message | |
| 205 | + - Call LLM (direct or gateway) | |
| 206 | + - Reply to channel/sender | |
| 207 | + | |
| 208 | +3. **LLM call** — either direct to provider API, or: | |
| 209 | + ```http | |
| 210 | + POST /v1/llm/complete | |
| 211 | + Authorization: Bearer <token> | |
| 212 | + Content-Type: application/json | |
| 213 | + | |
| 214 | + {"backend": "anthro", "prompt": "...full conversation prompt..."} | |
| 215 | + ``` | |
| 216 | + Returns `{"text": "..."}`. | |
| 217 | + | |
| 218 | +### Python sketch | |
| 219 | +```python | |
| 220 | +import irc.client | |
| 221 | +import requests | |
| 222 | + | |
| 223 | +def on_pubmsg(conn, event): | |
| 224 | + sender = event.source.nick | |
| 225 | + text = event.arguments[0] | |
| 226 | + | |
| 227 | + # Unwrap bridge prefix | |
| 228 | + if text.startswith("[") and "] " in text: | |
| 229 | + sender = text[1:text.index("] ")] | |
| 230 | + text = text[text.index("] ")+2:] | |
| 231 | + | |
| 232 | + # Skip activity posts | |
| 233 | + if sender.startswith("claude-") or sender.startswith("codex-") or sender.startswith("gemini-"): | |
| 234 | + return | |
| 235 | + | |
| 236 | + # Only respond when mentioned | |
| 237 | + if "claude" not in text.lower().split(): | |
| 238 | + return | |
| 239 | + | |
| 240 | + reply = gateway_complete(text) | |
| 241 | + conn.privmsg(event.target, f"{sender}: {reply}") | |
| 242 | + | |
| 243 | +def gateway_complete(prompt): | |
| 244 | + r = requests.post( | |
| 245 | + "http://localhost:8080/v1/llm/complete", | |
| 246 | + headers={"Authorization": f"Bearer {TOKEN}"}, | |
| 247 | + json={"backend": "anthro", "prompt": prompt}, | |
| 248 | + timeout=60, | |
| 249 | + ) | |
| 250 | + return r.json()["text"] | |
| 251 | +``` | |
| 252 | + | |
| 253 | +--- | |
| 254 | + | |
| 255 | +## Operational notes | |
| 256 | + | |
| 257 | +- The agent holds all history in memory. Restart clears it. | |
| 258 | +- One agent instance per nick. Multiple instances with the same nick will fight | |
| 259 | + over the SASL registration. | |
| 260 | +- The `--backend` name must match a backend registered in scuttlebot's LLM | |
| 261 | + config. If the backend isn't configured, responses fail with a gateway error. | |
| 262 | +- If the LLM is slow, |
| --- a/skills/irc-agent/README.md | |
| +++ b/skills/irc-agent/README.md | |
| @@ -0,0 +1,262 @@ | |
| --- a/skills/irc-agent/README.md | |
| +++ b/skills/irc-agent/README.md | |
| @@ -0,0 +1,262 @@ | |
| 1 | # Building an IRC agent on scuttlebot |
| 2 | |
| 3 | How to connect any agent — LLM-powered chat bot, task runner, monitoring agent, |
| 4 | or anything else — to scuttlebot's IRC backplane. Language-agnostic. The Go |
| 5 | reference runtime in this repo is `pkg/ircagent`; `cmd/claude-agent`, |
| 6 | `cmd/codex-agent`, and `cmd/gemini-agent` are thin wrappers with different defaults. |
| 7 | |
| 8 | This document is for IRC-resident agents. Live terminal runtimes such as |
| 9 | `codex-relay` use a different pattern: a broker owns session presence, |
| 10 | continuous operator input injection, and outbound activity mirroring while the |
| 11 | runtime stays local.TTskills/{runtime}-relay/`. |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## What scuttlebot gives you |
| 16 | |
| 17 | - An Ergo IRC server with NickServ account-per-agent (SASL auth) |
| 18 | - A bridge bot that relays web UI messages into IRC and back |
| 19 | - An HTTP API for agent registration, credential management, and LLM proxying |
| 20 | - Human-observable coordination: everything that happens is visible in IRC |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## Architecture |
| 25 | |
| 26 | ``` |
| 27 | Web UI / IRC client |
| 28 | │ |
| 29 | ▼ |
| 30 | scuttlebot (bridge bot) |
| 31 | │ PRIVMSG via girc |
| 32 | ▼ |
| 33 | Ergo IRC server (6667) |
| 34 | │ PRIVMSG event |
| 35 | ▼ |
| 36 | claude-agent / codex-agent |
| 37 | │ pkg/ircagent.Run(...) |
| 38 | │ buildPrompt() → completer.complete() |
| 39 | ▼ |
| 40 | LLM (direct or gateway) |
| 41 | │ reply text |
| 42 | ▼ |
| 43 | claude-agent → cl.Cmd.Message(channel, reply) |
| 44 | │ |
| 45 | ▼ |
| 46 | Ergo → bridge PRIVMSG → web UI renders it |
| 47 | ``` |
| 48 | |
| 49 | ### Two operation modes |
| 50 | |
| 51 | **Direct mode** — the agent calls the LLM provider directly. Needs the API key: |
| 52 | ``` |
| 53 | ./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> --api-key sk-ant-... |
| 54 | ``` |
| 55 | |
| 56 | **Gateway mode** — proxies through scuttlebot's `/v1/llm/complete` endpoint. |
| 57 | The key never leaves the server. Preferred for production: |
| 58 | ``` |
| 59 | |
| 60 | ### IRC-resident agent vs terminal-session broker |
| 61 | |
| 62 | - IRC-resident agent: logs into Ergo directly, lives in-channel, responds like a bot |
| 63 | - terminal-session broker: wraps a local tool loop, posts `online` / `offline`, |
| 64 | mirrors session activity, and injects addressed operator messages back into the |
| 65 | live terminal session |
| 66 | |
| 67 | Use `pkg/ircagent` when the process itself should be an IRC user. Use a broker |
| 68 | such as `cmd/codex-relay` when the process should remain a local interactive |
| 69 | session but still be operator-addressable from IRC. |
| 70 | ./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> \ |
| 71 | --api-url http://localhost:8080 --token <bearer> --backend anthro |
| 72 | ``` |
| 73 | |
| 74 | --- |
| 75 | |
| 76 | ## Key design decisions |
| 77 | |
| 78 | ### Nick registration |
| 79 | The agent's IRC nick must be pre-registered as a NickServ account (scuttlebot |
| 80 | does this when you register an agent via the UI or API). The agent authenticates |
| 81 | via SASL PLAIN on connect. |
| 82 | |
| 83 | ### Message routing |
| 84 | - **Channel messages**: the agent only responds when its nick is mentioned. |
| 85 | Mention detection uses word-boundary matching. Adjacent characters that |
| 86 | suppress a match: letters, digits, `-`, `_`, `.`, `/`, `\`. This means |
| 87 | `.claude/hooks/` does NOT trigger a response, but neither does `claude.` |
| 88 | at the end of a sentence. Address the agent with `claude:` or `claude,`. |
| 89 | - **DMs**: the agent always responds. |
| 90 | - **activity-post senders**: hook/session nicks like `claude-*` and |
| 91 | `codex-*` are silently observed (added to history) but never responded to. |
| 92 | They're status logs, not chat. |
| 93 | |
| 94 | ### Session nick format |
| 95 | |
| 96 | Hook nicks follow the pattern `{agent}-{basename}-{session_id[:8]}`: |
| 97 | |
| 98 | - `claude-scuttlebot-a1b2c3d4` |
| 99 | - `gemini-myapp-e5f6a7b8` |
| 100 | - `codex-api-9c0d1e2f` |
| 101 | |
| 102 | The 8-char session ID suffix is extracted from the hook input JSON (`session_id` field for Claude/Codex, `GEMINI_SESSION_ID` env for Gemini, `$PPID` as fallback). This ensures uniqueness across a fleet of agents all working on the same repo — same basename, different session IDs. |
| 103 | |
| 104 | ### Bridge prefix stripping |
| 105 | Messages from web UI users arrive via the bridge bot as: |
| 106 | ``` |
| 107 | [realNick] message text |
| 108 | ``` |
| 109 | The agent unwraps this before processing, so `senderNick` is the real web user |
| 110 | and `text` is the clean message. The response prefix (`senderNick: reply`) then |
| 111 | correctly addresses the human, not the bridge infrastructure nick. |
| 112 | |
| 113 | ### Conversation history |
| 114 | Per-conversation history (keyed by channel or DM partner nick) is kept in |
| 115 | memory, capped at 20 entries. Older entries are dropped. History is shared |
| 116 | across all sessions using the same `convKey` — everyone in a channel sees a |
| 117 | single running conversation. |
| 118 | |
| 119 | ### Response format |
| 120 | - Channel: `senderNick: first line of reply` (subsequent lines unindented) |
| 121 | - DM: plain reply (no prefix) |
| 122 | - No markdown, no bold/italic, no code blocks — IRC renders plain text only. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## Starting the agent |
| 127 | |
| 128 | ### 1. Register the agent in scuttlebot |
| 129 | Via the admin UI → Agents → Register Agent, or via API: |
| 130 | ```bash |
| 131 | curl -X POST http://localhost:8080/v1/agents \ |
| 132 | -H "Authorization: Bearer $TOKEN" \ |
| 133 | -H "Content-Type: application/json" \ |
| 134 | -d '{"nick":"claude","type":"worker","channels":["#general"]}' |
| 135 | ``` |
| 136 | The response contains a one-time password. Save it. |
| 137 | |
| 138 | ### 2. Configure an LLM backend (gateway mode) |
| 139 | Via admin UI → AI → Add Backend, or in `scuttlebot.yaml`: |
| 140 | ```yaml |
| 141 | llm: |
| 142 | backends: |
| 143 | - name: anthro |
| 144 | backend: anthropic |
| 145 | api_key: sk-ant-... |
| 146 | model: claude-sonnet-4-6 |
| 147 | ``` |
| 148 | |
| 149 | ### 3. Launch |
| 150 | ```bash |
| 151 | ./claude-agent \ |
| 152 | --irc 127.0.0.1:6667 \ |
| 153 | --nick claude \ |
| 154 | --pass <one-time-password> \ |
| 155 | --channels "#general" \ |
| 156 | --api-url http://localhost:8080 \ |
| 157 | --token $SCUTTLEBOT_TOKEN \ |
| 158 | --backend anthro |
| 159 | ``` |
| 160 | |
| 161 | Run as a background process or under a process supervisor. |
| 162 | |
| 163 | --- |
| 164 | |
| 165 | ## Shared Go runtime |
| 166 | |
| 167 | `pkg/ircagent` owns the common IRC agent behavior. `ircagent.Run(ctx, cfg)` |
| 168 | blocks until the context is cancelled or the IRC connection fails. |
| 169 | |
| 170 | Key `Config` fields: |
| 171 | |
| 172 | | Field | Purpose | Default | |
| 173 | |---|---|---| |
| 174 | | `IRCAddr` | `host:port` of the Ergo server | — (required) | |
| 175 | | `Nick` | IRC nick and SASL username | — (required) | |
| 176 | | `Pass` | SASL password | — (required) | |
| 177 | | `Channels` | channels to join on connect | `["#general"]` | |
| 178 | | `SystemPrompt` | LLM system prompt | — (required) | |
| 179 | | `HistoryLen` | per-conversation history cap | 20 | |
| 180 | | `TypingDelay` | pause before responding | 400ms | |
| 181 | | `ActivityPrefixes` | nick prefixes treated as status logs | `["claude-", "codex-", "gemini-"]` | |
| 182 | | `Direct` | direct LLM mode (needs `APIKey`) | nil | |
| 183 | | `Gateway` | gateway mode via `/v1/llm/complete` | nil | |
| 184 | |
| 185 | **Extending `ActivityPrefixes`**: add any prefix whose messages should be |
| 186 | observed (added to history for context) but never trigger a reply. E.g. adding |
| 187 | `"sentinel-"` means sentinel bots shout into the void without getting an answer. |
| 188 | |
| 189 | The two binaries in `cmd/` differ only in defaults: system prompt, direct |
| 190 | backend name (`anthropic` vs `openai`), and gateway backend default |
| 191 | (`anthro` vs `openai`). |
| 192 | |
| 193 | ## Porting to another language |
| 194 | |
| 195 | The agent needs three things: |
| 196 | |
| 197 | 1. **IRC connection with SASL PLAIN** — connect to port 6667, auth with nick+pass. |
| 198 | Any IRC library works: python-ircclient, node-irc, etc. |
| 199 | |
| 200 | 2. **Message handler** — on PRIVMSG: |
| 201 | - Strip `[realNick] ` prefix if present (bridge messages) |
| 202 | - Skip if sender starts with an activity prefix like `claude-`, `codex-`, or `gemini-` |
| 203 | - Check for mention (word boundary) or DM |
| 204 | - Build prompt from history + message |
| 205 | - Call LLM (direct or gateway) |
| 206 | - Reply to channel/sender |
| 207 | |
| 208 | 3. **LLM call** — either direct to provider API, or: |
| 209 | ```http |
| 210 | POST /v1/llm/complete |
| 211 | Authorization: Bearer <token> |
| 212 | Content-Type: application/json |
| 213 | |
| 214 | {"backend": "anthro", "prompt": "...full conversation prompt..."} |
| 215 | ``` |
| 216 | Returns `{"text": "..."}`. |
| 217 | |
| 218 | ### Python sketch |
| 219 | ```python |
| 220 | import irc.client |
| 221 | import requests |
| 222 | |
| 223 | def on_pubmsg(conn, event): |
| 224 | sender = event.source.nick |
| 225 | text = event.arguments[0] |
| 226 | |
| 227 | # Unwrap bridge prefix |
| 228 | if text.startswith("[") and "] " in text: |
| 229 | sender = text[1:text.index("] ")] |
| 230 | text = text[text.index("] ")+2:] |
| 231 | |
| 232 | # Skip activity posts |
| 233 | if sender.startswith("claude-") or sender.startswith("codex-") or sender.startswith("gemini-"): |
| 234 | return |
| 235 | |
| 236 | # Only respond when mentioned |
| 237 | if "claude" not in text.lower().split(): |
| 238 | return |
| 239 | |
| 240 | reply = gateway_complete(text) |
| 241 | conn.privmsg(event.target, f"{sender}: {reply}") |
| 242 | |
| 243 | def gateway_complete(prompt): |
| 244 | r = requests.post( |
| 245 | "http://localhost:8080/v1/llm/complete", |
| 246 | headers={"Authorization": f"Bearer {TOKEN}"}, |
| 247 | json={"backend": "anthro", "prompt": prompt}, |
| 248 | timeout=60, |
| 249 | ) |
| 250 | return r.json()["text"] |
| 251 | ``` |
| 252 | |
| 253 | --- |
| 254 | |
| 255 | ## Operational notes |
| 256 | |
| 257 | - The agent holds all history in memory. Restart clears it. |
| 258 | - One agent instance per nick. Multiple instances with the same nick will fight |
| 259 | over the SASL registration. |
| 260 | - The `--backend` name must match a backend registered in scuttlebot's LLM |
| 261 | config. If the backend isn't configured, responses fail with a gateway error. |
| 262 | - If the LLM is slow, |
| --- a/skills/openai-relay/FLEET.md | ||
| +++ b/skills/openai-relay/FLEET.md | ||
| @@ -0,0 +1,84 @@ | ||
| 1 | +# Codex Relay Fleet Launch | |
| 2 | + | |
| 3 | +This is the rollout guide for making local Codex terminal sessions IRC-visible and | |
| 4 | +operator-addressable through scuttlebot. | |
| 5 | + | |
| 6 | +-relay/ADDING_AGENTS.md). | |
| 7 | + | |
| 8 | +Source of truth: | |
| 9 | +- installer: [`scripts/install-codex-relay.sh`](scripts/install-codex-relay.sh) | |
| 10 | +- broker: [`../../cmd/codex-relay/main.go`](../../../../pkg/sessionrelay/) | |
| 11 | +- dev wrapper: [`scripts/codex-relay.sh`](scripts/codex-relay.sh) | |
| 12 | +- hooks: [`hooks/scuttlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) | |
| 13 | +- runtime docs: [`install.md`](install.md), [`hooks/READshared runtime contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) | |
| 14 | + | |
| 15 | +Installed files under `~/.codex/`, `~/.local/bin/`, and `~/.config/` are generated | |
| 16 | +copies. Point other engineers and agents at the repo docs and installer, not at one | |
| 17 | +person's home directory. | |
| 18 | + | |
| 19 | +Runtime prerequisites: | |
| 20 | +- `codex` | |
| 21 | +- `go` | |
| 22 | +- `curl` | |
| 23 | +- `jq` | |
| 24 | + | |
| 25 | +## patibility layer | |
| 26 | + | |
| 27 | +## What this gives you | |
| 28 | + | |
| 29 | +For each local Codex session launched through `codex-relay`: | |
| 30 | +- a stable nick: `codex-{repo}-{session}` | |
| 31 | +- immediate `online` post when the session starts | |
| 32 | +- mirrored tool activity from the active session log | |
| 33 | +- mirrored assistant messages from the active session log | |
| 34 | +- continuous addressed IRC input injection into the live terminal session | |
| 35 | +- explicit pre-tool fallback interrupts be for real presence | |
| 36 | + | |
| 37 | +This is the production control path for a human-operated Codex terminal. If you | |
| 38 | +want an always-on IRC-resident bot instead, use `cmd/codex-agent`. | |
| 39 | + | |
| 40 | +## One-machine install | |
| 41 | + | |
| 42 | +Run from glengoolie: codex-scuttlebot-a1b2c3d4 glengoolie: codex-scuttlebot-a1b2c3d4 Codex Relay Fleet Launch | |
| 43 | + | |
| 44 | +This is the rollout guide for making local Codex terminal sessions IRC-visible and | |
| 45 | +operator-addressable through scuttlebot. | |
| 46 | + | |
| 47 | +Codex and Gemini are the canonical terminal-broker reference implementations in | |
| 48 | +this repo. The normative path and convention contract lives in | |
| 49 | +[`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md). | |
| 50 | + | |
| 51 | +Source of truth: | |
| 52 | +- installer: [`scripts/install-codex-relay.sh`](scripts/install-codex-relay.sh) | |
| 53 | +- broker: [`../../cmd/codex-relay/m under `~/.codex/`, `~/.local/bin/`, and `~/.config/` are generated | |
| 54 | +copies. Point other engineers and agents at the repo docs and installer, not at one | |
| 55 | +person's home directory. | |
| 56 | + | |
| 57 | +Runtime prerequisites: | |
| 58 | +- `codex` | |
| 59 | +- `go` | |
| 60 | +- `curl` | |
| 61 | +- `jq` | |
| 62 | + | |
| 63 | +## patibility layer | |
| 64 | + | |
| 65 | +## What this gives you | |
| 66 | + | |
| 67 | +For each local Codex session launched through `codex-relay`: | |
| 68 | +- a stable nick: `codex-{repo}-{session}` | |
| 69 | +- immediate `online` post when the session starts | |
| 70 | +- mirrored tool activity from the active session log | |
| 71 | +- mirrored assistant messages from the active session log | |
| 72 | +- continuous addressed IRC input injection into the live terminal session | |
| 73 | +- explicit pre-tool fallback interrupts before the next action | |
| 74 | +- `offline` post on exit | |
| 75 | + | |
| 76 | +Transport choice: | |
| 77 | +- `SCUTTLEBOT_TRANSPORT=http` keeps the bridge/API path and now uses presence heartbeats | |
| 78 | +- `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence | |
| 79 | + | |
| 80 | +This is the proor-addressable through scuttlebot. | |
| 81 | + | |
| 82 | +Codex and Gemini are the canonical terminal-broker reference implementations in | |
| 83 | +this repo. The normative path and convention contract lives in | |
| 84 | +[`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| --- a/skills/openai-relay/FLEET.md | |
| +++ b/skills/openai-relay/FLEET.md | |
| @@ -0,0 +1,84 @@ | |
| --- a/skills/openai-relay/FLEET.md | |
| +++ b/skills/openai-relay/FLEET.md | |
| @@ -0,0 +1,84 @@ | |
| 1 | # Codex Relay Fleet Launch |
| 2 | |
| 3 | This is the rollout guide for making local Codex terminal sessions IRC-visible and |
| 4 | operator-addressable through scuttlebot. |
| 5 | |
| 6 | -relay/ADDING_AGENTS.md). |
| 7 | |
| 8 | Source of truth: |
| 9 | - installer: [`scripts/install-codex-relay.sh`](scripts/install-codex-relay.sh) |
| 10 | - broker: [`../../cmd/codex-relay/main.go`](../../../../pkg/sessionrelay/) |
| 11 | - dev wrapper: [`scripts/codex-relay.sh`](scripts/codex-relay.sh) |
| 12 | - hooks: [`hooks/scuttlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) |
| 13 | - runtime docs: [`install.md`](install.md), [`hooks/READshared runtime contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| 14 | |
| 15 | Installed files under `~/.codex/`, `~/.local/bin/`, and `~/.config/` are generated |
| 16 | copies. Point other engineers and agents at the repo docs and installer, not at one |
| 17 | person's home directory. |
| 18 | |
| 19 | Runtime prerequisites: |
| 20 | - `codex` |
| 21 | - `go` |
| 22 | - `curl` |
| 23 | - `jq` |
| 24 | |
| 25 | ## patibility layer |
| 26 | |
| 27 | ## What this gives you |
| 28 | |
| 29 | For each local Codex session launched through `codex-relay`: |
| 30 | - a stable nick: `codex-{repo}-{session}` |
| 31 | - immediate `online` post when the session starts |
| 32 | - mirrored tool activity from the active session log |
| 33 | - mirrored assistant messages from the active session log |
| 34 | - continuous addressed IRC input injection into the live terminal session |
| 35 | - explicit pre-tool fallback interrupts be for real presence |
| 36 | |
| 37 | This is the production control path for a human-operated Codex terminal. If you |
| 38 | want an always-on IRC-resident bot instead, use `cmd/codex-agent`. |
| 39 | |
| 40 | ## One-machine install |
| 41 | |
| 42 | Run from glengoolie: codex-scuttlebot-a1b2c3d4 glengoolie: codex-scuttlebot-a1b2c3d4 Codex Relay Fleet Launch |
| 43 | |
| 44 | This is the rollout guide for making local Codex terminal sessions IRC-visible and |
| 45 | operator-addressable through scuttlebot. |
| 46 | |
| 47 | Codex and Gemini are the canonical terminal-broker reference implementations in |
| 48 | this repo. The normative path and convention contract lives in |
| 49 | [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md). |
| 50 | |
| 51 | Source of truth: |
| 52 | - installer: [`scripts/install-codex-relay.sh`](scripts/install-codex-relay.sh) |
| 53 | - broker: [`../../cmd/codex-relay/m under `~/.codex/`, `~/.local/bin/`, and `~/.config/` are generated |
| 54 | copies. Point other engineers and agents at the repo docs and installer, not at one |
| 55 | person's home directory. |
| 56 | |
| 57 | Runtime prerequisites: |
| 58 | - `codex` |
| 59 | - `go` |
| 60 | - `curl` |
| 61 | - `jq` |
| 62 | |
| 63 | ## patibility layer |
| 64 | |
| 65 | ## What this gives you |
| 66 | |
| 67 | For each local Codex session launched through `codex-relay`: |
| 68 | - a stable nick: `codex-{repo}-{session}` |
| 69 | - immediate `online` post when the session starts |
| 70 | - mirrored tool activity from the active session log |
| 71 | - mirrored assistant messages from the active session log |
| 72 | - continuous addressed IRC input injection into the live terminal session |
| 73 | - explicit pre-tool fallback interrupts before the next action |
| 74 | - `offline` post on exit |
| 75 | |
| 76 | Transport choice: |
| 77 | - `SCUTTLEBOT_TRANSPORT=http` keeps the bridge/API path and now uses presence heartbeats |
| 78 | - `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence |
| 79 | |
| 80 | This is the proor-addressable through scuttlebot. |
| 81 | |
| 82 | Codex and Gemini are the canonical terminal-broker reference implementations in |
| 83 | this repo. The normative path and convention contract lives in |
| 84 | [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
+287
| --- a/skills/openai-relay/SKILL.md | ||
| +++ b/skills/openai-relay/SKILL.md | ||
| @@ -0,0 +1,287 @@ | ||
| 1 | +--- | |
| 2 | +name: openai-relay | |
| 3 | +description: Bidirectional OpenAI agent integration for scuttlebot. Primary local path: run the compiled `cmd/codex-relay` broker plus native Codex hooks so a live Codex terminal session appears in IRC immediately, streams tool activity, and accepts addressed operator instructions continuously. Secondary path: run the Go `codex-agent` IRC client for an autonomous IRC-resident agent. Use when wiring Codex or other OpenAI-based agents into scuttlebot locally or over the internet. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# OpenAI Relay | |
| 7 | + | |
| 8 | +There are two production paths: | |
| 9 | +- local Codex terminal session: `cmd/codex-relay` | |
| 10 | +- IRC-resident autonomous agent: `cmd/codex-agent` | |
| 11 | + | |
| 12 | +Use the broker path when you want the local Codex terminal to show up in IRC as | |
| 13 | +soon as it starts, post `online`/`offline` presence, stream per-tool activity via | |
| 14 | +hooks, and accept addressed instructions continuously while the session is running. | |
| 15 | + | |
| 16 | +Source-of-truth files in the repo: | |
| 17 | +- installer: `skills/openai-relay/scripts/install-codex-relay.sh` | |
| 18 | +- broker: `cmd/codex-relay/main.go` | |
| 19 | +- -relay | |
| 20 | +description: Bidirectional OpenAI agent integration for scuttlebot. Primary local path: run the compiled `cmd/codex-relay` broker plus native Codex hooks so a live Codex terminal session appears in IRC imm--- | |
| 21 | +name: openai-relay | |
| 22 | +description: Bidirectional OpenAI agent integration fornfig` are copies. | |
| 23 | + | |
| 24 | +## Setup | |
| 25 | +- Export gateway env vars: | |
| 26 | + - `SCUTTLEBOT_URL` e.g. `http://localhost:8080` | |
| 27 | + - `SCUTTLEBOT_TOKEN` bearer token | |
| 28 | +- Ensure the daemon has an `openai` backend configured. | |
| 29 | +- Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. | |
| 30 | + | |
| 31 | +## Preferred For Local Codex CLI: codex-relay broker | |
| 32 | +Installer-first path: | |
| 33 | + | |
| 34 | +```bash | |
| 35 | +bash skills/openai-relay/scripts/install-codex-relay.sh \ | |
| 36 | + --url http://localhost:8080 \ | |
| 37 | + --token "$(./run.sh token)" \ | |
| 38 | + --channel general | |
| 39 | +``` | |
| 40 | + | |
| 41 | +Then launch: | |
| 42 | + | |
| 43 | +```bash | |
| 44 | +~/.local/bin/codex-relay | |
| 45 | +``` | |
| 46 | + | |
| 47 | +Manual install and launch: | |
| 48 | +```bash | |
| 49 | +mkdir -p ~/.codex/hooks ~/.local/bin | |
| 50 | +cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ | |
| 51 | +cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ | |
| 52 | +go build -o ~/.local/bin/codex-relay ./cmd/codex-relay | |
| 53 | +chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay | |
| 54 | +``` | |
| 55 | + | |
| 56 | +Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then: | |
| 57 | + | |
| 58 | +```bash | |
| 59 | +~/.local/bin/codex-relay | |
| 60 | +``` | |
| 61 | + | |
| 62 | +Behavior: | |
| 63 | +- export a stable `SCUTTLEBOT_SESSION_ID` | |
| 64 | +- derive a stable `codex-{basename}-{session}` nick | |
| 65 | +- post `online ...` immediately when Codex starts | |
| 66 | +- post `offline ...` when Codex exits | |
| 67 | +- continuously inject addressed IRC messages into the live Codex terminal | |
| 68 | +- mirror assistant output and tool activity from the active session log | |
| 69 | +- use `pkg/sessionrelay` for both `http` and `irc` transport modes | |
| 70 | +- let the existing hooks remain the pre-tool fallback path | |
| 71 | + | |
| 72 | +token | |
| 73 | +- Ensure the daemon has an `openai` backend configured. | |
| 74 | +- Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. | |
| 75 | + | |
| 76 | +## Preferred For Local Codex CLI: codex-relay broker | |
| 77 | +Installer-first path: | |
| 78 | + | |
| 79 | +```bash | |
| 80 | +bash skills/openai-relay/scripts/install-codex-relay.sh \ | |
| 81 | + --url http://localhost:8080 \ | |
| 82 | + --token "$(./run.sh token)" \ | |
| 83 | + --channel general | |
| 84 | +``` | |
| 85 | + | |
| 86 | +Then launch: | |
| 87 | + | |
| 88 | +```bash | |
| 89 | +~/.local/bin/codex-relay | |
| 90 | +``` | |
| 91 | + | |
| 92 | +Manual install and launch: | |
| 93 | +```bash | |
| 94 | +mkdir -p ~/.codex/hooks ~/.local/bin | |
| 95 | +cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ | |
| 96 | +cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ | |
| 97 | +go build -o ~/.local/bin/codex-relay ./cmd/codex-relay | |
| 98 | +chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay | |
| 99 | +``` | |
| 100 | + | |
| 101 | +Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then: | |
| 102 | + | |
| 103 | +```bash | |
| 104 | +~/.local/bin/codex-relay | |
| 105 | +``` | |
| 106 | + | |
| 107 | +Behavior: | |
| 108 | +- export a stable `SCUTTLEBOT_SESSION_ID` | |
| 109 | +- derive a stable `codex-{basename}-{session}` nick | |
| 110 | +- post `online ...` immediately when Codex starts | |
| 111 | +- post `offline ...` when Codex exits | |
| 112 | +- continuously inject addressed IRC messages into the live Codex terminal | |
| 113 | +- mirror assistant output and tool activity from the active session log | |
| 114 | +- use `pkg/sessionrelay` for both `http` and `irc` transport modes | |
| 115 | +- let the existing hooks remain the pre-tool fallback path | |
| 116 | + | |
| 117 | +Canonical pattern summary: | |
| 118 | +- broker entrypoint: `cmd/codex-relay/main.go` | |
| 119 | +- tracked installer: `skills/openai-relay/scripts/install-codex-relay.sh` | |
| 120 | +- runtime docs: `skills/openai-relay/install.md` and `skills/openai-relay/FLEET.md` | |
| 121 | +- hooks: `skills/openai-relay/hooks/` | |
| 122 | +- shared transport: `pkg/sessionrelay/` | |
| 123 | + | |
| 124 | +Transport modes: | |
| 125 | +- `SCUTTLEBOT_TRANSPORT=http` uses the working HTTP bridge path and presence heartbeats | |
| 126 | +- `SCUTTLEBOT_TRANSPORT=irc` connects the live session nick directly to Ergo over SASL | |
| 127 | +- in `irc` mode, `SCUTTLEBOT_IRC_PASS` uses a fixed NickServ password; otherwise the broker auto-registers the ephemeral session nick through `/v1/agents/register` and deletes it on clean exit by default | |
| 128 | + | |
| 129 | +To disable the relay without uninstalling: | |
| 130 | + | |
| 131 | +```bash | |
| 132 | +SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/codex-relay | |
| 133 | +``` | |
| 134 | + | |
| 135 | +Optional shell alias: | |
| 136 | +```bash | |
| 137 | +alias codex="$HOME/.local/bin/codex-relay" | |
| 138 | +``` | |
| 139 | + | |
| 140 | +## Preferred For IRC-Resident Agents: Go codex-agent | |
| 141 | +Build and run: | |
| 142 | +```bash | |
| 143 | +go build -o bin/codex-agent ./cmd/codex-agent | |
| 144 | +bin/codex-agent \ | |
| 145 | + --irc 127.0.0.1:6667 \ | |
| 146 | + --nick codex-1234 \ | |
| 147 | + --pass <nickserv-passphrase> \ | |
| 148 | + --channels "#general" \ | |
| 149 | + --api-url "$SCUTTLEBOT_URL" \ | |
| 150 | + --token "$SCUTTLEBOT_TOKEN" \ | |
| 151 | + --backend openai | |
| 152 | +``` | |
| 153 | + | |
| 154 | +Register a new nick via HTTP: | |
| 155 | +```bash | |
| 156 | +curl -X POST "$SCUTTLEBOT_URL/v1/agents/register" \ | |
| 157 | + -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ | |
| 158 | + -H "Content-Type: application/json" \ | |
| 159 | + -d '{"nick":"codex-1234","type":"worker","channels":["#general"]}' | |
| 160 | +``` | |
| 161 | + | |
| 162 | +Behavior: | |
| 163 | +- connect to Ergo using SASL | |
| 164 | +- join configured channels | |
| 165 | +- respond to DMs or messages that mention the agent nick | |
| 166 | +- keep short in-memory conversation history per channel/DM | |
| 167 | +- call scuttlebot's `/v1/llm/complete` with backend `openai` | |
| 168 | + | |
| 169 | +## Direct mode | |
| 170 | +Use direct mode only if you want the agent to call OpenAI itself instead of the daemon gateway: | |
| 171 | +```bash | |
| 172 | +OPENAI_API_KEY=... \ | |
| 173 | +bin/codex-agent \ | |
| 174 | + --irc 127.0.0.1:6667 \ | |
| 175 | + --nick codex-1234 \ | |
| 176 | + --pass <nickserv-passphrase> \ | |
| 177 | + --channels "#general" \ | |
| 178 | + --api-key "$OPENAI_API_KEY" \ | |
| 179 | + --model gpt-5.4-mini | |
| 180 | +``` | |
| 181 | + | |
| 182 | +## Hook-based operator control | |
| 183 | +If you want operator instructions to feed back into a live Codex tool loop before | |
| 184 | +the next action, install the shell hooks in `skills/openai-relay/hooks/`. | |
| 185 | +For immediate startup presence plus continuous IRC input injection, launch through | |
| 186 | +the compiled `cmd/codex-relay` broker installed as `~/.local/bin/codex-relay`. | |
| 187 | + | |
| 188 | +- `scuttlebot-post.sh` posts one-line activity after each tool call | |
| 189 | +- `scuttlebot-check.sh` checks the channel before the next action | |
| 190 | +- `cmd/codex-relay` posts `online` at session start, injects addressed IRC messages into the live PTY, and posts `offline` on exit | |
| 191 | +- only messages that explicitly mention the session nick block the loop | |
| 192 | +- default session nick format is `codex-{basename}-{session}` unless you override | |
| 193 | + `SCUTTLEBOT_NICK` | |
| 194 | + | |
| 195 | +Install: | |
| 196 | +```bash | |
| 197 | +mkdir -p ~/.codex/hooks | |
| 198 | +cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ | |
| 199 | +cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ | |
| 200 | +chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh | |
| 201 | +``` | |
| 202 | + | |
| 203 | +Config in `~/.codex/hooks.json`: | |
| 204 | +```json | |
| 205 | +{ | |
| 206 | + "hooks": { | |
| 207 | + "pre-tool-use": [ | |
| 208 | + { | |
| 209 | + "matcher": "Bash|Edit|Write", | |
| 210 | + "hooks": [ | |
| 211 | + { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-check.sh" } | |
| 212 | + ] | |
| 213 | + } | |
| 214 | + ], | |
| 215 | + "post-tool-use": [ | |
| 216 | + { | |
| 217 | + "matcher": "Bash|Read|Edit|Write|Glob|Grep|Agent", | |
| 218 | + "hooks": [ | |
| 219 | + { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" } | |
| 220 | + ] | |
| 221 | + } | |
| 222 | + ] | |
| 223 | + } | |
| 224 | +} | |
| 225 | +``` | |
| 226 | + | |
| 227 | +Enable the feature in `~/.codex/config.toml`: | |
| 228 | +```toml | |
| 229 | +[features] | |
| 230 | +codex_hooks = true | |
| 231 | +``` | |
| 232 | + | |
| 233 | +Required env: | |
| 234 | +- `SCUTTLEBOT_URL` | |
| 235 | +- `SCUTTLEBOT_TOKEN` | |
| 236 | +- `SCUTTLEBOT_CHANNEL` | |
| 237 | + | |
| 238 | +The hooks also auto-load `~/.config/scuttlebot-relay.env` if present. | |
| 239 | + | |
| 240 | +For fleet rollout instructions, see `skills/openai-relay/FLEET.md`. | |
| 241 | + | |
| 242 | +## Lightweight HTTP relay examples | |
| 243 | +Use these only when you need custom status/poll integrations without the shell | |
| 244 | +hooks or a full IRC client. The shipped scripts in `skills/openai-relay/scripts/` | |
| 245 | +already implement stable session nicks and mention-targeted polling; treat the | |
| 246 | +inline snippets below as transport illustrations. | |
| 247 | + | |
| 248 | +### Node 18+ | |
| 249 | +```js | |
| 250 | +import OpenAI from "openai"; | |
| 251 | + | |
| 252 | +const cfg = { | |
| 253 | + url: process.env.SCUTTLEBOT_URL, | |
| 254 | + token: process.env.SCUTTLEBOT_TOKEN, | |
| 255 | + channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""), | |
| 256 | + nick: process.env.SCUTTLEBOT_NICK || "codex", | |
| 257 | + model: process.env.OPENAI_MODEL || "gpt-4.1-mini", | |
| 258 | + backend: process.env.SCUTTLEBOT_LLM_BACKEND, // optional: use daemon-stored key | |
| 259 | +}; | |
| 260 | + | |
| 261 | +const openai = cfg.backend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); | |
| 262 | +let lastCheck = 0; | |
| 263 | + | |
| 264 | +async function relayPost(text) { | |
| 265 | + await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { | |
| 266 | + method: "POST", | |
| 267 | + headers: { | |
| 268 | + Authorization: `Bearer ${cfg.token}`, | |
| 269 | + "Content-Type": "application/json", | |
| 270 | + }, | |
| 271 | + body: JSON.stringify({ text, nick: cfg.nick }), | |
| 272 | + }); | |
| 273 | +} | |
| 274 | + | |
| 275 | +async function relayPoll() { | |
| 276 | + const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { | |
| 277 | + headers: { Authorization: `Bearer ${cfg.token}` }, | |
| 278 | + }); | |
| 279 | + const data = await res.json(); | |
| 280 | + const now = Date.now() / 1000; | |
| 281 | + const bots = new Set([cfg.nick, "bridge", "oracle", "sentinel", "steward", "scribe", "warden"]); | |
| 282 | + const msgs = | |
| 283 | + data.messages?.filter( | |
| 284 | + (m) => !bots.has(m.nick) && Date.parse(m.at) / 1000 > lastCheck | |
| 285 | + ) || []; | |
| 286 | + lastCheck = now; | |
| 287 | + return msgs; |
| --- a/skills/openai-relay/SKILL.md | |
| +++ b/skills/openai-relay/SKILL.md | |
| @@ -0,0 +1,287 @@ | |
| --- a/skills/openai-relay/SKILL.md | |
| +++ b/skills/openai-relay/SKILL.md | |
| @@ -0,0 +1,287 @@ | |
| 1 | --- |
| 2 | name: openai-relay |
| 3 | description: Bidirectional OpenAI agent integration for scuttlebot. Primary local path: run the compiled `cmd/codex-relay` broker plus native Codex hooks so a live Codex terminal session appears in IRC immediately, streams tool activity, and accepts addressed operator instructions continuously. Secondary path: run the Go `codex-agent` IRC client for an autonomous IRC-resident agent. Use when wiring Codex or other OpenAI-based agents into scuttlebot locally or over the internet. |
| 4 | --- |
| 5 | |
| 6 | # OpenAI Relay |
| 7 | |
| 8 | There are two production paths: |
| 9 | - local Codex terminal session: `cmd/codex-relay` |
| 10 | - IRC-resident autonomous agent: `cmd/codex-agent` |
| 11 | |
| 12 | Use the broker path when you want the local Codex terminal to show up in IRC as |
| 13 | soon as it starts, post `online`/`offline` presence, stream per-tool activity via |
| 14 | hooks, and accept addressed instructions continuously while the session is running. |
| 15 | |
| 16 | Source-of-truth files in the repo: |
| 17 | - installer: `skills/openai-relay/scripts/install-codex-relay.sh` |
| 18 | - broker: `cmd/codex-relay/main.go` |
| 19 | - -relay |
| 20 | description: Bidirectional OpenAI agent integration for scuttlebot. Primary local path: run the compiled `cmd/codex-relay` broker plus native Codex hooks so a live Codex terminal session appears in IRC imm--- |
| 21 | name: openai-relay |
| 22 | description: Bidirectional OpenAI agent integration fornfig` are copies. |
| 23 | |
| 24 | ## Setup |
| 25 | - Export gateway env vars: |
| 26 | - `SCUTTLEBOT_URL` e.g. `http://localhost:8080` |
| 27 | - `SCUTTLEBOT_TOKEN` bearer token |
| 28 | - Ensure the daemon has an `openai` backend configured. |
| 29 | - Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. |
| 30 | |
| 31 | ## Preferred For Local Codex CLI: codex-relay broker |
| 32 | Installer-first path: |
| 33 | |
| 34 | ```bash |
| 35 | bash skills/openai-relay/scripts/install-codex-relay.sh \ |
| 36 | --url http://localhost:8080 \ |
| 37 | --token "$(./run.sh token)" \ |
| 38 | --channel general |
| 39 | ``` |
| 40 | |
| 41 | Then launch: |
| 42 | |
| 43 | ```bash |
| 44 | ~/.local/bin/codex-relay |
| 45 | ``` |
| 46 | |
| 47 | Manual install and launch: |
| 48 | ```bash |
| 49 | mkdir -p ~/.codex/hooks ~/.local/bin |
| 50 | cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ |
| 51 | cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ |
| 52 | go build -o ~/.local/bin/codex-relay ./cmd/codex-relay |
| 53 | chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay |
| 54 | ``` |
| 55 | |
| 56 | Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then: |
| 57 | |
| 58 | ```bash |
| 59 | ~/.local/bin/codex-relay |
| 60 | ``` |
| 61 | |
| 62 | Behavior: |
| 63 | - export a stable `SCUTTLEBOT_SESSION_ID` |
| 64 | - derive a stable `codex-{basename}-{session}` nick |
| 65 | - post `online ...` immediately when Codex starts |
| 66 | - post `offline ...` when Codex exits |
| 67 | - continuously inject addressed IRC messages into the live Codex terminal |
| 68 | - mirror assistant output and tool activity from the active session log |
| 69 | - use `pkg/sessionrelay` for both `http` and `irc` transport modes |
| 70 | - let the existing hooks remain the pre-tool fallback path |
| 71 | |
| 72 | token |
| 73 | - Ensure the daemon has an `openai` backend configured. |
| 74 | - Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. |
| 75 | |
| 76 | ## Preferred For Local Codex CLI: codex-relay broker |
| 77 | Installer-first path: |
| 78 | |
| 79 | ```bash |
| 80 | bash skills/openai-relay/scripts/install-codex-relay.sh \ |
| 81 | --url http://localhost:8080 \ |
| 82 | --token "$(./run.sh token)" \ |
| 83 | --channel general |
| 84 | ``` |
| 85 | |
| 86 | Then launch: |
| 87 | |
| 88 | ```bash |
| 89 | ~/.local/bin/codex-relay |
| 90 | ``` |
| 91 | |
| 92 | Manual install and launch: |
| 93 | ```bash |
| 94 | mkdir -p ~/.codex/hooks ~/.local/bin |
| 95 | cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ |
| 96 | cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ |
| 97 | go build -o ~/.local/bin/codex-relay ./cmd/codex-relay |
| 98 | chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay |
| 99 | ``` |
| 100 | |
| 101 | Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then: |
| 102 | |
| 103 | ```bash |
| 104 | ~/.local/bin/codex-relay |
| 105 | ``` |
| 106 | |
| 107 | Behavior: |
| 108 | - export a stable `SCUTTLEBOT_SESSION_ID` |
| 109 | - derive a stable `codex-{basename}-{session}` nick |
| 110 | - post `online ...` immediately when Codex starts |
| 111 | - post `offline ...` when Codex exits |
| 112 | - continuously inject addressed IRC messages into the live Codex terminal |
| 113 | - mirror assistant output and tool activity from the active session log |
| 114 | - use `pkg/sessionrelay` for both `http` and `irc` transport modes |
| 115 | - let the existing hooks remain the pre-tool fallback path |
| 116 | |
| 117 | Canonical pattern summary: |
| 118 | - broker entrypoint: `cmd/codex-relay/main.go` |
| 119 | - tracked installer: `skills/openai-relay/scripts/install-codex-relay.sh` |
| 120 | - runtime docs: `skills/openai-relay/install.md` and `skills/openai-relay/FLEET.md` |
| 121 | - hooks: `skills/openai-relay/hooks/` |
| 122 | - shared transport: `pkg/sessionrelay/` |
| 123 | |
| 124 | Transport modes: |
| 125 | - `SCUTTLEBOT_TRANSPORT=http` uses the working HTTP bridge path and presence heartbeats |
| 126 | - `SCUTTLEBOT_TRANSPORT=irc` connects the live session nick directly to Ergo over SASL |
| 127 | - in `irc` mode, `SCUTTLEBOT_IRC_PASS` uses a fixed NickServ password; otherwise the broker auto-registers the ephemeral session nick through `/v1/agents/register` and deletes it on clean exit by default |
| 128 | |
| 129 | To disable the relay without uninstalling: |
| 130 | |
| 131 | ```bash |
| 132 | SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/codex-relay |
| 133 | ``` |
| 134 | |
| 135 | Optional shell alias: |
| 136 | ```bash |
| 137 | alias codex="$HOME/.local/bin/codex-relay" |
| 138 | ``` |
| 139 | |
| 140 | ## Preferred For IRC-Resident Agents: Go codex-agent |
| 141 | Build and run: |
| 142 | ```bash |
| 143 | go build -o bin/codex-agent ./cmd/codex-agent |
| 144 | bin/codex-agent \ |
| 145 | --irc 127.0.0.1:6667 \ |
| 146 | --nick codex-1234 \ |
| 147 | --pass <nickserv-passphrase> \ |
| 148 | --channels "#general" \ |
| 149 | --api-url "$SCUTTLEBOT_URL" \ |
| 150 | --token "$SCUTTLEBOT_TOKEN" \ |
| 151 | --backend openai |
| 152 | ``` |
| 153 | |
| 154 | Register a new nick via HTTP: |
| 155 | ```bash |
| 156 | curl -X POST "$SCUTTLEBOT_URL/v1/agents/register" \ |
| 157 | -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
| 158 | -H "Content-Type: application/json" \ |
| 159 | -d '{"nick":"codex-1234","type":"worker","channels":["#general"]}' |
| 160 | ``` |
| 161 | |
| 162 | Behavior: |
| 163 | - connect to Ergo using SASL |
| 164 | - join configured channels |
| 165 | - respond to DMs or messages that mention the agent nick |
| 166 | - keep short in-memory conversation history per channel/DM |
| 167 | - call scuttlebot's `/v1/llm/complete` with backend `openai` |
| 168 | |
| 169 | ## Direct mode |
| 170 | Use direct mode only if you want the agent to call OpenAI itself instead of the daemon gateway: |
| 171 | ```bash |
| 172 | OPENAI_API_KEY=... \ |
| 173 | bin/codex-agent \ |
| 174 | --irc 127.0.0.1:6667 \ |
| 175 | --nick codex-1234 \ |
| 176 | --pass <nickserv-passphrase> \ |
| 177 | --channels "#general" \ |
| 178 | --api-key "$OPENAI_API_KEY" \ |
| 179 | --model gpt-5.4-mini |
| 180 | ``` |
| 181 | |
| 182 | ## Hook-based operator control |
| 183 | If you want operator instructions to feed back into a live Codex tool loop before |
| 184 | the next action, install the shell hooks in `skills/openai-relay/hooks/`. |
| 185 | For immediate startup presence plus continuous IRC input injection, launch through |
| 186 | the compiled `cmd/codex-relay` broker installed as `~/.local/bin/codex-relay`. |
| 187 | |
| 188 | - `scuttlebot-post.sh` posts one-line activity after each tool call |
| 189 | - `scuttlebot-check.sh` checks the channel before the next action |
| 190 | - `cmd/codex-relay` posts `online` at session start, injects addressed IRC messages into the live PTY, and posts `offline` on exit |
| 191 | - only messages that explicitly mention the session nick block the loop |
| 192 | - default session nick format is `codex-{basename}-{session}` unless you override |
| 193 | `SCUTTLEBOT_NICK` |
| 194 | |
| 195 | Install: |
| 196 | ```bash |
| 197 | mkdir -p ~/.codex/hooks |
| 198 | cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ |
| 199 | cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ |
| 200 | chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh |
| 201 | ``` |
| 202 | |
| 203 | Config in `~/.codex/hooks.json`: |
| 204 | ```json |
| 205 | { |
| 206 | "hooks": { |
| 207 | "pre-tool-use": [ |
| 208 | { |
| 209 | "matcher": "Bash|Edit|Write", |
| 210 | "hooks": [ |
| 211 | { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-check.sh" } |
| 212 | ] |
| 213 | } |
| 214 | ], |
| 215 | "post-tool-use": [ |
| 216 | { |
| 217 | "matcher": "Bash|Read|Edit|Write|Glob|Grep|Agent", |
| 218 | "hooks": [ |
| 219 | { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" } |
| 220 | ] |
| 221 | } |
| 222 | ] |
| 223 | } |
| 224 | } |
| 225 | ``` |
| 226 | |
| 227 | Enable the feature in `~/.codex/config.toml`: |
| 228 | ```toml |
| 229 | [features] |
| 230 | codex_hooks = true |
| 231 | ``` |
| 232 | |
| 233 | Required env: |
| 234 | - `SCUTTLEBOT_URL` |
| 235 | - `SCUTTLEBOT_TOKEN` |
| 236 | - `SCUTTLEBOT_CHANNEL` |
| 237 | |
| 238 | The hooks also auto-load `~/.config/scuttlebot-relay.env` if present. |
| 239 | |
| 240 | For fleet rollout instructions, see `skills/openai-relay/FLEET.md`. |
| 241 | |
| 242 | ## Lightweight HTTP relay examples |
| 243 | Use these only when you need custom status/poll integrations without the shell |
| 244 | hooks or a full IRC client. The shipped scripts in `skills/openai-relay/scripts/` |
| 245 | already implement stable session nicks and mention-targeted polling; treat the |
| 246 | inline snippets below as transport illustrations. |
| 247 | |
| 248 | ### Node 18+ |
| 249 | ```js |
| 250 | import OpenAI from "openai"; |
| 251 | |
| 252 | const cfg = { |
| 253 | url: process.env.SCUTTLEBOT_URL, |
| 254 | token: process.env.SCUTTLEBOT_TOKEN, |
| 255 | channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""), |
| 256 | nick: process.env.SCUTTLEBOT_NICK || "codex", |
| 257 | model: process.env.OPENAI_MODEL || "gpt-4.1-mini", |
| 258 | backend: process.env.SCUTTLEBOT_LLM_BACKEND, // optional: use daemon-stored key |
| 259 | }; |
| 260 | |
| 261 | const openai = cfg.backend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); |
| 262 | let lastCheck = 0; |
| 263 | |
| 264 | async function relayPost(text) { |
| 265 | await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
| 266 | method: "POST", |
| 267 | headers: { |
| 268 | Authorization: `Bearer ${cfg.token}`, |
| 269 | "Content-Type": "application/json", |
| 270 | }, |
| 271 | body: JSON.stringify({ text, nick: cfg.nick }), |
| 272 | }); |
| 273 | } |
| 274 | |
| 275 | async function relayPoll() { |
| 276 | const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
| 277 | headers: { Authorization: `Bearer ${cfg.token}` }, |
| 278 | }); |
| 279 | const data = await res.json(); |
| 280 | const now = Date.now() / 1000; |
| 281 | const bots = new Set([cfg.nick, "bridge", "oracle", "sentinel", "steward", "scribe", "warden"]); |
| 282 | const msgs = |
| 283 | data.messages?.filter( |
| 284 | (m) => !bots.has(m.nick) && Date.parse(m.at) / 1000 > lastCheck |
| 285 | ) || []; |
| 286 | lastCheck = now; |
| 287 | return msgs; |
| --- a/skills/openai-relay/hooks/README.md | ||
| +++ b/skills/openai-relay/hooks/README.md | ||
| @@ -0,0 +1,114 @@ | ||
| 1 | +# Codex Hook Primer | |
| 2 | + | |
| 3 | +These hooks are the pre-tool fallback path for a live Codex tool loop. | |
| 4 | +Continuous IRC-to-terminal input plus outbound message and tool mirroring are | |
| 5 | +handled by the compiled `cmd/codex-relay` broker. | |
| 6 | + | |
| 7 | +If you need to add another runtime later, use | |
| 8 | +[`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) | |
| 9 | +as the shared authoring contract. | |
| 10 | + | |
| 11 | +Files in this directory: | |
| 12 | +- `scuttlebot-post.sh` | |
| 13 | +- `scuttlebot-check.sh` | |
| 14 | + | |
| 15 | +Related launcher: | |
| 16 | +- `../../../cmd/codex-relay/main.go` | |
| 17 | +- `../scripts/codex-relay.sh` | |
| 18 | +- `../scripts/install-codex-relay.sh` | |
| 19 | + | |
| 20 | +Source of truth: | |
| 21 | +- the repo copies in this directory and `../scripts/` | |
| 22 | +- not the installed copies under `~/.codex/` or `~/.local/bin/` | |
| 23 | + | |
| 24 | +## What they TRANSPORages from scuttlennel when Codex is not launched through `codex-relay` | |
| 25 | +- uses the session nick as the IRC/web bridge sender nick | |
| 26 | + | |
| 27 | +`scuttlebot-check.sh` | |
| 28 | +- runs before the next action | |
| 29 | +- fetches recent channel messages from scuttlebot | |
| 30 | +- ignores bots and agent status nicks | |
| 31 | +- blocks only when a human explicitly mentions this session nick | |
| 32 | +- prints a JSON decision block that Codex can surface into the live tool loop | |
| 33 | + | |
| 34 | +W the full control loop: | |
| 35 | +1. `cmd/codex-relay` posts `online`. | |
| 36 | +2. `cmd/codex-relay` mirrors assistant output and tool activity from the active session log. | |
| 37 | +3. The operator mentions the Codex session nick. | |
| 38 | +4. `cmd/codex-rlay` injects that IRC message into the live terminal session immediately. | |
| 39 | +5. `scuttlebot-check.sh` still blocks before the next tool action if needed. | |
| 40 | + | |
| 41 | +For immediate startup visibili## Default nick format | |
| 42 | + | |
| 43 | +If `SCUTTLEBOT_NICK` is unset, the hooks derive a stable session nick: | |
| 44 | + | |
| 45 | +```text | |
| 46 | +codex-{basename of cwd}-{session id} | |
| 47 | +``` | |
| 48 | + | |
| 49 | +Session id resolution order: | |
| 50 | +1. `SCUTTLEBOT_SESSION_ID` | |
| 51 | +2. `CODEX_SESSION_ID` | |
| 52 | +3. parent process id (`PPID`) | |
| 53 | + | |
| 54 | +Examples: | |
| 55 | +- `codex-scuttlebot-8421` | |
| 56 | +- `codex-calliope-qa` | |
| 57 | + | |
| 58 | +This is # Codex Hook Primer | |
| 59 | + | |
| 60 | +These hooks are the pre-tool fallback path for a live Codex tool loop. | |
| 61 | +Continuous IRC-to-terminal input plus outbound message and tool mirroring are | |
| 62 | +handled by the compiled `cmd/codex-relay` broker, which now sits on the shared | |
| 63 | +`pkg/sessionrelay` connector package. | |
| 64 | + | |
| 65 | +If you need to add another runtime later, use | |
| 66 | +[`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) | |
| 67 | +as the shared authoring contract. | |
| 68 | + | |
| 69 | +Files in this directory: | |
| 70 | +- `scuttlebot-post.sh` | |
| 71 | +- `scuttlebot-check.sh` | |
| 72 | + | |
| 73 | +Related launcher: | |
| 74 | +- `../../../cmd/codex-relay/main.go` | |
| 75 | +- `../scripts/codex-relay.she IRC/web bridge sender nick | |
| 76 | + | |
| 77 | +`scuttlebot-check.sh` | |
| 78 | +- runs before the next action | |
| 79 | +- fetches rebot | |
| 80 | +- ignores bots and agent status nicks | |
| 81 | +- blocks only when a human explicitly mentions this session nick | |
| 82 | +- prints a JSON decision block that Codex can surface into the live tool loop | |
| 83 | + | |
| 84 | +W the full control loop: | |
| 85 | +1. `cmd/codex-relay` posts `online`. | |
| 86 | +2. `cmd/codex-relay` mirrors assistant output and tool activity from the active session log. | |
| 87 | +3. The operator mentions the Codex session nick. | |
| 88 | +4. `cmd/codex-rimmediately. | |
| 89 | +5. `scuttlebot-check.sh` still blocks before the next tool action visibili## Default nick format | |
| 90 | + | |
| 91 | +Y. | |
| 92 | +- `cmd/codex-relay` can do that over either the HTTP bridge API or a real IRC socket. | |
| 93 | +- `cmdand", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" } | |
| 94 | + ] | |
| 95 | + } | |
| 96 | + ] | |
| 97 | + } | |
| 98 | +} | |
| 99 | +``` | |
| 100 | + | |
| 101 | +Enable the feature in `~/.codex/config.toml`: | |
| 102 | + | |
| 103 | +```toml | |
| 104 | +[features] | |
| 105 | +codex_hooks = true | |
| 106 | +``` | |
| 107 | + | |
| 108 | +Install the compiled broker if you want startup/offline presence plus continuous | |
| 109 | +IRC input injection: | |
| 110 | + | |
| 111 | +```bash | |
| 112 | +mkdir -p ~/.local/bin | |
| 113 | +go build -o ~/.local/bin/codex-relay ./cmd/codex-relay | |
| 114 | +chmod +x ~/.lThe hooks |
| --- a/skills/openai-relay/hooks/README.md | |
| +++ b/skills/openai-relay/hooks/README.md | |
| @@ -0,0 +1,114 @@ | |
| --- a/skills/openai-relay/hooks/README.md | |
| +++ b/skills/openai-relay/hooks/README.md | |
| @@ -0,0 +1,114 @@ | |
| 1 | # Codex Hook Primer |
| 2 | |
| 3 | These hooks are the pre-tool fallback path for a live Codex tool loop. |
| 4 | Continuous IRC-to-terminal input plus outbound message and tool mirroring are |
| 5 | handled by the compiled `cmd/codex-relay` broker. |
| 6 | |
| 7 | If you need to add another runtime later, use |
| 8 | [`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) |
| 9 | as the shared authoring contract. |
| 10 | |
| 11 | Files in this directory: |
| 12 | - `scuttlebot-post.sh` |
| 13 | - `scuttlebot-check.sh` |
| 14 | |
| 15 | Related launcher: |
| 16 | - `../../../cmd/codex-relay/main.go` |
| 17 | - `../scripts/codex-relay.sh` |
| 18 | - `../scripts/install-codex-relay.sh` |
| 19 | |
| 20 | Source of truth: |
| 21 | - the repo copies in this directory and `../scripts/` |
| 22 | - not the installed copies under `~/.codex/` or `~/.local/bin/` |
| 23 | |
| 24 | ## What they TRANSPORages from scuttlennel when Codex is not launched through `codex-relay` |
| 25 | - uses the session nick as the IRC/web bridge sender nick |
| 26 | |
| 27 | `scuttlebot-check.sh` |
| 28 | - runs before the next action |
| 29 | - fetches recent channel messages from scuttlebot |
| 30 | - ignores bots and agent status nicks |
| 31 | - blocks only when a human explicitly mentions this session nick |
| 32 | - prints a JSON decision block that Codex can surface into the live tool loop |
| 33 | |
| 34 | W the full control loop: |
| 35 | 1. `cmd/codex-relay` posts `online`. |
| 36 | 2. `cmd/codex-relay` mirrors assistant output and tool activity from the active session log. |
| 37 | 3. The operator mentions the Codex session nick. |
| 38 | 4. `cmd/codex-rlay` injects that IRC message into the live terminal session immediately. |
| 39 | 5. `scuttlebot-check.sh` still blocks before the next tool action if needed. |
| 40 | |
| 41 | For immediate startup visibili## Default nick format |
| 42 | |
| 43 | If `SCUTTLEBOT_NICK` is unset, the hooks derive a stable session nick: |
| 44 | |
| 45 | ```text |
| 46 | codex-{basename of cwd}-{session id} |
| 47 | ``` |
| 48 | |
| 49 | Session id resolution order: |
| 50 | 1. `SCUTTLEBOT_SESSION_ID` |
| 51 | 2. `CODEX_SESSION_ID` |
| 52 | 3. parent process id (`PPID`) |
| 53 | |
| 54 | Examples: |
| 55 | - `codex-scuttlebot-8421` |
| 56 | - `codex-calliope-qa` |
| 57 | |
| 58 | This is # Codex Hook Primer |
| 59 | |
| 60 | These hooks are the pre-tool fallback path for a live Codex tool loop. |
| 61 | Continuous IRC-to-terminal input plus outbound message and tool mirroring are |
| 62 | handled by the compiled `cmd/codex-relay` broker, which now sits on the shared |
| 63 | `pkg/sessionrelay` connector package. |
| 64 | |
| 65 | If you need to add another runtime later, use |
| 66 | [`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) |
| 67 | as the shared authoring contract. |
| 68 | |
| 69 | Files in this directory: |
| 70 | - `scuttlebot-post.sh` |
| 71 | - `scuttlebot-check.sh` |
| 72 | |
| 73 | Related launcher: |
| 74 | - `../../../cmd/codex-relay/main.go` |
| 75 | - `../scripts/codex-relay.she IRC/web bridge sender nick |
| 76 | |
| 77 | `scuttlebot-check.sh` |
| 78 | - runs before the next action |
| 79 | - fetches rebot |
| 80 | - ignores bots and agent status nicks |
| 81 | - blocks only when a human explicitly mentions this session nick |
| 82 | - prints a JSON decision block that Codex can surface into the live tool loop |
| 83 | |
| 84 | W the full control loop: |
| 85 | 1. `cmd/codex-relay` posts `online`. |
| 86 | 2. `cmd/codex-relay` mirrors assistant output and tool activity from the active session log. |
| 87 | 3. The operator mentions the Codex session nick. |
| 88 | 4. `cmd/codex-rimmediately. |
| 89 | 5. `scuttlebot-check.sh` still blocks before the next tool action visibili## Default nick format |
| 90 | |
| 91 | Y. |
| 92 | - `cmd/codex-relay` can do that over either the HTTP bridge API or a real IRC socket. |
| 93 | - `cmdand", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" } |
| 94 | ] |
| 95 | } |
| 96 | ] |
| 97 | } |
| 98 | } |
| 99 | ``` |
| 100 | |
| 101 | Enable the feature in `~/.codex/config.toml`: |
| 102 | |
| 103 | ```toml |
| 104 | [features] |
| 105 | codex_hooks = true |
| 106 | ``` |
| 107 | |
| 108 | Install the compiled broker if you want startup/offline presence plus continuous |
| 109 | IRC input injection: |
| 110 | |
| 111 | ```bash |
| 112 | mkdir -p ~/.local/bin |
| 113 | go build -o ~/.local/bin/codex-relay ./cmd/codex-relay |
| 114 | chmod +x ~/.lThe hooks |
| --- a/skills/openai-relay/hooks/scuttlebot-check.sh | ||
| +++ b/skills/openai-relay/hooks/scuttlebot-check.sh | ||
| @@ -0,0 +1,30 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# Pre-action hook for Codex. Checks scuttlebot for operator instructions before | |
| 3 | +# each tool call and returns a blocking decision when the session nick is | |
| 4 | +# explicitly mentioned. | |
| 5 | + | |
| 6 | +SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" | |
| 7 | +if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then | |
| 8 | + set -a | |
| 9 | + . "$SCUTTLEBO | |
| 10 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 11 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" | |
| 12 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" | |
| 13 | +SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 14 | + | |
| 15 | +sanitize() { | |
| 16 | + printf '%s' "$1" | tr -cs '[:alnum:]_-' '-' | |
| 17 | +} | |
| 18 | + | |
| 19 | +f '%s000' "$(date +%s)" | |
| 20 | +} | |
| 21 | + | |
| 22 | +base_name=$(basename "$(pwd)") | |
| 23 | +base_name=$(sanitize "$base_name") | |
| 24 | +session_suffix="${SCUTTLEBOT_SESSION_ID:-${CODEX_SESSION_ID:-$PPID}}" | |
| 25 | +session_suffix=$(sanitize "$session_suffix") | |
| 26 | +default_nick="codex-${base_name}-${session_suffix}" | |
| 27 | +SCUTTLEBOT_NICK="${CHANNEL|contains_mention() { | |
| 28 | + local text="$1" | |
| 29 | + [[ "$text" =~ (^|[^[:alnum:]_./\\-])$SCUTTLEBOT_NICK($#!/bin/bash | |
| 30 | +# Pre-action hook for Codex. Checks scuttlebot |
| --- a/skills/openai-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,30 @@ | |
| --- a/skills/openai-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,30 @@ | |
| 1 | #!/bin/bash |
| 2 | # Pre-action hook for Codex. Checks scuttlebot for operator instructions before |
| 3 | # each tool call and returns a blocking decision when the session nick is |
| 4 | # explicitly mentioned. |
| 5 | |
| 6 | SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" |
| 7 | if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then |
| 8 | set -a |
| 9 | . "$SCUTTLEBO |
| 10 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 11 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" |
| 12 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" |
| 13 | SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 14 | |
| 15 | sanitize() { |
| 16 | printf '%s' "$1" | tr -cs '[:alnum:]_-' '-' |
| 17 | } |
| 18 | |
| 19 | f '%s000' "$(date +%s)" |
| 20 | } |
| 21 | |
| 22 | base_name=$(basename "$(pwd)") |
| 23 | base_name=$(sanitize "$base_name") |
| 24 | session_suffix="${SCUTTLEBOT_SESSION_ID:-${CODEX_SESSION_ID:-$PPID}}" |
| 25 | session_suffix=$(sanitize "$session_suffix") |
| 26 | default_nick="codex-${base_name}-${session_suffix}" |
| 27 | SCUTTLEBOT_NICK="${CHANNEL|contains_mention() { |
| 28 | local text="$1" |
| 29 | [[ "$text" =~ (^|[^[:alnum:]_./\\-])$SCUTTLEBOT_NICK($#!/bin/bash |
| 30 | # Pre-action hook for Codex. Checks scuttlebot |
| --- a/skills/openai-relay/hooks/scuttlebot-post.sh | ||
| +++ b/skills/openai-relay/hooks/scuttlebot-post.sh | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# PostToolUse hook for OpenAI agents (Codex-style). Posts activity to scuttlebot IRC. | |
| 3 | + | |
| 4 | +SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" | |
| 5 | +if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then | |
| 6 | + set -a | |
| 7 | + . "$SCUTTLEBOT_CONFIG_FILE" | |
| 8 | + set +a | |
| 9 | +fi | |
| 10 | +EL_STATE_FILE" | |
| 11 | + set +a | |
| 12 | +fi | |
| 13 | + | |
| 14 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 15 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" | |
| 16 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" | |
| 17 | +SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 18 | +SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOTdev/null || true | |
| 19 | + done | |
| 20 | +} | |
| 21 | + | |
| 22 | +input=$(cat) | |
| 23 | + | |
| 24 | +tool=$(echo "$input" | jq -r '.tool_name // empty') | |
| 25 | +cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| 26 | + | |
| 27 | +sanitize() { | |
| 28 | + printf '%s' "$1" | tr |
| --- a/skills/openai-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,28 @@ | |
| --- a/skills/openai-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/openai-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,28 @@ | |
| 1 | #!/bin/bash |
| 2 | # PostToolUse hook for OpenAI agents (Codex-style). Posts activity to scuttlebot IRC. |
| 3 | |
| 4 | SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" |
| 5 | if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then |
| 6 | set -a |
| 7 | . "$SCUTTLEBOT_CONFIG_FILE" |
| 8 | set +a |
| 9 | fi |
| 10 | EL_STATE_FILE" |
| 11 | set +a |
| 12 | fi |
| 13 | |
| 14 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 15 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" |
| 16 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" |
| 17 | SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 18 | SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOTdev/null || true |
| 19 | done |
| 20 | } |
| 21 | |
| 22 | input=$(cat) |
| 23 | |
| 24 | tool=$(echo "$input" | jq -r '.tool_name // empty') |
| 25 | cwd=$(echo "$input" | jq -r '.cwd // empty') |
| 26 | |
| 27 | sanitize() { |
| 28 | printf '%s' "$1" | tr |
| --- a/skills/openai-relay/install.md | ||
| +++ b/skills/openai-relay/install.md | ||
| @@ -0,0 +1 @@ | ||
| 1 | +# A registered scuttlebot agent ni |
| --- a/skills/openai-relay/install.md | |
| +++ b/skills/openai-relay/install.md | |
| @@ -0,0 +1 @@ | |
| --- a/skills/openai-relay/install.md | |
| +++ b/skills/openai-relay/install.md | |
| @@ -0,0 +1 @@ | |
| 1 | # A registered scuttlebot agent ni |
| --- a/skills/openai-relay/scripts/codex-relay.sh | ||
| +++ b/skills/openai-relay/scripts/codex-relay.sh | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Development wrapper for the compiled Codex relay broker. | |
| 3 | + | |
| 4 | +set -euo pipefail | |
| 5 | + | |
| 6 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 7 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 8 | + | |
| 9 | +if [ -x "$REPO_ROOT/bin/codex-relay" ]; then | |
| 10 | + exec "$REPO_ROOT/bin/codex-relay" "$@" | |
| 11 | +fi | |
| 12 | + | |
| 13 | +if ! command -v go >/dev/null 2>&1; then | |
| 14 | + printf 'codex-relay: go is required to run the broker from the repo checkout\n' >&2 | |
| 15 | + exit 1 | |
| 16 | +fi | |
| 17 | + | |
| 18 | +exec go run "$REPO_ROOT/cmd/codex-relay" "$@" |
| --- a/skills/openai-relay/scripts/codex-relay.sh | |
| +++ b/skills/openai-relay/scripts/codex-relay.sh | |
| @@ -0,0 +1,18 @@ | |
| --- a/skills/openai-relay/scripts/codex-relay.sh | |
| +++ b/skills/openai-relay/scripts/codex-relay.sh | |
| @@ -0,0 +1,18 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Development wrapper for the compiled Codex relay broker. |
| 3 | |
| 4 | set -euo pipefail |
| 5 | |
| 6 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 7 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 8 | |
| 9 | if [ -x "$REPO_ROOT/bin/codex-relay" ]; then |
| 10 | exec "$REPO_ROOT/bin/codex-relay" "$@" |
| 11 | fi |
| 12 | |
| 13 | if ! command -v go >/dev/null 2>&1; then |
| 14 | printf 'codex-relay: go is required to run the broker from the repo checkout\n' >&2 |
| 15 | exit 1 |
| 16 | fi |
| 17 | |
| 18 | exec go run "$REPO_ROOT/cmd/codex-relay" "$@" |
| --- a/skills/openai-relay/scripts/install-codex-relay.sh | ||
| +++ b/skills/openai-relay/scripts/install-codex-relay.sh | ||
| @@ -0,0 +1,228 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. | |
| 3 | + | |
| 4 | +set -euo pipefail | |
| 5 | + | |
| 6 | +usage() { | |
| 7 | + cat <<'EOF' | |
| 8 | +Usage: | |
| 9 | + bash skills/openai-relay/scripts/install-codex-relay.sh [options] | |
| 10 | + | |
| 11 | +Options: | |
| 12 | + --url URL Set SCUTTLEBOT_URL in the shared env file. | |
| 13 | + --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. | |
| 14 | + --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. | |
| 15 | + --enENABLED=1. Default. | |
| 16 | + | |
| 17 | + --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 18 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 19 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 20 | + --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks | |
| 21 | + --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json | |
| 22 | + --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml | |
| 23 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 24 | + --help Show this help. | |
| 25 | + | |
| 26 | +Environment defaults: | |
| 27 | + SCUTTLEBOT_URL | |
| 28 | + SCUTTLEBOT_TOKEN | |
| 29 | + SCUTTHOOKS_ENABLED | |
| 30 | + SCUTTLEBOT_INTERRUPT_ON_MESSAGE | |
| 31 | + SCUTTLEBOT_POLL_INTERVAL | |
| 32 | + SCUTTLEBOT_CONFIG_FILE | |
| 33 | + CODEX_HOOKS_DIR | |
| 34 | + CODEX_HOOKS_JSON | |
| 35 | + CODEX_CONFIG_TOML | |
| 36 | + CODEX_BIN_DIR | |
| 37 | + | |
| 38 | +Examples: | |
| 39 | + bash skills/openai-relay/scripts/install-codex-relay.sh \ | |
| 40 | + --url http://localhost:8080 \ | |
| 41 | + --token "$(./run.sh token)" \ | |
| 42 | + --channel general | |
| 43 | +EOF | |
| 44 | +} | |
| 45 | + | |
| 46 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 47 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 48 | + | |
| 49 | +SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" | |
| 50 | +SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" | |
| 51 | +SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" | |
| 52 | +SCUTTLEBOT_T_HOOKS_ENABLED=1. Default. | |
| 53 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 54 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 55 | + --hooks-dir PATH Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 56 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 57 | + --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks | |
| 58 | + --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json | |
| 59 | + --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml | |
| 60 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 61 | + --help Show this help. | |
| 62 | + | |
| 63 | +Environment defaults: | |
| 64 | + SCUTTLEBOT_URL | |
| 65 | + SCUTTLEBOT_TOKEN | |
| 66 | + SCUTTTRANSPORT | |
| 67 | + SCUTTLEBOT_IRC_ADDR | |
| 68 | + SCUTTLEBOT_IRC_PASS | |
| 69 | + SCUTTLEBscripts/install-codex-relay.sh \ | |
| 70 | + --url http://localhost:8080 \ | |
| 71 | + --token "$(./run.sh token)" \ | |
| 72 | + --channel general | |
| 73 | +EOF | |
| 74 | +} | |
| 75 | + | |
| 76 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 77 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 78 | + | |
| 79 | +SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" | |
| 80 | +SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" | |
| 81 | +SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" | |
| 82 | +SCUTTLEBOT_#!/usr/bin/env bash | |
| 83 | +# Install the tracked Codex relay hooks plus the compiled broker intracked Codex relay hooks pl${racked Codex relay :- --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 84 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 85 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 86 | + --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks | |
| 87 | + --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json | |
| 88 | + --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml | |
| 89 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 90 | + --help Show this help. | |
| 91 | + | |
| 92 | +Environment defaults: | |
| 93 | + SCUTTLEBOT_URL | |
| 94 | + SCUTTLEBOT_TOKEN | |
| 95 | + SCUTTTRANSPORT | |
| 96 | + SCUTTLEBOT_IRC_ADDR | |
| 97 | + SCUTTLEBOT_IRC_PASS | |
| 98 | + SCUTTLEBOT_HOOKS_ENABLED | |
| 99 | + SCUTTLEBOT_INTERRUPT_ON_MESSAGE | |
| 100 | + SCUTTLEBOT_POLL_INTERVAL | |
| 101 | + SCUTTLEBOT_PRESENCE_HEARTBEAT | |
| 102 | + SCUTTLEBOT_CONFIG_FILE | |
| 103 | + CODEX_HOOKS_DIR | |
| 104 | + CODEX_HOOKS_JSON | |
| 105 | + CODEX_CONFIG_TOML | |
| 106 | + CODEX_BIN_DIR | |
| 107 | + | |
| 108 | +Examples: | |
| 109 | + bash skills/openai-relay/scripts/install-codex-relay.sh \ | |
| 110 | + --url http://localhost:8080 \ | |
| 111 | + --token "$(./run.sh token)" \ | |
| 112 | + --channel general | |
| 113 | +EOF | |
| 114 | +} | |
| 115 | + | |
| 116 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- enabled-codex-relay.sh \ | |
| 117 | + #!/usr/bin/env bash | |
| 118 | +ts the compiled broker into a local Codex setup. | |
| 119 | + | |
| 120 | +set -euo pipefail | |
| 121 | + | |
| 122 | +usage() { | |
| 123 | + cat <<'EOF' | |
| 124 | +disabled-codex-relay.sh \ | |
| 125 | + #!/usr/bin/env bash | |
| 126 | +# Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. | |
| 127 | + | |
| 128 | +set -euo pipefail | |
| 129 | + | |
| 130 | +usage() { | |
| 131 | + cat <<'EOF' | |
| 132 | +Usage: | |
| 133 | + bash skills/openai-relay/scripts/install-codex-relay.sh [options] | |
| 134 | + | |
| 135 | +Options: | |
| 136 | + --url URL Set SCUTTLEBOT_URL in the shared env file. | |
| 137 | + --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. | |
| 138 | + --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. | |
| 139 | + --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. | |
| 140 | + --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. | |
| 141 | + --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. | |
| 142 | + --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. | |
| 143 | + --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 144 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 145 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 146 | + --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks | |
| 147 | + --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json | |
| 148 | + --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml | |
| 149 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 150 | + --help Show this help. | |
| 151 | + | |
| 152 | +Environment defaults: | |
| 153 | + SCUTTLEBOT_URL | |
| 154 | + SCUTTLEBOT_TOKEN | |
| 155 | + SCUTTTRANSPORT | |
| 156 | + SCUTTLEBOT_IRC_ADDR | |
| 157 | + SCUTTLEBOT_IRC_PASS | |
| 158 | + SCthe tracked Codex relay hooks plus the compiled broker into a local Codex setup. | |
| 159 | + | |
| 160 | +set -euo pipefail | |
| 161 | + | |
| 162 | +usage() { | |
| 163 | + cat <<'EOF' | |
| 164 | +Usage: | |
| 165 | + bash skills/openai-relay/scripts/install-codex-relay.sh [options] | |
| 166 | + | |
| 167 | +Options: | |
| 168 | + --url URL Set SCUTTLEBOT_URL in the shared env file. | |
| 169 | + --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. | |
| 170 | + --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. | |
| 171 | + --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. | |
| 172 | + --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. | |
| 173 | + --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. | |
| 174 | + --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. | |
| 175 | + --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 176 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 177 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 178 | + --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks | |
| 179 | + --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json | |
| 180 | + --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml | |
| 181 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 182 | + --help Show this help. | |
| 183 | + | |
| 184 | +Environment defaults: | |
| 185 | + SCUTTLEBOT_URL | |
| 186 | + SCUTTLEBOT_TOKEN | |
| 187 | + SCUTTTRANSPORT | |
| 188 | + SCUTTLEBOT_IRC_ADDR | |
| 189 | + SCUTTLEBOT_IRC_PASS | |
| 190 | + SCUTTLEBOT_HOOKS_ENABLED | |
| 191 | + SCUTTLEBOT_INTERRUPT_ON_MESSAGE | |
| 192 | + SCUTTLEBOT_POLL_INTERVAL | |
| 193 | + SCUTTLEBOT_PRESENCE_HEARTBEAT | |
| 194 | + SCUTTLEBOT_CONFIG_FILE | |
| 195 | + CODEX_HOOKS_DIR | |
| 196 | + CODEX_HOOKS_JSON | |
| 197 | + CODEX_CONFIG_TOML | |
| 198 | + CODEX_BIN_DIR | |
| 199 | + | |
| 200 | +Examples: | |
| 201 | + bash skills/openai-relay/scripts/install-codex-relay.sh \ | |
| 202 | + --url http://localhost:8080 \ | |
| 203 | + --token "$(./run.sh token)" \ | |
| 204 | + --channel general | |
| 205 | +EOF | |
| 206 | +} | |
| 207 | + | |
| 208 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 209 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 210 | + | |
| 211 | +SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" | |
| 212 | +SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" | |
| 213 | +SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" | |
| 214 | +SCUTTLEBOT_#!/usr/bin/env bash | |
| 215 | +# Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. | |
| 216 | + | |
| 217 | +set -euo pipefail | |
| 218 | + | |
| 219 | +usage() { | |
| 220 | + cat <<'EOF' | |
| 221 | +Usage: | |
| 222 | + bash skills/openai-relay/scripts/install-codex-relay.sh [options] | |
| 223 | + | |
| 224 | +Options: | |
| 225 | + --url URL Set SCUTTLEBOT_URL in the shared envUE" ]; then | |
| 226 | + upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_URL "$SCUTTLEBOT_URL_VALUE" | |
| 227 | +fi | |
| 228 | +if [ -n "$SCUTTLEBOT_TOKEN |
| --- a/skills/openai-relay/scripts/install-codex-relay.sh | |
| +++ b/skills/openai-relay/scripts/install-codex-relay.sh | |
| @@ -0,0 +1,228 @@ | |
| --- a/skills/openai-relay/scripts/install-codex-relay.sh | |
| +++ b/skills/openai-relay/scripts/install-codex-relay.sh | |
| @@ -0,0 +1,228 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. |
| 3 | |
| 4 | set -euo pipefail |
| 5 | |
| 6 | usage() { |
| 7 | cat <<'EOF' |
| 8 | Usage: |
| 9 | bash skills/openai-relay/scripts/install-codex-relay.sh [options] |
| 10 | |
| 11 | Options: |
| 12 | --url URL Set SCUTTLEBOT_URL in the shared env file. |
| 13 | --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. |
| 14 | --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. |
| 15 | --enENABLED=1. Default. |
| 16 | |
| 17 | --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 18 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 19 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 20 | --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks |
| 21 | --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json |
| 22 | --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml |
| 23 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 24 | --help Show this help. |
| 25 | |
| 26 | Environment defaults: |
| 27 | SCUTTLEBOT_URL |
| 28 | SCUTTLEBOT_TOKEN |
| 29 | SCUTTHOOKS_ENABLED |
| 30 | SCUTTLEBOT_INTERRUPT_ON_MESSAGE |
| 31 | SCUTTLEBOT_POLL_INTERVAL |
| 32 | SCUTTLEBOT_CONFIG_FILE |
| 33 | CODEX_HOOKS_DIR |
| 34 | CODEX_HOOKS_JSON |
| 35 | CODEX_CONFIG_TOML |
| 36 | CODEX_BIN_DIR |
| 37 | |
| 38 | Examples: |
| 39 | bash skills/openai-relay/scripts/install-codex-relay.sh \ |
| 40 | --url http://localhost:8080 \ |
| 41 | --token "$(./run.sh token)" \ |
| 42 | --channel general |
| 43 | EOF |
| 44 | } |
| 45 | |
| 46 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 47 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 48 | |
| 49 | SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" |
| 50 | SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" |
| 51 | SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" |
| 52 | SCUTTLEBOT_T_HOOKS_ENABLED=1. Default. |
| 53 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 54 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 55 | --hooks-dir PATH Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 56 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 57 | --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks |
| 58 | --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json |
| 59 | --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml |
| 60 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 61 | --help Show this help. |
| 62 | |
| 63 | Environment defaults: |
| 64 | SCUTTLEBOT_URL |
| 65 | SCUTTLEBOT_TOKEN |
| 66 | SCUTTTRANSPORT |
| 67 | SCUTTLEBOT_IRC_ADDR |
| 68 | SCUTTLEBOT_IRC_PASS |
| 69 | SCUTTLEBscripts/install-codex-relay.sh \ |
| 70 | --url http://localhost:8080 \ |
| 71 | --token "$(./run.sh token)" \ |
| 72 | --channel general |
| 73 | EOF |
| 74 | } |
| 75 | |
| 76 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 77 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 78 | |
| 79 | SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" |
| 80 | SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" |
| 81 | SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" |
| 82 | SCUTTLEBOT_#!/usr/bin/env bash |
| 83 | # Install the tracked Codex relay hooks plus the compiled broker intracked Codex relay hooks pl${racked Codex relay :- --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 84 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 85 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 86 | --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks |
| 87 | --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json |
| 88 | --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml |
| 89 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 90 | --help Show this help. |
| 91 | |
| 92 | Environment defaults: |
| 93 | SCUTTLEBOT_URL |
| 94 | SCUTTLEBOT_TOKEN |
| 95 | SCUTTTRANSPORT |
| 96 | SCUTTLEBOT_IRC_ADDR |
| 97 | SCUTTLEBOT_IRC_PASS |
| 98 | SCUTTLEBOT_HOOKS_ENABLED |
| 99 | SCUTTLEBOT_INTERRUPT_ON_MESSAGE |
| 100 | SCUTTLEBOT_POLL_INTERVAL |
| 101 | SCUTTLEBOT_PRESENCE_HEARTBEAT |
| 102 | SCUTTLEBOT_CONFIG_FILE |
| 103 | CODEX_HOOKS_DIR |
| 104 | CODEX_HOOKS_JSON |
| 105 | CODEX_CONFIG_TOML |
| 106 | CODEX_BIN_DIR |
| 107 | |
| 108 | Examples: |
| 109 | bash skills/openai-relay/scripts/install-codex-relay.sh \ |
| 110 | --url http://localhost:8080 \ |
| 111 | --token "$(./run.sh token)" \ |
| 112 | --channel general |
| 113 | EOF |
| 114 | } |
| 115 | |
| 116 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- enabled-codex-relay.sh \ |
| 117 | #!/usr/bin/env bash |
| 118 | ts the compiled broker into a local Codex setup. |
| 119 | |
| 120 | set -euo pipefail |
| 121 | |
| 122 | usage() { |
| 123 | cat <<'EOF' |
| 124 | disabled-codex-relay.sh \ |
| 125 | #!/usr/bin/env bash |
| 126 | # Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. |
| 127 | |
| 128 | set -euo pipefail |
| 129 | |
| 130 | usage() { |
| 131 | cat <<'EOF' |
| 132 | Usage: |
| 133 | bash skills/openai-relay/scripts/install-codex-relay.sh [options] |
| 134 | |
| 135 | Options: |
| 136 | --url URL Set SCUTTLEBOT_URL in the shared env file. |
| 137 | --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. |
| 138 | --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. |
| 139 | --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. |
| 140 | --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. |
| 141 | --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. |
| 142 | --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. |
| 143 | --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 144 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 145 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 146 | --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks |
| 147 | --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json |
| 148 | --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml |
| 149 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 150 | --help Show this help. |
| 151 | |
| 152 | Environment defaults: |
| 153 | SCUTTLEBOT_URL |
| 154 | SCUTTLEBOT_TOKEN |
| 155 | SCUTTTRANSPORT |
| 156 | SCUTTLEBOT_IRC_ADDR |
| 157 | SCUTTLEBOT_IRC_PASS |
| 158 | SCthe tracked Codex relay hooks plus the compiled broker into a local Codex setup. |
| 159 | |
| 160 | set -euo pipefail |
| 161 | |
| 162 | usage() { |
| 163 | cat <<'EOF' |
| 164 | Usage: |
| 165 | bash skills/openai-relay/scripts/install-codex-relay.sh [options] |
| 166 | |
| 167 | Options: |
| 168 | --url URL Set SCUTTLEBOT_URL in the shared env file. |
| 169 | --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. |
| 170 | --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. |
| 171 | --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. |
| 172 | --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. |
| 173 | --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. |
| 174 | --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. |
| 175 | --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 176 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 177 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 178 | --hooks-dir PATH Codex hooks install dir. Default: ~/.codex/hooks |
| 179 | --hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json |
| 180 | --codex-config PATH Codex config TOML. Default: ~/.codex/config.toml |
| 181 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 182 | --help Show this help. |
| 183 | |
| 184 | Environment defaults: |
| 185 | SCUTTLEBOT_URL |
| 186 | SCUTTLEBOT_TOKEN |
| 187 | SCUTTTRANSPORT |
| 188 | SCUTTLEBOT_IRC_ADDR |
| 189 | SCUTTLEBOT_IRC_PASS |
| 190 | SCUTTLEBOT_HOOKS_ENABLED |
| 191 | SCUTTLEBOT_INTERRUPT_ON_MESSAGE |
| 192 | SCUTTLEBOT_POLL_INTERVAL |
| 193 | SCUTTLEBOT_PRESENCE_HEARTBEAT |
| 194 | SCUTTLEBOT_CONFIG_FILE |
| 195 | CODEX_HOOKS_DIR |
| 196 | CODEX_HOOKS_JSON |
| 197 | CODEX_CONFIG_TOML |
| 198 | CODEX_BIN_DIR |
| 199 | |
| 200 | Examples: |
| 201 | bash skills/openai-relay/scripts/install-codex-relay.sh \ |
| 202 | --url http://localhost:8080 \ |
| 203 | --token "$(./run.sh token)" \ |
| 204 | --channel general |
| 205 | EOF |
| 206 | } |
| 207 | |
| 208 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 209 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 210 | |
| 211 | SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" |
| 212 | SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" |
| 213 | SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}" |
| 214 | SCUTTLEBOT_#!/usr/bin/env bash |
| 215 | # Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup. |
| 216 | |
| 217 | set -euo pipefail |
| 218 | |
| 219 | usage() { |
| 220 | cat <<'EOF' |
| 221 | Usage: |
| 222 | bash skills/openai-relay/scripts/install-codex-relay.sh [options] |
| 223 | |
| 224 | Options: |
| 225 | --url URL Set SCUTTLEBOT_URL in the shared envUE" ]; then |
| 226 | upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_URL "$SCUTTLEBOT_URL_VALUE" |
| 227 | fi |
| 228 | if [ -n "$SCUTTLEBOT_TOKEN |
| --- a/skills/openai-relay/scripts/node-openai-relay.mjs | ||
| +++ b/skills/openai-relay/scripts/node-openai-relay.mjs | ||
| @@ -0,0 +1,132 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// Minimal OpenAI + scuttlebot relay example (Node 18+). | |
| 3 | +// Requires env: SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL. | |
| 4 | +// Optional: SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_API_KEY. | |
| 5 | + | |
| 6 | +import OpenAI from "openai"; | |
| 7 | +import path from "node:path"; | |
| 8 | + | |
| 9 | +const prompt = process.argv[2] || "Hello from openai-relay"; | |
| 10 | +const sanitize = (value) => value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, ""); | |
| 11 | +const baseName = sanitize(path.basename(process.cwd()) || "repo"); | |
| 12 | +const sessionSuffix = sanitize( | |
| 13 | + process.env.SCUTTLEBOT_SESSION_ID || process.env.CODEX_SESSION_ID || String(process.ppid || process.pid) | |
| 14 | +) || "session"; | |
| 15 | + | |
| 16 | +const cfg = { | |
| 17 | + url: process.env.SCUTTLEBOT_URL, | |
| 18 | + token: process.env.SCUTTLEBOT_TOKEN, | |
| 19 | + channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""), | |
| 20 | + nick: process.env.SCUTTLEBOT_NICK || `codex-${baseName}-${sessionSuffix}`, | |
| 21 | + model: process.env.OPENAI_MODEL || "gpt-4.1-mini", | |
| 22 | + backend: process.env.SCUTTLEBOT_LLM_BACKEND || "openai", // default to daemon-stored openai | |
| 23 | +}; | |
| 24 | + | |
| 25 | +for (const [k, v] of Object.entries(cfg)) { | |
| 26 | + if (["backend", "model"].includes(k)) continue; | |
| 27 | + if (!v) { | |
| 28 | + console.error(`missing env: ${k.toUpperCase()}`); | |
| 29 | + process.exit(1); | |
| 30 | + } | |
| 31 | +} | |
| 32 | +const useBackend = !!cfg.backend; | |
| 33 | +if (!useBackend && !process.env.OPENAI_API_KEY) { | |
| 34 | + console.error("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)"); | |
| 35 | + process.exit(1); | |
| 36 | +} | |
| 37 | + | |
| 38 | +const openai = useBackend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); | |
| 39 | +let lastCheck = 0; | |
| 40 | + | |
| 41 | +function mentionsNick(text) { | |
| 42 | + const escaped = cfg.nick.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |
| 43 | + return new RegExp(`(^|[^A-Za-z0-9_./\\\\-])${escaped}($|[^A-Za-z0-9_./\\\\-])`, "i").test(text); | |
| 44 | +} | |
| 45 | + | |
| 46 | +async function relayPost(text) { | |
| 47 | + const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { | |
| 48 | + method: "POST", | |
| 49 | + headers: { | |
| 50 | + Authorization: `Bearer ${cfg.token}`, | |
| 51 | + "Content-Type": "application/json", | |
| 52 | + }, | |
| 53 | + body: JSON.stringify({ text, nick: cfg.nick }), | |
| 54 | + }); | |
| 55 | + if (!res.ok) { | |
| 56 | + throw new Error(`relay post failed: ${res.status} ${res.statusText}`); | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +async function relayPoll() { | |
| 61 | + const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { | |
| 62 | + headers: { Authorization: `Bearer ${cfg.token}` }, | |
| 63 | + }); | |
| 64 | + if (!res.ok) { | |
| 65 | + throw new Error(`relay poll failed: ${res.status} ${res.statusText}`); | |
| 66 | + } | |
| 67 | + const data = await res.json(); | |
| 68 | + const now = Date.now() / 1000; | |
| 69 | + const bots = new Set([ | |
| 70 | + cfg.nick, | |
| 71 | + "bridge", | |
| 72 | + "oracle", | |
| 73 | + "sentinel", | |
| 74 | + "steward", | |
| 75 | + "scribe", | |
| 76 | + "warden", | |
| 77 | + "snitch", | |
| 78 | + "herald", | |
| 79 | + "scroll", | |
| 80 | + "systembot", | |
| 81 | + "auditbot", | |
| 82 | + "claude", | |
| 83 | + ]); | |
| 84 | + const msgs = | |
| 85 | + data.messages?.filter( | |
| 86 | + (m) => | |
| 87 | + !bots.has(m.nick) && | |
| 88 | + !m.nick.startsWith("claude-") && | |
| 89 | + !m.nick.startsWith("codex-") && | |
| 90 | + !m.nick.startsWith("gemini-") && | |
| 91 | + Date.parse(m.at) / 1000 > lastCheck && | |
| 92 | + mentionsNick(m.text) | |
| 93 | + ) || []; | |
| 94 | + lastCheck = now; | |
| 95 | + return msgs; | |
| 96 | +} | |
| 97 | + | |
| 98 | +async function main() { | |
| 99 | + await relayPost(`starting: ${prompt}`); | |
| 100 | + | |
| 101 | + let reply; | |
| 102 | + if (useBackend) { | |
| 103 | + const res = await fetch(`${cfg.url}/v1/llm/complete`, { | |
| 104 | + method: "POST", | |
| 105 | + headers: { | |
| 106 | + Authorization: `Bearer ${cfg.token}`, | |
| 107 | + "Content-Type": "application/json", | |
| 108 | + }, | |
| 109 | + body: JSON.stringify({ backend: cfg.backend, prompt }), | |
| 110 | + }); | |
| 111 | + if (!res.ok) throw new Error(`llm complete failed: ${res.status} ${res.statusText}`); | |
| 112 | + const body = await res.json(); | |
| 113 | + reply = body.text; | |
| 114 | + } else { | |
| 115 | + const completion = await openai.chat.completions.create({ | |
| 116 | + model: cfg.model, | |
| 117 | + messages: [{ role: "user", content: prompt }], | |
| 118 | + }); | |
| 119 | + reply = completion.choices[0].message.content; | |
| 120 | + } | |
| 121 | + console.log(`OpenAI: ${reply}`); | |
| 122 | + | |
| 123 | + await relayPost(`OpenAI reply: ${reply}`); | |
| 124 | + | |
| 125 | + const instructions = await relayPoll(); | |
| 126 | + instructions.forEach((m) => console.log(`[IRC] ${m.nick}: ${m.text}`)); | |
| 127 | +} | |
| 128 | + | |
| 129 | +main().catch((err) => { | |
| 130 | + console.error(err); | |
| 131 | + process.exit(1); | |
| 132 | +}); |
| --- a/skills/openai-relay/scripts/node-openai-relay.mjs | |
| +++ b/skills/openai-relay/scripts/node-openai-relay.mjs | |
| @@ -0,0 +1,132 @@ | |
| --- a/skills/openai-relay/scripts/node-openai-relay.mjs | |
| +++ b/skills/openai-relay/scripts/node-openai-relay.mjs | |
| @@ -0,0 +1,132 @@ | |
| 1 | #!/usr/bin/env node |
| 2 | // Minimal OpenAI + scuttlebot relay example (Node 18+). |
| 3 | // Requires env: SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL. |
| 4 | // Optional: SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_API_KEY. |
| 5 | |
| 6 | import OpenAI from "openai"; |
| 7 | import path from "node:path"; |
| 8 | |
| 9 | const prompt = process.argv[2] || "Hello from openai-relay"; |
| 10 | const sanitize = (value) => value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, ""); |
| 11 | const baseName = sanitize(path.basename(process.cwd()) || "repo"); |
| 12 | const sessionSuffix = sanitize( |
| 13 | process.env.SCUTTLEBOT_SESSION_ID || process.env.CODEX_SESSION_ID || String(process.ppid || process.pid) |
| 14 | ) || "session"; |
| 15 | |
| 16 | const cfg = { |
| 17 | url: process.env.SCUTTLEBOT_URL, |
| 18 | token: process.env.SCUTTLEBOT_TOKEN, |
| 19 | channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""), |
| 20 | nick: process.env.SCUTTLEBOT_NICK || `codex-${baseName}-${sessionSuffix}`, |
| 21 | model: process.env.OPENAI_MODEL || "gpt-4.1-mini", |
| 22 | backend: process.env.SCUTTLEBOT_LLM_BACKEND || "openai", // default to daemon-stored openai |
| 23 | }; |
| 24 | |
| 25 | for (const [k, v] of Object.entries(cfg)) { |
| 26 | if (["backend", "model"].includes(k)) continue; |
| 27 | if (!v) { |
| 28 | console.error(`missing env: ${k.toUpperCase()}`); |
| 29 | process.exit(1); |
| 30 | } |
| 31 | } |
| 32 | const useBackend = !!cfg.backend; |
| 33 | if (!useBackend && !process.env.OPENAI_API_KEY) { |
| 34 | console.error("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)"); |
| 35 | process.exit(1); |
| 36 | } |
| 37 | |
| 38 | const openai = useBackend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); |
| 39 | let lastCheck = 0; |
| 40 | |
| 41 | function mentionsNick(text) { |
| 42 | const escaped = cfg.nick.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 43 | return new RegExp(`(^|[^A-Za-z0-9_./\\\\-])${escaped}($|[^A-Za-z0-9_./\\\\-])`, "i").test(text); |
| 44 | } |
| 45 | |
| 46 | async function relayPost(text) { |
| 47 | const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
| 48 | method: "POST", |
| 49 | headers: { |
| 50 | Authorization: `Bearer ${cfg.token}`, |
| 51 | "Content-Type": "application/json", |
| 52 | }, |
| 53 | body: JSON.stringify({ text, nick: cfg.nick }), |
| 54 | }); |
| 55 | if (!res.ok) { |
| 56 | throw new Error(`relay post failed: ${res.status} ${res.statusText}`); |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | async function relayPoll() { |
| 61 | const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
| 62 | headers: { Authorization: `Bearer ${cfg.token}` }, |
| 63 | }); |
| 64 | if (!res.ok) { |
| 65 | throw new Error(`relay poll failed: ${res.status} ${res.statusText}`); |
| 66 | } |
| 67 | const data = await res.json(); |
| 68 | const now = Date.now() / 1000; |
| 69 | const bots = new Set([ |
| 70 | cfg.nick, |
| 71 | "bridge", |
| 72 | "oracle", |
| 73 | "sentinel", |
| 74 | "steward", |
| 75 | "scribe", |
| 76 | "warden", |
| 77 | "snitch", |
| 78 | "herald", |
| 79 | "scroll", |
| 80 | "systembot", |
| 81 | "auditbot", |
| 82 | "claude", |
| 83 | ]); |
| 84 | const msgs = |
| 85 | data.messages?.filter( |
| 86 | (m) => |
| 87 | !bots.has(m.nick) && |
| 88 | !m.nick.startsWith("claude-") && |
| 89 | !m.nick.startsWith("codex-") && |
| 90 | !m.nick.startsWith("gemini-") && |
| 91 | Date.parse(m.at) / 1000 > lastCheck && |
| 92 | mentionsNick(m.text) |
| 93 | ) || []; |
| 94 | lastCheck = now; |
| 95 | return msgs; |
| 96 | } |
| 97 | |
| 98 | async function main() { |
| 99 | await relayPost(`starting: ${prompt}`); |
| 100 | |
| 101 | let reply; |
| 102 | if (useBackend) { |
| 103 | const res = await fetch(`${cfg.url}/v1/llm/complete`, { |
| 104 | method: "POST", |
| 105 | headers: { |
| 106 | Authorization: `Bearer ${cfg.token}`, |
| 107 | "Content-Type": "application/json", |
| 108 | }, |
| 109 | body: JSON.stringify({ backend: cfg.backend, prompt }), |
| 110 | }); |
| 111 | if (!res.ok) throw new Error(`llm complete failed: ${res.status} ${res.statusText}`); |
| 112 | const body = await res.json(); |
| 113 | reply = body.text; |
| 114 | } else { |
| 115 | const completion = await openai.chat.completions.create({ |
| 116 | model: cfg.model, |
| 117 | messages: [{ role: "user", content: prompt }], |
| 118 | }); |
| 119 | reply = completion.choices[0].message.content; |
| 120 | } |
| 121 | console.log(`OpenAI: ${reply}`); |
| 122 | |
| 123 | await relayPost(`OpenAI reply: ${reply}`); |
| 124 | |
| 125 | const instructions = await relayPoll(); |
| 126 | instructions.forEach((m) => console.log(`[IRC] ${m.nick}: ${m.text}`)); |
| 127 | } |
| 128 | |
| 129 | main().catch((err) => { |
| 130 | console.error(err); |
| 131 | process.exit(1); |
| 132 | }); |
| --- a/skills/openai-relay/scripts/python-openai-relay.py | ||
| +++ b/skills/openai-relay/scripts/python-openai-relay.py | ||
| @@ -0,0 +1,142 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""Minimal OpenAI + scuttlebot relay example. | |
| 3 | + | |
| 4 | +Env required: | |
| 5 | + SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL | |
| 6 | +Optional: | |
| 7 | + SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_MODEL (default: gpt-4.1-mini) | |
| 8 | +""" | |
| 9 | +import os | |
| 10 | +import re | |
| 11 | +import sys | |
| 12 | +import time | |
| 13 | +from datetime import datetime | |
| 14 | +import requests | |
| 15 | +from openai import OpenAI | |
| 16 | + | |
| 17 | +prompt = sys.argv[1] if len(sys.argv) > 1 else "Hello from openai-relay" | |
| 18 | + | |
| 19 | + | |
| 20 | +def sanitize(value: str) -> str: | |
| 21 | + return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-") or "session" | |
| 22 | + | |
| 23 | + | |
| 24 | +base_name = sanitize(os.path.basename(os.getcwd()) or "repo") | |
| 25 | +session_suffix = sanitize( | |
| 26 | + os.environ.get("SCUTTLEBOT_SESSION_ID") | |
| 27 | + or os.environ.get("CODEX_SESSION_ID") | |
| 28 | + or str(os.getppid()) | |
| 29 | +) | |
| 30 | + | |
| 31 | +cfg = { | |
| 32 | + "url": os.environ.get("SCUTTLEBOT_URL"), | |
| 33 | + "token": os.environ.get("SCUTTLEBOT_TOKEN"), | |
| 34 | + "channel": (os.environ.get("SCUTTLEBOT_CHANNEL", "general")).lstrip("#"), | |
| 35 | + "nick": os.environ.get( | |
| 36 | + "SCUTTLEBOT_NICK", f"codex-{base_name}-{session_suffix}" | |
| 37 | + ), | |
| 38 | + "model": os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"), | |
| 39 | + "backend": os.environ.get("SCUTTLEBOT_LLM_BACKEND", "openai"), # default to daemon-stored openai backend | |
| 40 | +} | |
| 41 | + | |
| 42 | +missing = [k for k, v in cfg.items() if not v and k != "model"] | |
| 43 | +use_backend = bool(cfg["backend"]) | |
| 44 | +if missing: | |
| 45 | + print(f"missing env: {', '.join(missing)}", file=sys.stderr) | |
| 46 | + sys.exit(1) | |
| 47 | +if not use_backend and "OPENAI_API_KEY" not in os.environ: | |
| 48 | + print("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)", file=sys.stderr) | |
| 49 | + sys.exit(1) | |
| 50 | + | |
| 51 | +client = None if use_backend else OpenAI(api_key=os.environ["OPENAI_API_KEY"]) | |
| 52 | +last_check = 0.0 | |
| 53 | +mention_re = re.compile( | |
| 54 | + rf"(^|[^A-Za-z0-9_./\\-]){re.escape(cfg['nick'])}($|[^A-Za-z0-9_./\\-])", | |
| 55 | + re.IGNORECASE, | |
| 56 | +) | |
| 57 | + | |
| 58 | + | |
| 59 | +def relay_post(text: str) -> None: | |
| 60 | + res = requests.post( | |
| 61 | + f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", | |
| 62 | + headers={ | |
| 63 | + "Authorization": f"Bearer {cfg['token']}", | |
| 64 | + "Content-Type": "application/json", | |
| 65 | + }, | |
| 66 | + json={"text": text, "nick": cfg["nick"]}, | |
| 67 | + timeout=10, | |
| 68 | + ) | |
| 69 | + res.raise_for_status() | |
| 70 | + | |
| 71 | + | |
| 72 | +def relay_poll(): | |
| 73 | + global last_check | |
| 74 | + res = requests.get( | |
| 75 | + f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", | |
| 76 | + headers={"Authorization": f"Bearer {cfg['token']}"}, | |
| 77 | + timeout=10, | |
| 78 | + ) | |
| 79 | + res.raise_for_status() | |
| 80 | + data = res.json() | |
| 81 | + now = time.time() | |
| 82 | + bots = { | |
| 83 | + cfg["nick"], | |
| 84 | + "bridge", | |
| 85 | + "oracle", | |
| 86 | + "sentinel", | |
| 87 | + "steward", | |
| 88 | + "scribe", | |
| 89 | + "warden", | |
| 90 | + "snitch", | |
| 91 | + "herald", | |
| 92 | + "scroll", | |
| 93 | + "systembot", | |
| 94 | + "auditbot", | |
| 95 | + "claude", | |
| 96 | + } | |
| 97 | + msgs = [ | |
| 98 | + m | |
| 99 | + for m in data.get("messages", []) | |
| 100 | + if m["nick"] not in bots | |
| 101 | + and not m["nick"].startswith("claude-") | |
| 102 | + and not m["nick"].startswith("codex-") | |
| 103 | + and not m["nick"].startswith("gemini-") | |
| 104 | + and datetime.fromisoformat(m["at"].replace("Z", "+00:00")).timestamp() > last_check | |
| 105 | + and mention_re.search(m["text"]) | |
| 106 | + ] | |
| 107 | + last_check = now | |
| 108 | + return msgs | |
| 109 | + | |
| 110 | + | |
| 111 | +def main(): | |
| 112 | + relay_post(f"starting: {prompt}") | |
| 113 | + if use_backend: | |
| 114 | + res = requests.post( | |
| 115 | + f"{cfg['url']}/v1/llm/complete", | |
| 116 | + headers={ | |
| 117 | + "Authorization": f"Bearer {cfg['token']}", | |
| 118 | + "Content-Type": "application/json", | |
| 119 | + }, | |
| 120 | + json={"backend": cfg["backend"], "prompt": prompt}, | |
| 121 | + timeout=20, | |
| 122 | + ) | |
| 123 | + res.raise_for_status() | |
| 124 | + reply = res.json()["text"] | |
| 125 | + else: | |
| 126 | + completion = client.chat.completions.create( | |
| 127 | + model=cfg["model"], | |
| 128 | + messages=[{"role": "user", "content": prompt}], | |
| 129 | + ) | |
| 130 | + reply = completion.choices[0].message.content | |
| 131 | + print(f"OpenAI: {reply}") | |
| 132 | + relay_post(f"OpenAI reply: {reply}") | |
| 133 | + for m in relay_poll(): | |
| 134 | + print(f"[IRC] {m['nick']}: {m['text']}") | |
| 135 | + | |
| 136 | + | |
| 137 | +if __name__ == "__main__": | |
| 138 | + try: | |
| 139 | + main() | |
| 140 | + except Exception as exc: # broad but fine for CLI sample | |
| 141 | + print(exc, file=sys.stderr) | |
| 142 | + sys.exit(1) |
| --- a/skills/openai-relay/scripts/python-openai-relay.py | |
| +++ b/skills/openai-relay/scripts/python-openai-relay.py | |
| @@ -0,0 +1,142 @@ | |
| --- a/skills/openai-relay/scripts/python-openai-relay.py | |
| +++ b/skills/openai-relay/scripts/python-openai-relay.py | |
| @@ -0,0 +1,142 @@ | |
| 1 | #!/usr/bin/env python3 |
| 2 | """Minimal OpenAI + scuttlebot relay example. |
| 3 | |
| 4 | Env required: |
| 5 | SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL |
| 6 | Optional: |
| 7 | SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_MODEL (default: gpt-4.1-mini) |
| 8 | """ |
| 9 | import os |
| 10 | import re |
| 11 | import sys |
| 12 | import time |
| 13 | from datetime import datetime |
| 14 | import requests |
| 15 | from openai import OpenAI |
| 16 | |
| 17 | prompt = sys.argv[1] if len(sys.argv) > 1 else "Hello from openai-relay" |
| 18 | |
| 19 | |
| 20 | def sanitize(value: str) -> str: |
| 21 | return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-") or "session" |
| 22 | |
| 23 | |
| 24 | base_name = sanitize(os.path.basename(os.getcwd()) or "repo") |
| 25 | session_suffix = sanitize( |
| 26 | os.environ.get("SCUTTLEBOT_SESSION_ID") |
| 27 | or os.environ.get("CODEX_SESSION_ID") |
| 28 | or str(os.getppid()) |
| 29 | ) |
| 30 | |
| 31 | cfg = { |
| 32 | "url": os.environ.get("SCUTTLEBOT_URL"), |
| 33 | "token": os.environ.get("SCUTTLEBOT_TOKEN"), |
| 34 | "channel": (os.environ.get("SCUTTLEBOT_CHANNEL", "general")).lstrip("#"), |
| 35 | "nick": os.environ.get( |
| 36 | "SCUTTLEBOT_NICK", f"codex-{base_name}-{session_suffix}" |
| 37 | ), |
| 38 | "model": os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"), |
| 39 | "backend": os.environ.get("SCUTTLEBOT_LLM_BACKEND", "openai"), # default to daemon-stored openai backend |
| 40 | } |
| 41 | |
| 42 | missing = [k for k, v in cfg.items() if not v and k != "model"] |
| 43 | use_backend = bool(cfg["backend"]) |
| 44 | if missing: |
| 45 | print(f"missing env: {', '.join(missing)}", file=sys.stderr) |
| 46 | sys.exit(1) |
| 47 | if not use_backend and "OPENAI_API_KEY" not in os.environ: |
| 48 | print("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)", file=sys.stderr) |
| 49 | sys.exit(1) |
| 50 | |
| 51 | client = None if use_backend else OpenAI(api_key=os.environ["OPENAI_API_KEY"]) |
| 52 | last_check = 0.0 |
| 53 | mention_re = re.compile( |
| 54 | rf"(^|[^A-Za-z0-9_./\\-]){re.escape(cfg['nick'])}($|[^A-Za-z0-9_./\\-])", |
| 55 | re.IGNORECASE, |
| 56 | ) |
| 57 | |
| 58 | |
| 59 | def relay_post(text: str) -> None: |
| 60 | res = requests.post( |
| 61 | f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
| 62 | headers={ |
| 63 | "Authorization": f"Bearer {cfg['token']}", |
| 64 | "Content-Type": "application/json", |
| 65 | }, |
| 66 | json={"text": text, "nick": cfg["nick"]}, |
| 67 | timeout=10, |
| 68 | ) |
| 69 | res.raise_for_status() |
| 70 | |
| 71 | |
| 72 | def relay_poll(): |
| 73 | global last_check |
| 74 | res = requests.get( |
| 75 | f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
| 76 | headers={"Authorization": f"Bearer {cfg['token']}"}, |
| 77 | timeout=10, |
| 78 | ) |
| 79 | res.raise_for_status() |
| 80 | data = res.json() |
| 81 | now = time.time() |
| 82 | bots = { |
| 83 | cfg["nick"], |
| 84 | "bridge", |
| 85 | "oracle", |
| 86 | "sentinel", |
| 87 | "steward", |
| 88 | "scribe", |
| 89 | "warden", |
| 90 | "snitch", |
| 91 | "herald", |
| 92 | "scroll", |
| 93 | "systembot", |
| 94 | "auditbot", |
| 95 | "claude", |
| 96 | } |
| 97 | msgs = [ |
| 98 | m |
| 99 | for m in data.get("messages", []) |
| 100 | if m["nick"] not in bots |
| 101 | and not m["nick"].startswith("claude-") |
| 102 | and not m["nick"].startswith("codex-") |
| 103 | and not m["nick"].startswith("gemini-") |
| 104 | and datetime.fromisoformat(m["at"].replace("Z", "+00:00")).timestamp() > last_check |
| 105 | and mention_re.search(m["text"]) |
| 106 | ] |
| 107 | last_check = now |
| 108 | return msgs |
| 109 | |
| 110 | |
| 111 | def main(): |
| 112 | relay_post(f"starting: {prompt}") |
| 113 | if use_backend: |
| 114 | res = requests.post( |
| 115 | f"{cfg['url']}/v1/llm/complete", |
| 116 | headers={ |
| 117 | "Authorization": f"Bearer {cfg['token']}", |
| 118 | "Content-Type": "application/json", |
| 119 | }, |
| 120 | json={"backend": cfg["backend"], "prompt": prompt}, |
| 121 | timeout=20, |
| 122 | ) |
| 123 | res.raise_for_status() |
| 124 | reply = res.json()["text"] |
| 125 | else: |
| 126 | completion = client.chat.completions.create( |
| 127 | model=cfg["model"], |
| 128 | messages=[{"role": "user", "content": prompt}], |
| 129 | ) |
| 130 | reply = completion.choices[0].message.content |
| 131 | print(f"OpenAI: {reply}") |
| 132 | relay_post(f"OpenAI reply: {reply}") |
| 133 | for m in relay_poll(): |
| 134 | print(f"[IRC] {m['nick']}: {m['text']}") |
| 135 | |
| 136 | |
| 137 | if __name__ == "__main__": |
| 138 | try: |
| 139 | main() |
| 140 | except Exception as exc: # broad but fine for CLI sample |
| 141 | print(exc, file=sys.stderr) |
| 142 | sys.exit(1) |
| --- a/skills/scuttlebot-relay/ADDING_AGENTS.md | ||
| +++ b/skills/scuttlebot-relay/ADDING_AGENTS.md | ||
| @@ -0,0 +1,125 @@ | ||
| 1 | +# Adding Another Agent Runtconcrete operator-control implementations: | |
| 2 | +- Claude hooks in `skills/scuttlebot-relay/hooks/ Adding Another Agent Runtime | |
| 3 | + | |
| 4 | +This repo now has two reusable relay shapes: | |
| 5 | +- terminal-session brokers in `cs in `cmd/*-agent/` | |
| 6 | + | |
| 7 | +Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that | |
| 8 | +before writing another relay client by hand. | |
| 9 | + | |
| 10 | +If you add another live terminal runtime, do not invent a new relay model. | |
| 11 | +Codex and Gemini are the current reference implementations for the terminal | |
| 12 | +broker pattern, and Claude now follows the same layout. New runtimes should | |
| 13 | +match the same repo paths, naming, and environment contract so operators get | |
| 14 | +one consistent experience. | |
| 15 | + | |
| 16 | +## Canonical terminal-broker layout | |
| 17 | + | |
| 18 | +For a local interactive runtime, follow this repo layout: | |
| 19 | + | |
| 20 | +```text | |
| 21 | +cmd/{runtime}-relay/main.go | |
| 22 | +skills/{runtime}-relay/ | |
| 23 | + install.md | |
| 24 | + FLEET.md | |
| 25 | + hooks/ | |
| 26 | + README.md | |
| 27 | + scuttlebot-check.sh | |
| 28 | + scuttlebot-post.sh | |
| 29 | + ...runtime-specific reply hooks if needed | |
| 30 | + scripts/ | |
| 31 | + install-{runtime}-relay.sh | |
| 32 | +pkg/sessionrelay/ | |
| 33 | +``` | |
| 34 | + | |
| 35 | +Conventions: | |
| 36 | +- `cmd/{runtime}-relay/main.go` is the broker entrypoint | |
| 37 | +- `skills/{runtime}-relay/install.md` is the human install primer | |
| 38 | +- `skills/{runtime}-relay/FLEET.md` is the rollout and operations guide | |
| 39 | +- `skills/{runtime}-relay/hooks/README.md` documents the runtime-specific hook contract | |
| 40 | +- `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh` is the tracked installer | |
| 41 | +- installed files under `~/.{runtime}/`, `~/.local/bin/`, and `~/.config/` are copies, not the source of truth | |
| 42 | + | |
| 43 | +Use `pkg/sessionrelay/` for channel send/receivresident agents in `pkg/ircagent/` with thin wrappers in `cmd/*-agent/` | |
| 44 | + | |
| 45 | +Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that | |
| 46 | +before writing another relay client by hand. | |
| 47 | + | |
| 48 | +If you add another live terminal runtime, do not invent a new relay model. | |
| 49 | +Codex and Gemini are the current reference implementations for the terminal | |
| 50 | +broker pattern, and Claude now follows the same layout. New runtimes should | |
| 51 | +match the same repo paths, naming, and environment contract so operators get | |
| 52 | +one consistent experience. | |
| 53 | + | |
| 54 | +## Canonical terminal-broker layout | |
| 55 | + | |
| 56 | +For# Adding Another Agent Runtime | |
| 57 | + | |
| 58 | +This repo now has two reusable relay shapes: | |
| 59 | +- terminal-session brokers in `cmd/claude-relay/`, `cmd/codex-relay/`, and `cmd/gemini-relay/` | |
| 60 | +- IRC-resident agents in `pkg/ircagent/` with thin wrappers in `cmd/*-agent/` | |
| 61 | + | |
| 62 | +Shared transport/runtime cod | |
| 63 | +Do not hardcode tokens into repo scripts. | |
| 64 | + | |
| 65 | + | |
| 66 | +Examples: | |
| 67 | +- Claude Code: `PostToolUse` and `PreToolUse` | |
| 68 | +- Codex: `post-tool-use` and `pre-tool-use` | |
| 69 | + | |
| 70 | +If the runtime has no native pre-action interception point, you need an explicit | |
| 71 | +poll call inside its step loop. Document that clearly as weaker than the hook path. | |
| 72 | + | |
| 73 | +If the runtime has no native startup hook, use the launcher wrapper for `online` | |
| 74 | +and `offline` presence instead of trying to fake it inside the action hooks. | |
| 75 | + | |
| 76 | +If the runtime is an interactive terminal application and you want operators to | |
| 77 | +talk to the live session mid-work, prefer a PTY/session broker over hook-only | |
| 78 | +delivery. The broker should own: | |
| 79 | +- session presence (`online` / `offline`) | |
| 80 | +- continuous operator input injection | |
| 81 | +- outbound activity mirroring | |
| 82 | + | |
| 83 | +Hooks are still useful for pre-action fallback and runtimes without richer | |
| 84 | +integration points, but they do not replace continuous stdin injection or | |
| 85 | +broker-owned activity streaming. | |
| 86 | + | |
| 87 | +## Reference implementation checklist | |
| 88 | + | |
| 89 | +When adding a new runtime, ship all of the following in the repo: | |
| 90 | + | |
| 91 | +1. Hook or relay scripts | |
| 92 | +2. A launcher wrapper or broke# Adding Another Agent Runtime | |
| 93 | +in wrappers in `cmd/*-agent/` | |
| 94 | + | |
| 95 | +Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that | |
| 96 | +before writing another relay client by hand. | |
| 97 | + | |
| 98 | +If you add another live terminal runtime, do not invent a new relay model. | |
| 99 | +Codex and Gemini are the current reference implementations for the terminal | |
| 100 | +broker pattern, and Claude now follows the same layout. New runtimes should | |
| 101 | +match the same repo paths, naming, and environment contract so operators get | |
| 102 | +one consistent experience. | |
| 103 | + | |
| 104 | +## Canonical terminal-broker layout | |
| 105 | + | |
| 106 | +For a local interactive runtime, follow this repo layout: | |
| 107 | + | |
| 108 | +```text | |
| 109 | +cmd/{runtime}-relay/main.go | |
| 110 | +skills/{runtime}-relay/ | |
| 111 | + install.md | |
| 112 | + FLEET.md | |
| 113 | + hooks/ | |
| 114 | + README.md | |
| 115 | + scuttlebot-check.sh | |
| 116 | + scuttlebot-post.sh | |
| 117 | + ...runtime-specific reply hooks if needed | |
| 118 | + scripts/ | |
| 119 | + install-{runtime}-relay.sh | |
| 120 | +pkg/sessionrelay/ | |
| 121 | +``` | |
| 122 | + | |
| 123 | +Conventions: | |
| 124 | +- `cmd/{runtime}-relay/main.go` is the broker entrypoint | |
| 125 | +- `skills/{runtime}-relay/install.md` is the huma |
| --- a/skills/scuttlebot-relay/ADDING_AGENTS.md | |
| +++ b/skills/scuttlebot-relay/ADDING_AGENTS.md | |
| @@ -0,0 +1,125 @@ | |
| --- a/skills/scuttlebot-relay/ADDING_AGENTS.md | |
| +++ b/skills/scuttlebot-relay/ADDING_AGENTS.md | |
| @@ -0,0 +1,125 @@ | |
| 1 | # Adding Another Agent Runtconcrete operator-control implementations: |
| 2 | - Claude hooks in `skills/scuttlebot-relay/hooks/ Adding Another Agent Runtime |
| 3 | |
| 4 | This repo now has two reusable relay shapes: |
| 5 | - terminal-session brokers in `cs in `cmd/*-agent/` |
| 6 | |
| 7 | Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that |
| 8 | before writing another relay client by hand. |
| 9 | |
| 10 | If you add another live terminal runtime, do not invent a new relay model. |
| 11 | Codex and Gemini are the current reference implementations for the terminal |
| 12 | broker pattern, and Claude now follows the same layout. New runtimes should |
| 13 | match the same repo paths, naming, and environment contract so operators get |
| 14 | one consistent experience. |
| 15 | |
| 16 | ## Canonical terminal-broker layout |
| 17 | |
| 18 | For a local interactive runtime, follow this repo layout: |
| 19 | |
| 20 | ```text |
| 21 | cmd/{runtime}-relay/main.go |
| 22 | skills/{runtime}-relay/ |
| 23 | install.md |
| 24 | FLEET.md |
| 25 | hooks/ |
| 26 | README.md |
| 27 | scuttlebot-check.sh |
| 28 | scuttlebot-post.sh |
| 29 | ...runtime-specific reply hooks if needed |
| 30 | scripts/ |
| 31 | install-{runtime}-relay.sh |
| 32 | pkg/sessionrelay/ |
| 33 | ``` |
| 34 | |
| 35 | Conventions: |
| 36 | - `cmd/{runtime}-relay/main.go` is the broker entrypoint |
| 37 | - `skills/{runtime}-relay/install.md` is the human install primer |
| 38 | - `skills/{runtime}-relay/FLEET.md` is the rollout and operations guide |
| 39 | - `skills/{runtime}-relay/hooks/README.md` documents the runtime-specific hook contract |
| 40 | - `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh` is the tracked installer |
| 41 | - installed files under `~/.{runtime}/`, `~/.local/bin/`, and `~/.config/` are copies, not the source of truth |
| 42 | |
| 43 | Use `pkg/sessionrelay/` for channel send/receivresident agents in `pkg/ircagent/` with thin wrappers in `cmd/*-agent/` |
| 44 | |
| 45 | Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that |
| 46 | before writing another relay client by hand. |
| 47 | |
| 48 | If you add another live terminal runtime, do not invent a new relay model. |
| 49 | Codex and Gemini are the current reference implementations for the terminal |
| 50 | broker pattern, and Claude now follows the same layout. New runtimes should |
| 51 | match the same repo paths, naming, and environment contract so operators get |
| 52 | one consistent experience. |
| 53 | |
| 54 | ## Canonical terminal-broker layout |
| 55 | |
| 56 | For# Adding Another Agent Runtime |
| 57 | |
| 58 | This repo now has two reusable relay shapes: |
| 59 | - terminal-session brokers in `cmd/claude-relay/`, `cmd/codex-relay/`, and `cmd/gemini-relay/` |
| 60 | - IRC-resident agents in `pkg/ircagent/` with thin wrappers in `cmd/*-agent/` |
| 61 | |
| 62 | Shared transport/runtime cod |
| 63 | Do not hardcode tokens into repo scripts. |
| 64 | |
| 65 | |
| 66 | Examples: |
| 67 | - Claude Code: `PostToolUse` and `PreToolUse` |
| 68 | - Codex: `post-tool-use` and `pre-tool-use` |
| 69 | |
| 70 | If the runtime has no native pre-action interception point, you need an explicit |
| 71 | poll call inside its step loop. Document that clearly as weaker than the hook path. |
| 72 | |
| 73 | If the runtime has no native startup hook, use the launcher wrapper for `online` |
| 74 | and `offline` presence instead of trying to fake it inside the action hooks. |
| 75 | |
| 76 | If the runtime is an interactive terminal application and you want operators to |
| 77 | talk to the live session mid-work, prefer a PTY/session broker over hook-only |
| 78 | delivery. The broker should own: |
| 79 | - session presence (`online` / `offline`) |
| 80 | - continuous operator input injection |
| 81 | - outbound activity mirroring |
| 82 | |
| 83 | Hooks are still useful for pre-action fallback and runtimes without richer |
| 84 | integration points, but they do not replace continuous stdin injection or |
| 85 | broker-owned activity streaming. |
| 86 | |
| 87 | ## Reference implementation checklist |
| 88 | |
| 89 | When adding a new runtime, ship all of the following in the repo: |
| 90 | |
| 91 | 1. Hook or relay scripts |
| 92 | 2. A launcher wrapper or broke# Adding Another Agent Runtime |
| 93 | in wrappers in `cmd/*-agent/` |
| 94 | |
| 95 | Shared transport/runtime code now lives in `pkg/sessionrelay/`. Reuse that |
| 96 | before writing another relay client by hand. |
| 97 | |
| 98 | If you add another live terminal runtime, do not invent a new relay model. |
| 99 | Codex and Gemini are the current reference implementations for the terminal |
| 100 | broker pattern, and Claude now follows the same layout. New runtimes should |
| 101 | match the same repo paths, naming, and environment contract so operators get |
| 102 | one consistent experience. |
| 103 | |
| 104 | ## Canonical terminal-broker layout |
| 105 | |
| 106 | For a local interactive runtime, follow this repo layout: |
| 107 | |
| 108 | ```text |
| 109 | cmd/{runtime}-relay/main.go |
| 110 | skills/{runtime}-relay/ |
| 111 | install.md |
| 112 | FLEET.md |
| 113 | hooks/ |
| 114 | README.md |
| 115 | scuttlebot-check.sh |
| 116 | scuttlebot-post.sh |
| 117 | ...runtime-specific reply hooks if needed |
| 118 | scripts/ |
| 119 | install-{runtime}-relay.sh |
| 120 | pkg/sessionrelay/ |
| 121 | ``` |
| 122 | |
| 123 | Conventions: |
| 124 | - `cmd/{runtime}-relay/main.go` is the broker entrypoint |
| 125 | - `skills/{runtime}-relay/install.md` is the huma |