ScuttleBot

scuttlebot / skills / openai-relay / scripts / node-openai-relay.mjs
Source Blame History 132 lines
50baf1a… lmata 1 #!/usr/bin/env node
50baf1a… lmata 2 // Minimal OpenAI + scuttlebot relay example (Node 18+).
50baf1a… lmata 3 // Requires env: SCUTTLEBOT_URL, SCUTTLEBOT_TOKEN, SCUTTLEBOT_CHANNEL.
50baf1a… lmata 4 // Optional: SCUTTLEBOT_NICK, SCUTTLEBOT_SESSION_ID, OPENAI_API_KEY.
50baf1a… lmata 5
50baf1a… lmata 6 import OpenAI from "openai";
50baf1a… lmata 7 import path from "node:path";
50baf1a… lmata 8
50baf1a… lmata 9 const prompt = process.argv[2] || "Hello from openai-relay";
50baf1a… lmata 10 const sanitize = (value) => value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
50baf1a… lmata 11 const baseName = sanitize(path.basename(process.cwd()) || "repo");
50baf1a… lmata 12 const sessionSuffix = sanitize(
50baf1a… lmata 13 process.env.SCUTTLEBOT_SESSION_ID || process.env.CODEX_SESSION_ID || String(process.ppid || process.pid)
50baf1a… lmata 14 ) || "session";
50baf1a… lmata 15
50baf1a… lmata 16 const cfg = {
50baf1a… lmata 17 url: process.env.SCUTTLEBOT_URL,
50baf1a… lmata 18 token: process.env.SCUTTLEBOT_TOKEN,
50baf1a… lmata 19 channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""),
50baf1a… lmata 20 nick: process.env.SCUTTLEBOT_NICK || `codex-${baseName}-${sessionSuffix}`,
50baf1a… lmata 21 model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
50baf1a… lmata 22 backend: process.env.SCUTTLEBOT_LLM_BACKEND || "openai", // default to daemon-stored openai
50baf1a… lmata 23 };
50baf1a… lmata 24
50baf1a… lmata 25 for (const [k, v] of Object.entries(cfg)) {
50baf1a… lmata 26 if (["backend", "model"].includes(k)) continue;
50baf1a… lmata 27 if (!v) {
50baf1a… lmata 28 console.error(`missing env: ${k.toUpperCase()}`);
50baf1a… lmata 29 process.exit(1);
50baf1a… lmata 30 }
50baf1a… lmata 31 }
50baf1a… lmata 32 const useBackend = !!cfg.backend;
50baf1a… lmata 33 if (!useBackend && !process.env.OPENAI_API_KEY) {
50baf1a… lmata 34 console.error("missing env: OPENAI_API_KEY (or set SCUTTLEBOT_LLM_BACKEND to use server-side key)");
50baf1a… lmata 35 process.exit(1);
50baf1a… lmata 36 }
50baf1a… lmata 37
50baf1a… lmata 38 const openai = useBackend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
50baf1a… lmata 39 let lastCheck = 0;
50baf1a… lmata 40
50baf1a… lmata 41 function mentionsNick(text) {
50baf1a… lmata 42 const escaped = cfg.nick.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50baf1a… lmata 43 return new RegExp(`(^|[^A-Za-z0-9_./\\\\-])${escaped}($|[^A-Za-z0-9_./\\\\-])`, "i").test(text);
50baf1a… lmata 44 }
50baf1a… lmata 45
50baf1a… lmata 46 async function relayPost(text) {
50baf1a… lmata 47 const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, {
50baf1a… lmata 48 method: "POST",
50baf1a… lmata 49 headers: {
50baf1a… lmata 50 Authorization: `Bearer ${cfg.token}`,
50baf1a… lmata 51 "Content-Type": "application/json",
50baf1a… lmata 52 },
50baf1a… lmata 53 body: JSON.stringify({ text, nick: cfg.nick }),
50baf1a… lmata 54 });
50baf1a… lmata 55 if (!res.ok) {
50baf1a… lmata 56 throw new Error(`relay post failed: ${res.status} ${res.statusText}`);
50baf1a… lmata 57 }
50baf1a… lmata 58 }
50baf1a… lmata 59
50baf1a… lmata 60 async function relayPoll() {
50baf1a… lmata 61 const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, {
50baf1a… lmata 62 headers: { Authorization: `Bearer ${cfg.token}` },
50baf1a… lmata 63 });
50baf1a… lmata 64 if (!res.ok) {
50baf1a… lmata 65 throw new Error(`relay poll failed: ${res.status} ${res.statusText}`);
50baf1a… lmata 66 }
50baf1a… lmata 67 const data = await res.json();
50baf1a… lmata 68 const now = Date.now() / 1000;
50baf1a… lmata 69 const bots = new Set([
50baf1a… lmata 70 cfg.nick,
50baf1a… lmata 71 "bridge",
50baf1a… lmata 72 "oracle",
50baf1a… lmata 73 "sentinel",
50baf1a… lmata 74 "steward",
50baf1a… lmata 75 "scribe",
50baf1a… lmata 76 "warden",
50baf1a… lmata 77 "snitch",
50baf1a… lmata 78 "herald",
50baf1a… lmata 79 "scroll",
50baf1a… lmata 80 "systembot",
50baf1a… lmata 81 "auditbot",
50baf1a… lmata 82 "claude",
50baf1a… lmata 83 ]);
50baf1a… lmata 84 const msgs =
50baf1a… lmata 85 data.messages?.filter(
50baf1a… lmata 86 (m) =>
50baf1a… lmata 87 !bots.has(m.nick) &&
50baf1a… lmata 88 !m.nick.startsWith("claude-") &&
50baf1a… lmata 89 !m.nick.startsWith("codex-") &&
50baf1a… lmata 90 !m.nick.startsWith("gemini-") &&
50baf1a… lmata 91 Date.parse(m.at) / 1000 > lastCheck &&
50baf1a… lmata 92 mentionsNick(m.text)
50baf1a… lmata 93 ) || [];
50baf1a… lmata 94 lastCheck = now;
50baf1a… lmata 95 return msgs;
50baf1a… lmata 96 }
50baf1a… lmata 97
50baf1a… lmata 98 async function main() {
50baf1a… lmata 99 await relayPost(`starting: ${prompt}`);
50baf1a… lmata 100
50baf1a… lmata 101 let reply;
50baf1a… lmata 102 if (useBackend) {
50baf1a… lmata 103 const res = await fetch(`${cfg.url}/v1/llm/complete`, {
50baf1a… lmata 104 method: "POST",
50baf1a… lmata 105 headers: {
50baf1a… lmata 106 Authorization: `Bearer ${cfg.token}`,
50baf1a… lmata 107 "Content-Type": "application/json",
50baf1a… lmata 108 },
50baf1a… lmata 109 body: JSON.stringify({ backend: cfg.backend, prompt }),
50baf1a… lmata 110 });
50baf1a… lmata 111 if (!res.ok) throw new Error(`llm complete failed: ${res.status} ${res.statusText}`);
50baf1a… lmata 112 const body = await res.json();
50baf1a… lmata 113 reply = body.text;
50baf1a… lmata 114 } else {
50baf1a… lmata 115 const completion = await openai.chat.completions.create({
50baf1a… lmata 116 model: cfg.model,
50baf1a… lmata 117 messages: [{ role: "user", content: prompt }],
50baf1a… lmata 118 });
50baf1a… lmata 119 reply = completion.choices[0].message.content;
50baf1a… lmata 120 }
50baf1a… lmata 121 console.log(`OpenAI: ${reply}`);
50baf1a… lmata 122
50baf1a… lmata 123 await relayPost(`OpenAI reply: ${reply}`);
50baf1a… lmata 124
50baf1a… lmata 125 const instructions = await relayPoll();
50baf1a… lmata 126 instructions.forEach((m) => console.log(`[IRC] ${m.nick}: ${m.text}`));
50baf1a… lmata 127 }
50baf1a… lmata 128
50baf1a… lmata 129 main().catch((err) => {
50baf1a… lmata 130 console.error(err);
50baf1a… lmata 131 process.exit(1);
50baf1a… lmata 132 });

Keyboard Shortcuts

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