ScuttleBot

Add codex relay broker and install primers

lmata 2026-04-01 04:23 trunk
Commit 50baf1a5833ef0a2c3f18771073d708ed9e2bcda72f713678804a5f66c41cf43
--- 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
--- 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
--- 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)
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button