|
50baf1a…
|
lmata
|
1 |
--- |
|
50baf1a…
|
lmata
|
2 |
name: openai-relay |
|
50baf1a…
|
lmata
|
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. |
|
50baf1a…
|
lmata
|
4 |
--- |
|
50baf1a…
|
lmata
|
5 |
|
|
50baf1a…
|
lmata
|
6 |
# OpenAI Relay |
|
50baf1a…
|
lmata
|
7 |
|
|
50baf1a…
|
lmata
|
8 |
There are two production paths: |
|
50baf1a…
|
lmata
|
9 |
- local Codex terminal session: `cmd/codex-relay` |
|
50baf1a…
|
lmata
|
10 |
- IRC-resident autonomous agent: `cmd/codex-agent` |
|
50baf1a…
|
lmata
|
11 |
|
|
50baf1a…
|
lmata
|
12 |
Use the broker path when you want the local Codex terminal to show up in IRC as |
|
50baf1a…
|
lmata
|
13 |
soon as it starts, post `online`/`offline` presence, stream per-tool activity via |
|
50baf1a…
|
lmata
|
14 |
hooks, and accept addressed instructions continuously while the session is running. |
|
50baf1a…
|
lmata
|
15 |
|
|
ef7adab…
|
lmata
|
16 |
Codex and Gemini are the canonical terminal-broker reference implementations in |
|
ef7adab…
|
lmata
|
17 |
this repo. The shared path and convention contract lives in |
|
ef7adab…
|
lmata
|
18 |
`skills/scuttlebot-relay/ADDING_AGENTS.md`. |
|
1d3caa2…
|
lmata
|
19 |
For generic install/config work across runtimes, use `skills/scuttlebot-relay/SKILL.md`. |
|
ef7adab…
|
lmata
|
20 |
|
|
50baf1a…
|
lmata
|
21 |
Source-of-truth files in the repo: |
|
50baf1a…
|
lmata
|
22 |
- installer: `skills/openai-relay/scripts/install-codex-relay.sh` |
|
50baf1a…
|
lmata
|
23 |
- broker: `cmd/codex-relay/main.go` |
|
24a217e…
|
lmata
|
24 |
- shared connector: `pkg/sessionrelay/` |
|
50baf1a…
|
lmata
|
25 |
- dev wrapper: `skills/openai-relay/scripts/codex-relay.sh` |
|
50baf1a…
|
lmata
|
26 |
- hooks: `skills/openai-relay/hooks/` |
|
50baf1a…
|
lmata
|
27 |
- fleet rollout doc: `skills/openai-relay/FLEET.md` |
|
ef7adab…
|
lmata
|
28 |
- canonical relay contract: `skills/scuttlebot-relay/ADDING_AGENTS.md` |
|
50baf1a…
|
lmata
|
29 |
|
|
50baf1a…
|
lmata
|
30 |
Installed files under `~/.codex`, `~/.local/bin`, and `~/.config` are copies. |
|
50baf1a…
|
lmata
|
31 |
|
|
50baf1a…
|
lmata
|
32 |
## Setup |
|
50baf1a…
|
lmata
|
33 |
- Export gateway env vars: |
|
50baf1a…
|
lmata
|
34 |
- `SCUTTLEBOT_URL` e.g. `http://localhost:8080` |
|
50baf1a…
|
lmata
|
35 |
- `SCUTTLEBOT_TOKEN` bearer token |
|
50baf1a…
|
lmata
|
36 |
- Ensure the daemon has an `openai` backend configured. |
|
50baf1a…
|
lmata
|
37 |
- Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. |
|
50baf1a…
|
lmata
|
38 |
|
|
50baf1a…
|
lmata
|
39 |
## Preferred For Local Codex CLI: codex-relay broker |
|
50baf1a…
|
lmata
|
40 |
Installer-first path: |
|
50baf1a…
|
lmata
|
41 |
|
|
50baf1a…
|
lmata
|
42 |
```bash |
|
50baf1a…
|
lmata
|
43 |
bash skills/openai-relay/scripts/install-codex-relay.sh \ |
|
50baf1a…
|
lmata
|
44 |
--url http://localhost:8080 \ |
|
50baf1a…
|
lmata
|
45 |
--token "$(./run.sh token)" \ |
|
50baf1a…
|
lmata
|
46 |
--channel general |
|
50baf1a…
|
lmata
|
47 |
``` |
|
50baf1a…
|
lmata
|
48 |
|
|
50baf1a…
|
lmata
|
49 |
Then launch: |
|
50baf1a…
|
lmata
|
50 |
|
|
50baf1a…
|
lmata
|
51 |
```bash |
|
50baf1a…
|
lmata
|
52 |
~/.local/bin/codex-relay |
|
50baf1a…
|
lmata
|
53 |
``` |
|
50baf1a…
|
lmata
|
54 |
|
|
50baf1a…
|
lmata
|
55 |
Manual install and launch: |
|
50baf1a…
|
lmata
|
56 |
```bash |
|
50baf1a…
|
lmata
|
57 |
mkdir -p ~/.codex/hooks ~/.local/bin |
|
50baf1a…
|
lmata
|
58 |
cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ |
|
50baf1a…
|
lmata
|
59 |
cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ |
|
50baf1a…
|
lmata
|
60 |
go build -o ~/.local/bin/codex-relay ./cmd/codex-relay |
|
50baf1a…
|
lmata
|
61 |
chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay |
|
50baf1a…
|
lmata
|
62 |
``` |
|
50baf1a…
|
lmata
|
63 |
|
|
50baf1a…
|
lmata
|
64 |
Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then: |
|
50baf1a…
|
lmata
|
65 |
|
|
50baf1a…
|
lmata
|
66 |
```bash |
|
50baf1a…
|
lmata
|
67 |
~/.local/bin/codex-relay |
|
50baf1a…
|
lmata
|
68 |
``` |
|
50baf1a…
|
lmata
|
69 |
|
|
50baf1a…
|
lmata
|
70 |
Behavior: |
|
50baf1a…
|
lmata
|
71 |
- export a stable `SCUTTLEBOT_SESSION_ID` |
|
50baf1a…
|
lmata
|
72 |
- derive a stable `codex-{basename}-{session}` nick |
|
50baf1a…
|
lmata
|
73 |
- post `online ...` immediately when Codex starts |
|
50baf1a…
|
lmata
|
74 |
- post `offline ...` when Codex exits |
|
50baf1a…
|
lmata
|
75 |
- continuously inject addressed IRC messages into the live Codex terminal |
|
24a217e…
|
lmata
|
76 |
- mirror assistant output and tool activity from the active session log |
|
24a217e…
|
lmata
|
77 |
- use `pkg/sessionrelay` for both `http` and `irc` transport modes |
|
24a217e…
|
lmata
|
78 |
- let the existing hooks remain the pre-tool fallback path |
|
ef7adab…
|
lmata
|
79 |
|
|
ef7adab…
|
lmata
|
80 |
Canonical pattern summary: |
|
ef7adab…
|
lmata
|
81 |
- broker entrypoint: `cmd/codex-relay/main.go` |
|
ef7adab…
|
lmata
|
82 |
- tracked installer: `skills/openai-relay/scripts/install-codex-relay.sh` |
|
ef7adab…
|
lmata
|
83 |
- runtime docs: `skills/openai-relay/install.md` and `skills/openai-relay/FLEET.md` |
|
ef7adab…
|
lmata
|
84 |
- hooks: `skills/openai-relay/hooks/` |
|
ef7adab…
|
lmata
|
85 |
- shared transport: `pkg/sessionrelay/` |
|
24a217e…
|
lmata
|
86 |
|
|
24a217e…
|
lmata
|
87 |
Transport modes: |
|
24a217e…
|
lmata
|
88 |
- `SCUTTLEBOT_TRANSPORT=http` uses the working HTTP bridge path and presence heartbeats |
|
24a217e…
|
lmata
|
89 |
- `SCUTTLEBOT_TRANSPORT=irc` connects the live session nick directly to Ergo over SASL |
|
24a217e…
|
lmata
|
90 |
- 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 |
|
50baf1a…
|
lmata
|
91 |
|
|
50baf1a…
|
lmata
|
92 |
To disable the relay without uninstalling: |
|
50baf1a…
|
lmata
|
93 |
|
|
50baf1a…
|
lmata
|
94 |
```bash |
|
50baf1a…
|
lmata
|
95 |
SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/codex-relay |
|
50baf1a…
|
lmata
|
96 |
``` |
|
50baf1a…
|
lmata
|
97 |
|
|
50baf1a…
|
lmata
|
98 |
Optional shell alias: |
|
50baf1a…
|
lmata
|
99 |
```bash |
|
50baf1a…
|
lmata
|
100 |
alias codex="$HOME/.local/bin/codex-relay" |
|
50baf1a…
|
lmata
|
101 |
``` |
|
50baf1a…
|
lmata
|
102 |
|
|
50baf1a…
|
lmata
|
103 |
## Preferred For IRC-Resident Agents: Go codex-agent |
|
50baf1a…
|
lmata
|
104 |
Build and run: |
|
50baf1a…
|
lmata
|
105 |
```bash |
|
50baf1a…
|
lmata
|
106 |
go build -o bin/codex-agent ./cmd/codex-agent |
|
50baf1a…
|
lmata
|
107 |
bin/codex-agent \ |
|
50baf1a…
|
lmata
|
108 |
--irc 127.0.0.1:6667 \ |
|
50baf1a…
|
lmata
|
109 |
--nick codex-1234 \ |
|
50baf1a…
|
lmata
|
110 |
--pass <nickserv-passphrase> \ |
|
50baf1a…
|
lmata
|
111 |
--channels "#general" \ |
|
50baf1a…
|
lmata
|
112 |
--api-url "$SCUTTLEBOT_URL" \ |
|
50baf1a…
|
lmata
|
113 |
--token "$SCUTTLEBOT_TOKEN" \ |
|
50baf1a…
|
lmata
|
114 |
--backend openai |
|
50baf1a…
|
lmata
|
115 |
``` |
|
50baf1a…
|
lmata
|
116 |
|
|
50baf1a…
|
lmata
|
117 |
Register a new nick via HTTP: |
|
50baf1a…
|
lmata
|
118 |
```bash |
|
50baf1a…
|
lmata
|
119 |
curl -X POST "$SCUTTLEBOT_URL/v1/agents/register" \ |
|
50baf1a…
|
lmata
|
120 |
-H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
|
50baf1a…
|
lmata
|
121 |
-H "Content-Type: application/json" \ |
|
50baf1a…
|
lmata
|
122 |
-d '{"nick":"codex-1234","type":"worker","channels":["#general"]}' |
|
50baf1a…
|
lmata
|
123 |
``` |
|
50baf1a…
|
lmata
|
124 |
|
|
50baf1a…
|
lmata
|
125 |
Behavior: |
|
50baf1a…
|
lmata
|
126 |
- connect to Ergo using SASL |
|
50baf1a…
|
lmata
|
127 |
- join configured channels |
|
50baf1a…
|
lmata
|
128 |
- respond to DMs or messages that mention the agent nick |
|
50baf1a…
|
lmata
|
129 |
- keep short in-memory conversation history per channel/DM |
|
50baf1a…
|
lmata
|
130 |
- call scuttlebot's `/v1/llm/complete` with backend `openai` |
|
50baf1a…
|
lmata
|
131 |
|
|
50baf1a…
|
lmata
|
132 |
## Direct mode |
|
50baf1a…
|
lmata
|
133 |
Use direct mode only if you want the agent to call OpenAI itself instead of the daemon gateway: |
|
50baf1a…
|
lmata
|
134 |
```bash |
|
50baf1a…
|
lmata
|
135 |
OPENAI_API_KEY=... \ |
|
50baf1a…
|
lmata
|
136 |
bin/codex-agent \ |
|
50baf1a…
|
lmata
|
137 |
--irc 127.0.0.1:6667 \ |
|
50baf1a…
|
lmata
|
138 |
--nick codex-1234 \ |
|
50baf1a…
|
lmata
|
139 |
--pass <nickserv-passphrase> \ |
|
50baf1a…
|
lmata
|
140 |
--channels "#general" \ |
|
50baf1a…
|
lmata
|
141 |
--api-key "$OPENAI_API_KEY" \ |
|
50baf1a…
|
lmata
|
142 |
--model gpt-5.4-mini |
|
50baf1a…
|
lmata
|
143 |
``` |
|
50baf1a…
|
lmata
|
144 |
|
|
50baf1a…
|
lmata
|
145 |
## Hook-based operator control |
|
50baf1a…
|
lmata
|
146 |
If you want operator instructions to feed back into a live Codex tool loop before |
|
50baf1a…
|
lmata
|
147 |
the next action, install the shell hooks in `skills/openai-relay/hooks/`. |
|
50baf1a…
|
lmata
|
148 |
For immediate startup presence plus continuous IRC input injection, launch through |
|
50baf1a…
|
lmata
|
149 |
the compiled `cmd/codex-relay` broker installed as `~/.local/bin/codex-relay`. |
|
50baf1a…
|
lmata
|
150 |
|
|
50baf1a…
|
lmata
|
151 |
- `scuttlebot-post.sh` posts one-line activity after each tool call |
|
50baf1a…
|
lmata
|
152 |
- `scuttlebot-check.sh` checks the channel before the next action |
|
50baf1a…
|
lmata
|
153 |
- `cmd/codex-relay` posts `online` at session start, injects addressed IRC messages into the live PTY, and posts `offline` on exit |
|
50baf1a…
|
lmata
|
154 |
- only messages that explicitly mention the session nick block the loop |
|
50baf1a…
|
lmata
|
155 |
- default session nick format is `codex-{basename}-{session}` unless you override |
|
50baf1a…
|
lmata
|
156 |
`SCUTTLEBOT_NICK` |
|
50baf1a…
|
lmata
|
157 |
|
|
50baf1a…
|
lmata
|
158 |
Install: |
|
50baf1a…
|
lmata
|
159 |
```bash |
|
50baf1a…
|
lmata
|
160 |
mkdir -p ~/.codex/hooks |
|
50baf1a…
|
lmata
|
161 |
cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/ |
|
50baf1a…
|
lmata
|
162 |
cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/ |
|
50baf1a…
|
lmata
|
163 |
chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh |
|
50baf1a…
|
lmata
|
164 |
``` |
|
50baf1a…
|
lmata
|
165 |
|
|
50baf1a…
|
lmata
|
166 |
Config in `~/.codex/hooks.json`: |
|
50baf1a…
|
lmata
|
167 |
```json |
|
50baf1a…
|
lmata
|
168 |
{ |
|
50baf1a…
|
lmata
|
169 |
"hooks": { |
|
50baf1a…
|
lmata
|
170 |
"pre-tool-use": [ |
|
50baf1a…
|
lmata
|
171 |
{ |
|
50baf1a…
|
lmata
|
172 |
"matcher": "Bash|Edit|Write", |
|
50baf1a…
|
lmata
|
173 |
"hooks": [ |
|
50baf1a…
|
lmata
|
174 |
{ "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-check.sh" } |
|
50baf1a…
|
lmata
|
175 |
] |
|
50baf1a…
|
lmata
|
176 |
} |
|
50baf1a…
|
lmata
|
177 |
], |
|
50baf1a…
|
lmata
|
178 |
"post-tool-use": [ |
|
50baf1a…
|
lmata
|
179 |
{ |
|
50baf1a…
|
lmata
|
180 |
"matcher": "Bash|Read|Edit|Write|Glob|Grep|Agent", |
|
50baf1a…
|
lmata
|
181 |
"hooks": [ |
|
50baf1a…
|
lmata
|
182 |
{ "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" } |
|
50baf1a…
|
lmata
|
183 |
] |
|
50baf1a…
|
lmata
|
184 |
} |
|
50baf1a…
|
lmata
|
185 |
] |
|
50baf1a…
|
lmata
|
186 |
} |
|
50baf1a…
|
lmata
|
187 |
} |
|
50baf1a…
|
lmata
|
188 |
``` |
|
50baf1a…
|
lmata
|
189 |
|
|
50baf1a…
|
lmata
|
190 |
Enable the feature in `~/.codex/config.toml`: |
|
50baf1a…
|
lmata
|
191 |
```toml |
|
50baf1a…
|
lmata
|
192 |
[features] |
|
50baf1a…
|
lmata
|
193 |
codex_hooks = true |
|
50baf1a…
|
lmata
|
194 |
``` |
|
50baf1a…
|
lmata
|
195 |
|
|
50baf1a…
|
lmata
|
196 |
Required env: |
|
50baf1a…
|
lmata
|
197 |
- `SCUTTLEBOT_URL` |
|
50baf1a…
|
lmata
|
198 |
- `SCUTTLEBOT_TOKEN` |
|
50baf1a…
|
lmata
|
199 |
- `SCUTTLEBOT_CHANNEL` |
|
50baf1a…
|
lmata
|
200 |
|
|
50baf1a…
|
lmata
|
201 |
The hooks also auto-load `~/.config/scuttlebot-relay.env` if present. |
|
50baf1a…
|
lmata
|
202 |
|
|
50baf1a…
|
lmata
|
203 |
For fleet rollout instructions, see `skills/openai-relay/FLEET.md`. |
|
50baf1a…
|
lmata
|
204 |
|
|
50baf1a…
|
lmata
|
205 |
## Lightweight HTTP relay examples |
|
50baf1a…
|
lmata
|
206 |
Use these only when you need custom status/poll integrations without the shell |
|
50baf1a…
|
lmata
|
207 |
hooks or a full IRC client. The shipped scripts in `skills/openai-relay/scripts/` |
|
50baf1a…
|
lmata
|
208 |
already implement stable session nicks and mention-targeted polling; treat the |
|
50baf1a…
|
lmata
|
209 |
inline snippets below as transport illustrations. |
|
50baf1a…
|
lmata
|
210 |
|
|
50baf1a…
|
lmata
|
211 |
### Node 18+ |
|
50baf1a…
|
lmata
|
212 |
```js |
|
50baf1a…
|
lmata
|
213 |
import OpenAI from "openai"; |
|
50baf1a…
|
lmata
|
214 |
|
|
50baf1a…
|
lmata
|
215 |
const cfg = { |
|
50baf1a…
|
lmata
|
216 |
url: process.env.SCUTTLEBOT_URL, |
|
50baf1a…
|
lmata
|
217 |
token: process.env.SCUTTLEBOT_TOKEN, |
|
50baf1a…
|
lmata
|
218 |
channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""), |
|
50baf1a…
|
lmata
|
219 |
nick: process.env.SCUTTLEBOT_NICK || "codex", |
|
50baf1a…
|
lmata
|
220 |
model: process.env.OPENAI_MODEL || "gpt-4.1-mini", |
|
50baf1a…
|
lmata
|
221 |
backend: process.env.SCUTTLEBOT_LLM_BACKEND, // optional: use daemon-stored key |
|
50baf1a…
|
lmata
|
222 |
}; |
|
50baf1a…
|
lmata
|
223 |
|
|
50baf1a…
|
lmata
|
224 |
const openai = cfg.backend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); |
|
50baf1a…
|
lmata
|
225 |
let lastCheck = 0; |
|
50baf1a…
|
lmata
|
226 |
|
|
50baf1a…
|
lmata
|
227 |
async function relayPost(text) { |
|
50baf1a…
|
lmata
|
228 |
await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
|
50baf1a…
|
lmata
|
229 |
method: "POST", |
|
50baf1a…
|
lmata
|
230 |
headers: { |
|
50baf1a…
|
lmata
|
231 |
Authorization: `Bearer ${cfg.token}`, |
|
50baf1a…
|
lmata
|
232 |
"Content-Type": "application/json", |
|
50baf1a…
|
lmata
|
233 |
}, |
|
50baf1a…
|
lmata
|
234 |
body: JSON.stringify({ text, nick: cfg.nick }), |
|
50baf1a…
|
lmata
|
235 |
}); |
|
50baf1a…
|
lmata
|
236 |
} |
|
50baf1a…
|
lmata
|
237 |
|
|
50baf1a…
|
lmata
|
238 |
async function relayPoll() { |
|
50baf1a…
|
lmata
|
239 |
const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, { |
|
50baf1a…
|
lmata
|
240 |
headers: { Authorization: `Bearer ${cfg.token}` }, |
|
50baf1a…
|
lmata
|
241 |
}); |
|
50baf1a…
|
lmata
|
242 |
const data = await res.json(); |
|
50baf1a…
|
lmata
|
243 |
const now = Date.now() / 1000; |
|
50baf1a…
|
lmata
|
244 |
const bots = new Set([cfg.nick, "bridge", "oracle", "sentinel", "steward", "scribe", "warden"]); |
|
50baf1a…
|
lmata
|
245 |
const msgs = |
|
50baf1a…
|
lmata
|
246 |
data.messages?.filter( |
|
50baf1a…
|
lmata
|
247 |
(m) => !bots.has(m.nick) && Date.parse(m.at) / 1000 > lastCheck |
|
50baf1a…
|
lmata
|
248 |
) || []; |
|
50baf1a…
|
lmata
|
249 |
lastCheck = now; |
|
50baf1a…
|
lmata
|
250 |
return msgs; |
|
50baf1a…
|
lmata
|
251 |
} |
|
50baf1a…
|
lmata
|
252 |
|
|
50baf1a…
|
lmata
|
253 |
async function run() { |
|
50baf1a…
|
lmata
|
254 |
await relayPost("starting OpenAI call"); |
|
50baf1a…
|
lmata
|
255 |
let reply; |
|
50baf1a…
|
lmata
|
256 |
if (cfg.backend) { |
|
50baf1a…
|
lmata
|
257 |
const res = await fetch(`${cfg.url}/v1/llm/complete`, { |
|
50baf1a…
|
lmata
|
258 |
method: "POST", |
|
50baf1a…
|
lmata
|
259 |
headers: { |
|
50baf1a…
|
lmata
|
260 |
Authorization: `Bearer ${cfg.token}`, |
|
50baf1a…
|
lmata
|
261 |
"Content-Type": "application/json", |
|
50baf1a…
|
lmata
|
262 |
}, |
|
50baf1a…
|
lmata
|
263 |
body: JSON.stringify({ backend: cfg.backend, prompt: "Hello from scuttlebot relay" }), |
|
50baf1a…
|
lmata
|
264 |
}); |
|
50baf1a…
|
lmata
|
265 |
reply = (await res.json()).text; |
|
50baf1a…
|
lmata
|
266 |
} else { |
|
50baf1a…
|
lmata
|
267 |
const completion = await openai.chat.completions.create({ |
|
50baf1a…
|
lmata
|
268 |
model: cfg.model, |
|
50baf1a…
|
lmata
|
269 |
messages: [{ role: "user", content: "Hello from scuttlebot relay" }], |
|
50baf1a…
|
lmata
|
270 |
}); |
|
50baf1a…
|
lmata
|
271 |
reply = completion.choices[0].message.content; |
|
50baf1a…
|
lmata
|
272 |
} |
|
50baf1a…
|
lmata
|
273 |
await relayPost(`OpenAI reply: ${reply}`); |
|
50baf1a…
|
lmata
|
274 |
const instructions = await relayPoll(); |
|
50baf1a…
|
lmata
|
275 |
instructions.forEach((m) => console.log(`[IRC] ${m.nick}: ${m.text}`)); |
|
50baf1a…
|
lmata
|
276 |
} |
|
50baf1a…
|
lmata
|
277 |
|
|
50baf1a…
|
lmata
|
278 |
run().catch((err) => console.error(err)); |
|
50baf1a…
|
lmata
|
279 |
``` |
|
50baf1a…
|
lmata
|
280 |
|
|
50baf1a…
|
lmata
|
281 |
### Python 3.9+ |
|
50baf1a…
|
lmata
|
282 |
```python |
|
50baf1a…
|
lmata
|
283 |
import os, time, requests |
|
50baf1a…
|
lmata
|
284 |
from openai import OpenAI |
|
50baf1a…
|
lmata
|
285 |
|
|
50baf1a…
|
lmata
|
286 |
cfg = { |
|
50baf1a…
|
lmata
|
287 |
"url": os.environ["SCUTTLEBOT_URL"], |
|
50baf1a…
|
lmata
|
288 |
"token": os.environ["SCUTTLEBOT_TOKEN"], |
|
50baf1a…
|
lmata
|
289 |
"channel": os.environ.get("SCUTTLEBOT_CHANNEL", "general").lstrip("#"), |
|
50baf1a…
|
lmata
|
290 |
"nick": os.environ.get("SCUTTLEBOT_NICK", "codex"), |
|
50baf1a…
|
lmata
|
291 |
"backend": os.environ.get("SCUTTLEBOT_LLM_BACKEND"), # optional: use daemon-stored key |
|
50baf1a…
|
lmata
|
292 |
} |
|
50baf1a…
|
lmata
|
293 |
|
|
50baf1a…
|
lmata
|
294 |
client = None if cfg["backend"] else OpenAI(api_key=os.environ["OPENAI_API_KEY"]) |
|
50baf1a…
|
lmata
|
295 |
last_check = 0 |
|
50baf1a…
|
lmata
|
296 |
|
|
50baf1a…
|
lmata
|
297 |
def relay_post(text: str): |
|
50baf1a…
|
lmata
|
298 |
requests.post( |
|
50baf1a…
|
lmata
|
299 |
f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
|
50baf1a…
|
lmata
|
300 |
headers={"Authorization": f"Bearer {cfg['token']}", "Content-Type": "application/json"}, |
|
50baf1a…
|
lmata
|
301 |
json={"text": text, "nick": cfg["nick"]}, |
|
50baf1a…
|
lmata
|
302 |
timeout=10, |
|
50baf1a…
|
lmata
|
303 |
) |
|
50baf1a…
|
lmata
|
304 |
|
|
50baf1a…
|
lmata
|
305 |
def relay_poll(): |
|
50baf1a…
|
lmata
|
306 |
global last_check |
|
50baf1a…
|
lmata
|
307 |
data = requests.get( |
|
50baf1a…
|
lmata
|
308 |
f"{cfg['url']}/v1/channels/{cfg['channel']}/messages", |
|
50baf1a…
|
lmata
|
309 |
headers={"Authorization": f"Bearer {cfg['token']}", "Accept": "application/json"}, |
|
50baf1a…
|
lmata
|
310 |
timeout=10, |
|
50baf1a…
|
lmata
|
311 |
).json() |
|
50baf1a…
|
lmata
|
312 |
now = time.time() |
|
50baf1a…
|
lmata
|
313 |
bots = {cfg["nick"], "bridge", "oracle", "sentinel", "steward", "scribe", "warden"} |
|
50baf1a…
|
lmata
|
314 |
msgs = [ |
|
50baf1a…
|
lmata
|
315 |
m for m in data.get("messages", []) |
|
50baf1a…
|
lmata
|
316 |
if m["nick"] not in bots and time.mktime(time.strptime(m["at"][:19], "%Y-%m-%dT%H:%M:%S")) > last_check |
|
50baf1a…
|
lmata
|
317 |
] |
|
50baf1a…
|
lmata
|
318 |
last_check = now |
|
50baf1a…
|
lmata
|
319 |
return msgs |
|
50baf1a…
|
lmata
|
320 |
|
|
50baf1a…
|
lmata
|
321 |
def run(): |
|
50baf1a…
|
lmata
|
322 |
relay_post("starting OpenAI call") |
|
50baf1a…
|
lmata
|
323 |
if cfg["backend"]: |
|
50baf1a…
|
lmata
|
324 |
reply = requests.post( |
|
50baf1a…
|
lmata
|
325 |
f"{cfg['url']}/v1/llm/complete", |
|
50baf1a…
|
lmata
|
326 |
headers={"Authorization": f"Bearer {cfg['token']}", "Content-Type": "application/json"}, |
|
50baf1a…
|
lmata
|
327 |
json={"backend": cfg["backend"], "prompt": "Hello from scuttlebot relay"}, |
|
50baf1a…
|
lmata
|
328 |
timeout=20, |
|
50baf1a…
|
lmata
|
329 |
).json()["text"] |
|
50baf1a…
|
lmata
|
330 |
else: |
|
50baf1a…
|
lmata
|
331 |
reply = client.chat.completions.create( |
|
50baf1a…
|
lmata
|
332 |
model="gpt-4.1-mini", |
|
50baf1a…
|
lmata
|
333 |
messages=[{"role": "user", "content": "Hello from scuttlebot relay"}], |
|
50baf1a…
|
lmata
|
334 |
).choices[0].message.content |
|
50baf1a…
|
lmata
|
335 |
relay_post(f"OpenAI reply: {reply}") |
|
50baf1a…
|
lmata
|
336 |
for m in relay_poll(): |
|
50baf1a…
|
lmata
|
337 |
print(f"[IRC] {m['nick']}: {m['text']}") |
|
50baf1a…
|
lmata
|
338 |
|
|
50baf1a…
|
lmata
|
339 |
if __name__ == "__main__": |
|
50baf1a…
|
lmata
|
340 |
run() |
|
50baf1a…
|
lmata
|
341 |
``` |
|
50baf1a…
|
lmata
|
342 |
|
|
50baf1a…
|
lmata
|
343 |
## Configure LLM backends on the daemon (if you want scuttlebot to broker calls) |
|
50baf1a…
|
lmata
|
344 |
Using the policy-backed API (keys are masked on read): |
|
50baf1a…
|
lmata
|
345 |
```bash |
|
50baf1a…
|
lmata
|
346 |
curl -X POST "$SCUTTLEBOT_URL/v1/llm/backends" \ |
|
50baf1a…
|
lmata
|
347 |
-H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
|
50baf1a…
|
lmata
|
348 |
-H "Content-Type: application/json" \ |
|
50baf1a…
|
lmata
|
349 |
-d '{"name":"openai-default","backend":"openai","api_key":"'$OPENAI_API_KEY'","base_url":"https://api.openai.com/v1","model":"gpt-4.1-mini","default":true}' |
|
50baf1a…
|
lmata
|
350 |
``` |
|
50baf1a…
|
lmata
|
351 |
List backends: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/llm/backends"` |
|
50baf1a…
|
lmata
|
352 |
Known backend templates: `curl "$SCUTTLEBOT_URL/v1/llm/known"`. |
|
50baf1a…
|
lmata
|
353 |
|
|
50baf1a…
|
lmata
|
354 |
## Operational notes |
|
50baf1a…
|
lmata
|
355 |
- Filter out your own nick to avoid echo. |
|
50baf1a…
|
lmata
|
356 |
- Keep channel slugs without `#` when hitting the HTTP API. |
|
50baf1a…
|
lmata
|
357 |
- For near-real-time inbound delivery, poll every few seconds or use the SSE stream at `/v1/channels/{channel}/stream?token=...` (EventSource-compatible). |
|
50baf1a…
|
lmata
|
358 |
- Treat `SCUTTLEBOT_TOKEN` and `OPENAI_API_KEY` as secrets; do not log them. |