ScuttleBot
Add Claude and Gemini relay brokers and fleet tooling
Commit
016a29f42f463c2e9cb42974d1c38ff863c8d61e186d69e22be45113553a66a2
Parent
24a217e9a4c108f…
34 files changed
+35
-2
+12
+64
+2
+55
+63
+94
+64
+347
+71
+57
-3
+36
-3
+15
+57
-3
+1
+17
+22
+105
+107
+205
+29
+44
+59
+28
+18
+248
+65
+39
+36
+24
+6
+196
+123
+18
~
Makefile
~
README.md
+
cmd/claude-agent/main.go
+
cmd/claude-relay/main.go
+
cmd/claude-relay/main_test.go
+
cmd/codex-agent/main.go
+
cmd/fleet-cmd/main.go
+
cmd/gemini-agent/main.go
+
cmd/gemini-relay/main.go
+
cmd/gemini-relay/main_test.go
~
docs/getting-started/installation.md
~
docs/guide/agent-registration.md
+
docs/guide/fleet-management.md
~
docs/reference/cli.md
~
mkdocs.yml
+
pkg/ircagent/ircagent.go
+
pkg/ircagent/ircagent_test.go
+
skills/gemini-relay/FLEET.md
+
skills/gemini-relay/SKILL.md
+
skills/gemini-relay/hooks/README.md
+
skills/gemini-relay/hooks/scuttlebot-after-agent.sh
+
skills/gemini-relay/hooks/scuttlebot-check.sh
+
skills/gemini-relay/hooks/scuttlebot-post.sh
+
skills/gemini-relay/install.md
+
skills/gemini-relay/scripts/gemini-relay.sh
+
skills/gemini-relay/scripts/install-gemini-relay.sh
+
skills/scuttlebot-relay/FLEET.md
+
skills/scuttlebot-relay/hooks/README.md
+
skills/scuttlebot-relay/hooks/scuttlebot-check.sh
+
skills/scuttlebot-relay/hooks/scuttlebot-post.sh
+
skills/scuttlebot-relay/install.md
+
skills/scuttlebot-relay/scripts/claude-relay.sh
+
skills/scuttlebot-relay/scripts/install-claude-relay.sh
+
tests/smoke/test-installers.sh
M
Makefile
+35
-2
| --- Makefile | ||
| +++ Makefile | ||
| @@ -1,19 +1,52 @@ | ||
| 1 | -.PHONY: build test lint clean | |
| 1 | +.PHONY: build test lint clean install-codex-relay install-gemini-relay install-claude-relay test-smoke | |
| 2 | 2 | |
| 3 | 3 | build: |
| 4 | 4 | go build ./... |
| 5 | 5 | |
| 6 | 6 | test: |
| 7 | 7 | go test ./... |
| 8 | 8 | |
| 9 | +test-smoke: | |
| 10 | + bash tests/smoke/test-installers.sh | |
| 11 | + | |
| 9 | 12 | lint: |
| 10 | 13 | golangci-lint run |
| 11 | 14 | |
| 12 | 15 | clean: |
| 13 | - rm -f bin/scuttlebot bin/scuttlectl | |
| 16 | + rm -f bin/scuttlebot bin/scuttlectl bin/claude-agent bin/codex-agent bin/gemini-agent bin/codex-relay bin/gemini-relay bin/claude-relay bin/fleet-cmd | |
| 17 | + | |
| 18 | +install-codex-relay: | |
| 19 | + bash skills/openai-relay/scripts/install-codex-relay.sh | |
| 20 | + | |
| 21 | +install-gemini-relay: | |
| 22 | + bash skills/gemini-relay/scripts/install-gemini-relay.sh | |
| 23 | + | |
| 24 | +install-claude-relay: | |
| 25 | + bash skills/scuttlebot-relay/scripts/install-claude-relay.sh | |
| 14 | 26 | |
| 15 | 27 | bin/scuttlebot: |
| 16 | 28 | go build -o bin/scuttlebot ./cmd/scuttlebot |
| 17 | 29 | |
| 18 | 30 | bin/scuttlectl: |
| 19 | 31 | go build -o bin/scuttlectl ./cmd/scuttlectl |
| 32 | + | |
| 33 | +bin/claude-agent: | |
| 34 | + go build -o bin/claude-agent ./cmd/claude-agent | |
| 35 | + | |
| 36 | +bin/codex-agent: | |
| 37 | + go build -o bin/codex-agent ./cmd/codex-agent | |
| 38 | + | |
| 39 | +bin/gemini-agent: | |
| 40 | + go build -o bin/gemini-agent ./cmd/gemini-agent | |
| 41 | + | |
| 42 | +bin/codex-relay: | |
| 43 | + go build -o bin/codex-relay ./cmd/codex-relay | |
| 44 | + | |
| 45 | +bin/gemini-relay: | |
| 46 | + go build -o bin/gemini-relay ./cmd/gemini-relay | |
| 47 | + | |
| 48 | +bin/claude-relay: | |
| 49 | + go build -o bin/claude-relay ./cmd/claude-relay | |
| 50 | + | |
| 51 | +bin/fleet-cmd: | |
| 52 | + go build -o bin/fleet-cmd ./cmd/fleet-cmd | |
| 20 | 53 |
| --- Makefile | |
| +++ Makefile | |
| @@ -1,19 +1,52 @@ | |
| 1 | .PHONY: build test lint clean |
| 2 | |
| 3 | build: |
| 4 | go build ./... |
| 5 | |
| 6 | test: |
| 7 | go test ./... |
| 8 | |
| 9 | lint: |
| 10 | golangci-lint run |
| 11 | |
| 12 | clean: |
| 13 | rm -f bin/scuttlebot bin/scuttlectl |
| 14 | |
| 15 | bin/scuttlebot: |
| 16 | go build -o bin/scuttlebot ./cmd/scuttlebot |
| 17 | |
| 18 | bin/scuttlectl: |
| 19 | go build -o bin/scuttlectl ./cmd/scuttlectl |
| 20 |
| --- Makefile | |
| +++ Makefile | |
| @@ -1,19 +1,52 @@ | |
| 1 | .PHONY: build test lint clean install-codex-relay install-gemini-relay install-claude-relay test-smoke |
| 2 | |
| 3 | build: |
| 4 | go build ./... |
| 5 | |
| 6 | test: |
| 7 | go test ./... |
| 8 | |
| 9 | test-smoke: |
| 10 | bash tests/smoke/test-installers.sh |
| 11 | |
| 12 | lint: |
| 13 | golangci-lint run |
| 14 | |
| 15 | clean: |
| 16 | rm -f bin/scuttlebot bin/scuttlectl bin/claude-agent bin/codex-agent bin/gemini-agent bin/codex-relay bin/gemini-relay bin/claude-relay bin/fleet-cmd |
| 17 | |
| 18 | install-codex-relay: |
| 19 | bash skills/openai-relay/scripts/install-codex-relay.sh |
| 20 | |
| 21 | install-gemini-relay: |
| 22 | bash skills/gemini-relay/scripts/install-gemini-relay.sh |
| 23 | |
| 24 | install-claude-relay: |
| 25 | bash skills/scuttlebot-relay/scripts/install-claude-relay.sh |
| 26 | |
| 27 | bin/scuttlebot: |
| 28 | go build -o bin/scuttlebot ./cmd/scuttlebot |
| 29 | |
| 30 | bin/scuttlectl: |
| 31 | go build -o bin/scuttlectl ./cmd/scuttlectl |
| 32 | |
| 33 | bin/claude-agent: |
| 34 | go build -o bin/claude-agent ./cmd/claude-agent |
| 35 | |
| 36 | bin/codex-agent: |
| 37 | go build -o bin/codex-agent ./cmd/codex-agent |
| 38 | |
| 39 | bin/gemini-agent: |
| 40 | go build -o bin/gemini-agent ./cmd/gemini-agent |
| 41 | |
| 42 | bin/codex-relay: |
| 43 | go build -o bin/codex-relay ./cmd/codex-relay |
| 44 | |
| 45 | bin/gemini-relay: |
| 46 | go build -o bin/gemini-relay ./cmd/gemini-relay |
| 47 | |
| 48 | bin/claude-relay: |
| 49 | go build -o bin/claude-relay ./cmd/claude-relay |
| 50 | |
| 51 | bin/fleet-cmd: |
| 52 | go build -o bin/fleet-cmd ./cmd/fleet-cmd |
| 53 |
+12
| --- README.md | ||
| +++ README.md | ||
| @@ -51,10 +51,22 @@ | ||
| 51 | 51 | - **[OpenClaw](https://openclaw.ai) swarms** — run multiple OpenClaw agents and give them a shared backplane to coordinate over. The MCP server makes it plug-in ready with no custom integration code. |
| 52 | 52 | - **Claude Code / Gemini / Codex fleets** — multiple coding agents working on the same project, sharing context in real time |
| 53 | 53 | - **Ops and monitoring agents** — agents watching infrastructure, triaging alerts, escalating to humans — all visible in a single IRC channel |
| 54 | 54 | - **Any multi-agent system** where humans need to see what's happening without a custom dashboard |
| 55 | 55 | |
| 56 | +--- | |
| 57 | + | |
| 58 | +## Fleet Management & Relays | |
| 59 | + | |
| 60 | +scuttlebot provides an **Interactive Broker** for local LLM terminal sessions (Claude Code, Gemini, Codex). | |
| 61 | + | |
| 62 | +By running your agent through a scuttlebot relay, you get: | |
| 63 | +- **Real-time Observability:** Every tool call is automatically posted to an IRC channel. | |
| 64 | +- **Human-in-the-loop Control:** Operators can mention the agent's nick in IRC to inject instructions directly into its terminal context. | |
| 65 | +- **PTY Wrapper:** The relay uses a real pseudo-terminal to wrap the agent, enabling seamless interaction and interrupts. | |
| 66 | +- **Fleet Commander:** Use `fleet-cmd` to map every active session across your network and broadcast emergency instructions to the entire fleet at once. | |
| 67 | + | |
| 56 | 68 | --- |
| 57 | 69 | |
| 58 | 70 | ## How it works |
| 59 | 71 | |
| 60 | 72 | scuttlebot manages an [Ergo](https://ergo.chat) IRC server. Users configure scuttlebot — never Ergo directly. |
| 61 | 73 | |
| 62 | 74 | ADDED cmd/claude-agent/main.go |
| 63 | 75 | ADDED cmd/claude-relay/main.go |
| 64 | 76 | ADDED cmd/claude-relay/main_test.go |
| 65 | 77 | ADDED cmd/codex-agent/main.go |
| 66 | 78 | ADDED cmd/fleet-cmd/main.go |
| 67 | 79 | ADDED cmd/gemini-agent/main.go |
| 68 | 80 | ADDED cmd/gemini-relay/main.go |
| 69 | 81 | ADDED cmd/gemini-relay/main_test.go |
| --- README.md | |
| +++ README.md | |
| @@ -51,10 +51,22 @@ | |
| 51 | - **[OpenClaw](https://openclaw.ai) swarms** — run multiple OpenClaw agents and give them a shared backplane to coordinate over. The MCP server makes it plug-in ready with no custom integration code. |
| 52 | - **Claude Code / Gemini / Codex fleets** — multiple coding agents working on the same project, sharing context in real time |
| 53 | - **Ops and monitoring agents** — agents watching infrastructure, triaging alerts, escalating to humans — all visible in a single IRC channel |
| 54 | - **Any multi-agent system** where humans need to see what's happening without a custom dashboard |
| 55 | |
| 56 | --- |
| 57 | |
| 58 | ## How it works |
| 59 | |
| 60 | scuttlebot manages an [Ergo](https://ergo.chat) IRC server. Users configure scuttlebot — never Ergo directly. |
| 61 | |
| 62 | DDED cmd/claude-agent/main.go |
| 63 | DDED cmd/claude-relay/main.go |
| 64 | DDED cmd/claude-relay/main_test.go |
| 65 | DDED cmd/codex-agent/main.go |
| 66 | DDED cmd/fleet-cmd/main.go |
| 67 | DDED cmd/gemini-agent/main.go |
| 68 | DDED cmd/gemini-relay/main.go |
| 69 | DDED cmd/gemini-relay/main_test.go |
| --- README.md | |
| +++ README.md | |
| @@ -51,10 +51,22 @@ | |
| 51 | - **[OpenClaw](https://openclaw.ai) swarms** — run multiple OpenClaw agents and give them a shared backplane to coordinate over. The MCP server makes it plug-in ready with no custom integration code. |
| 52 | - **Claude Code / Gemini / Codex fleets** — multiple coding agents working on the same project, sharing context in real time |
| 53 | - **Ops and monitoring agents** — agents watching infrastructure, triaging alerts, escalating to humans — all visible in a single IRC channel |
| 54 | - **Any multi-agent system** where humans need to see what's happening without a custom dashboard |
| 55 | |
| 56 | --- |
| 57 | |
| 58 | ## Fleet Management & Relays |
| 59 | |
| 60 | scuttlebot provides an **Interactive Broker** for local LLM terminal sessions (Claude Code, Gemini, Codex). |
| 61 | |
| 62 | By running your agent through a scuttlebot relay, you get: |
| 63 | - **Real-time Observability:** Every tool call is automatically posted to an IRC channel. |
| 64 | - **Human-in-the-loop Control:** Operators can mention the agent's nick in IRC to inject instructions directly into its terminal context. |
| 65 | - **PTY Wrapper:** The relay uses a real pseudo-terminal to wrap the agent, enabling seamless interaction and interrupts. |
| 66 | - **Fleet Commander:** Use `fleet-cmd` to map every active session across your network and broadcast emergency instructions to the entire fleet at once. |
| 67 | |
| 68 | --- |
| 69 | |
| 70 | ## How it works |
| 71 | |
| 72 | scuttlebot manages an [Ergo](https://ergo.chat) IRC server. Users configure scuttlebot — never Ergo directly. |
| 73 | |
| 74 | DDED cmd/claude-agent/main.go |
| 75 | DDED cmd/claude-relay/main.go |
| 76 | DDED cmd/claude-relay/main_test.go |
| 77 | DDED cmd/codex-agent/main.go |
| 78 | DDED cmd/fleet-cmd/main.go |
| 79 | DDED cmd/gemini-agent/main.go |
| 80 | DDED cmd/gemini-relay/main.go |
| 81 | DDED cmd/gemini-relay/main_test.go |
+64
| --- a/cmd/claude-agent/main.go | ||
| +++ b/cmd/claude-agent/main.go | ||
| @@ -0,0 +1,64 @@ | ||
| 1 | +// claude-agent is a thin wrapper around pkg/ircagent with Claude defaults. | |
| 2 | +package main | |
| 3 | + | |
| 4 | +import ( | |
| 5 | + "context" | |
| 6 | + "flag" | |
| 7 | + "fmt" | |
| 8 | + "log/slog" | |
| 9 | + "os" | |
| 10 | + "os/signal" | |
| 11 | + "syscall" | |
| 12 | + | |
| 13 | + "github.com/conflicthq/scuttlebot/pkg/ircagent" | |
| 14 | +) | |
| 15 | + | |
| 16 | +const systemPrompt = `You are an AI assistant connected to an IRC chat server called scuttlebot. | |
| 17 | +Be helpful, concise, and friendly. Keep responses short — IRC is a chat medium, not a document editor. | |
| 18 | +No markdown formatting (no **, ##, backtick blocks) — IRC renders plain text only. | |
| 19 | +You may use multiple lines but keep each thought brief.` | |
| 20 | + | |
| 21 | +func main() { | |
| 22 | + ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") | |
| 23 | + nick := flag.String("nick", "claude", "IRC nick") | |
| 24 | + pass := flag.String("pass", "", "SASL password (required)") | |
| 25 | + channels := flag.String("channels", "#general", "Comma-separated channels to join") | |
| 26 | + apiKey := flag.String("api-key", os.Getenv("ANTHROPIC_API_KEY"), "Anthropic API key (direct mode)") | |
| 27 | + model := flag.String("model", "", "Model override (direct mode)") | |
| 28 | + apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") | |
| 29 | + token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") | |
| 30 | + backend := flag.String("backend", "anthro", "Backend name in scuttlebot (gateway mode)") | |
| 31 | + flag.Parse() | |
| 32 | + | |
| 33 | + if *pass == "" { | |
| 34 | + fmt.Fprintln(os.Stderr, "error: --pass is required") | |
| 35 | + os.Exit(1) | |
| 36 | + } | |
| 37 | + | |
| 38 | + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 39 | + defer cancel() | |
| 40 | + | |
| 41 | + err := ircagent.Run(ctx, ircagent.Config{ | |
| 42 | + IRCAddr: *ircAddr, | |
| 43 | + Nick: *nick, | |
| 44 | + Pass: *pass, | |
| 45 | + Channels: ircagent.SplitCSV(*channels), | |
| 46 | + SystemPrompt: systemPrompt, | |
| 47 | + Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), | |
| 48 | + ErrorJoiner: " — ", | |
| 49 | + Direct: &ircagent.DirectConfig{ | |
| 50 | + Backend: "anthropic", | |
| 51 | + APIKey: *apiKey, | |
| 52 | + Model: *model, | |
| 53 | + }, | |
| 54 | + Gateway: &ircagent.GatewayConfig{ | |
| 55 | + APIURL: *apiURL, | |
| 56 | + Token: *token, | |
| 57 | + Backend: *backend, | |
| 58 | + }, | |
| 59 | + }) | |
| 60 | + if err != nil { | |
| 61 | + fmt.Fprintln(os.Stderr, "error:", err) | |
| 62 | + os.Exit(1) | |
| 63 | + } | |
| 64 | +} |
| --- a/cmd/claude-agent/main.go | |
| +++ b/cmd/claude-agent/main.go | |
| @@ -0,0 +1,64 @@ | |
| --- a/cmd/claude-agent/main.go | |
| +++ b/cmd/claude-agent/main.go | |
| @@ -0,0 +1,64 @@ | |
| 1 | // claude-agent is a thin wrapper around pkg/ircagent with Claude defaults. |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "log/slog" |
| 9 | "os" |
| 10 | "os/signal" |
| 11 | "syscall" |
| 12 | |
| 13 | "github.com/conflicthq/scuttlebot/pkg/ircagent" |
| 14 | ) |
| 15 | |
| 16 | const systemPrompt = `You are an AI assistant connected to an IRC chat server called scuttlebot. |
| 17 | Be helpful, concise, and friendly. Keep responses short — IRC is a chat medium, not a document editor. |
| 18 | No markdown formatting (no **, ##, backtick blocks) — IRC renders plain text only. |
| 19 | You may use multiple lines but keep each thought brief.` |
| 20 | |
| 21 | func main() { |
| 22 | ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") |
| 23 | nick := flag.String("nick", "claude", "IRC nick") |
| 24 | pass := flag.String("pass", "", "SASL password (required)") |
| 25 | channels := flag.String("channels", "#general", "Comma-separated channels to join") |
| 26 | apiKey := flag.String("api-key", os.Getenv("ANTHROPIC_API_KEY"), "Anthropic API key (direct mode)") |
| 27 | model := flag.String("model", "", "Model override (direct mode)") |
| 28 | apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") |
| 29 | token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") |
| 30 | backend := flag.String("backend", "anthro", "Backend name in scuttlebot (gateway mode)") |
| 31 | flag.Parse() |
| 32 | |
| 33 | if *pass == "" { |
| 34 | fmt.Fprintln(os.Stderr, "error: --pass is required") |
| 35 | os.Exit(1) |
| 36 | } |
| 37 | |
| 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 39 | defer cancel() |
| 40 | |
| 41 | err := ircagent.Run(ctx, ircagent.Config{ |
| 42 | IRCAddr: *ircAddr, |
| 43 | Nick: *nick, |
| 44 | Pass: *pass, |
| 45 | Channels: ircagent.SplitCSV(*channels), |
| 46 | SystemPrompt: systemPrompt, |
| 47 | Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), |
| 48 | ErrorJoiner: " — ", |
| 49 | Direct: &ircagent.DirectConfig{ |
| 50 | Backend: "anthropic", |
| 51 | APIKey: *apiKey, |
| 52 | Model: *model, |
| 53 | }, |
| 54 | Gateway: &ircagent.GatewayConfig{ |
| 55 | APIURL: *apiURL, |
| 56 | Token: *token, |
| 57 | Backend: *backend, |
| 58 | }, |
| 59 | }) |
| 60 | if err != nil { |
| 61 | fmt.Fprintln(os.Stderr, "error:", err) |
| 62 | os.Exit(1) |
| 63 | } |
| 64 | } |
| --- a/cmd/claude-relay/main.go | ||
| +++ b/cmd/claude-relay/main.go | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +packapackapacka=HTTP | |
| 2 | + = 2nectWait = 1InjectDelaypresencepacka // Fa |
| --- a/cmd/claude-relay/main.go | |
| +++ b/cmd/claude-relay/main.go | |
| @@ -0,0 +1,2 @@ | |
| --- a/cmd/claude-relay/main.go | |
| +++ b/cmd/claude-relay/main.go | |
| @@ -0,0 +1,2 @@ | |
| 1 | packapackapacka=HTTP |
| 2 | = 2nectWait = 1InjectDelaypresencepacka // Fa |
| --- a/cmd/claude-relay/main_test.go | ||
| +++ b/cmd/claude-relay/main_test.go | ||
| @@ -0,0 +1,55 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "mport ( | |
| 5 | + "context" | |
| 6 | + "os" | |
| 7 | + "path/fi | |
| 8 | + "github.com/google/uuid" | |
| 9 | +) | |
| 10 | + | |
| 11 | +func TestFilterMessages(t *testing.T) { | |
| 12 | + now := time.Now() | |
| 13 | + nick := "claude-test" | |
| 14 | + messages := []message{ | |
| 15 | + {Nick: "operator", Text: "claude-test: hello", At: now}, | |
| 16 | + {Nick: "claude-test", Text: "i am claude", At: now}, // self | |
| 17 | + {Nick: "other", Text: "not for me", At: now}, // no mention | |
| 18 | + {Nick: "bridge", Text: "system message", At: now}, // service bot | |
| 19 | + } | |
| 20 | + | |
| 21 | + filtered, _ : main | |
| 22 | + | |
| 23 | +import ( | |
| 24 | + "mport ( | |
| 25 | + "context" | |
| 26 | + "os" | |
| 27 | + "path/fi | |
| 28 | + "github.com/google/uuid" | |
| 29 | +) | |
| 30 | + | |
| 31 | +func TestFilterMessages(t *testing.T) { | |
| 32 | + now := time.Now() | |
| 33 | + nick := "claude-test" | |
| 34 | + messages := []message{ | |
| 35 | + {Nick: "operator", Text: "claude-test: hello", At: now}, | |
| 36 | + {Nick: "claude-test", Text: "i am claude", At: now}, // self | |
| 37 | + {Nick: "other", Text: "not for me", At: now}, // no mention | |
| 38 | + {Nick: "bridge", Text: "system message", At: now}, // service bot | |
| 39 | + } | |
| 40 | + | |
| 41 | + filtered, _ := filterMessages(messages, now.Add(-time.Minute), nick, "worker") | |
| 42 | + if len(filtered) != 1 { | |
| 43 | + t.Errorf("expected 1 filtered message, got %d", len(filtered)) | |
| 44 | + } | |
| 45 | + if filtered[0].Nick != "operator" { | |
| 46 | + t.Errorf("expected operator message, got %s", filtered[0].Nick) | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +func TestLoadConfig(t *testing.T) { | |
| 51 | + t.Setenv("SCUTTLEBOT_CONFIG_FILE", filepath.Join(t.TempDir(), "scuttlebot-relay.env")) | |
| 52 | + t.Setenv("SCUTTLEBOT_URL", "http://test:8080") | |
| 53 | + t.Setenv("SCUTTLEBOT_TOKEN", "test-token") | |
| 54 | + t.Setenv("SCUTTLEBOT_SESSION_ID", "abc") | |
| 55 | + t.Setenv("SCUTTLEB |
| --- a/cmd/claude-relay/main_test.go | |
| +++ b/cmd/claude-relay/main_test.go | |
| @@ -0,0 +1,55 @@ | |
| --- a/cmd/claude-relay/main_test.go | |
| +++ b/cmd/claude-relay/main_test.go | |
| @@ -0,0 +1,55 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "mport ( |
| 5 | "context" |
| 6 | "os" |
| 7 | "path/fi |
| 8 | "github.com/google/uuid" |
| 9 | ) |
| 10 | |
| 11 | func TestFilterMessages(t *testing.T) { |
| 12 | now := time.Now() |
| 13 | nick := "claude-test" |
| 14 | messages := []message{ |
| 15 | {Nick: "operator", Text: "claude-test: hello", At: now}, |
| 16 | {Nick: "claude-test", Text: "i am claude", At: now}, // self |
| 17 | {Nick: "other", Text: "not for me", At: now}, // no mention |
| 18 | {Nick: "bridge", Text: "system message", At: now}, // service bot |
| 19 | } |
| 20 | |
| 21 | filtered, _ : main |
| 22 | |
| 23 | import ( |
| 24 | "mport ( |
| 25 | "context" |
| 26 | "os" |
| 27 | "path/fi |
| 28 | "github.com/google/uuid" |
| 29 | ) |
| 30 | |
| 31 | func TestFilterMessages(t *testing.T) { |
| 32 | now := time.Now() |
| 33 | nick := "claude-test" |
| 34 | messages := []message{ |
| 35 | {Nick: "operator", Text: "claude-test: hello", At: now}, |
| 36 | {Nick: "claude-test", Text: "i am claude", At: now}, // self |
| 37 | {Nick: "other", Text: "not for me", At: now}, // no mention |
| 38 | {Nick: "bridge", Text: "system message", At: now}, // service bot |
| 39 | } |
| 40 | |
| 41 | filtered, _ := filterMessages(messages, now.Add(-time.Minute), nick, "worker") |
| 42 | if len(filtered) != 1 { |
| 43 | t.Errorf("expected 1 filtered message, got %d", len(filtered)) |
| 44 | } |
| 45 | if filtered[0].Nick != "operator" { |
| 46 | t.Errorf("expected operator message, got %s", filtered[0].Nick) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestLoadConfig(t *testing.T) { |
| 51 | t.Setenv("SCUTTLEBOT_CONFIG_FILE", filepath.Join(t.TempDir(), "scuttlebot-relay.env")) |
| 52 | t.Setenv("SCUTTLEBOT_URL", "http://test:8080") |
| 53 | t.Setenv("SCUTTLEBOT_TOKEN", "test-token") |
| 54 | t.Setenv("SCUTTLEBOT_SESSION_ID", "abc") |
| 55 | t.Setenv("SCUTTLEB |
+63
| --- a/cmd/codex-agent/main.go | ||
| +++ b/cmd/codex-agent/main.go | ||
| @@ -0,0 +1,63 @@ | ||
| 1 | +// codex-agent is a thin wrapper around pkg/ircagent with Codex/OpenAI defaults. | |
| 2 | +package main | |
| 3 | + | |
| 4 | +import ( | |
| 5 | + "context" | |
| 6 | + "flag" | |
| 7 | + "fmt" | |
| 8 | + "log/slog" | |
| 9 | + "os" | |
| 10 | + "os/signal" | |
| 11 | + "syscall" | |
| 12 | + | |
| 13 | + "github.com/conflicthq/scuttlebot/pkg/ircagent" | |
| 14 | +) | |
| 15 | + | |
| 16 | +const systemPrompt = `You are Codex, an AI assistant connected to an IRC chat server called scuttlebot. | |
| 17 | +Be helpful, concise, and friendly. Keep responses short - IRC is a chat medium, not a document editor. | |
| 18 | +No markdown formatting (no **, ##, backtick blocks) - IRC renders plain text only. | |
| 19 | +You may use multiple lines but keep each thought brief.` | |
| 20 | + | |
| 21 | +func main() { | |
| 22 | + ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") | |
| 23 | + nick := flag.String("nick", "codex", "IRC nick") | |
| 24 | + pass := flag.String("pass", "", "SASL password (required)") | |
| 25 | + channels := flag.String("channels", "#general", "Comma-separated channels to join") | |
| 26 | + apiKey := flag.String("api-key", os.Getenv("OPENAI_API_KEY"), "OpenAI API key (direct mode)") | |
| 27 | + model := flag.String("model", os.Getenv("OPENAI_MODEL"), "Model override (direct mode)") | |
| 28 | + apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") | |
| 29 | + token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") | |
| 30 | + backend := flag.String("backend", "openai", "Backend name in scuttlebot (gateway mode)") | |
| 31 | + flag.Parse() | |
| 32 | + | |
| 33 | + if *pass == "" { | |
| 34 | + fmt.Fprintln(os.Stderr, "error: --pass is required") | |
| 35 | + os.Exit(1) | |
| 36 | + } | |
| 37 | + | |
| 38 | + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 39 | + defer cancel() | |
| 40 | + | |
| 41 | + err := ircagent.Run(ctx, ircagent.Config{ | |
| 42 | + IRCAddr: *ircAddr, | |
| 43 | + Nick: *nick, | |
| 44 | + Pass: *pass, | |
| 45 | + Channels: ircagent.SplitCSV(*channels), | |
| 46 | + SystemPrompt: systemPrompt, | |
| 47 | + Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), | |
| 48 | + Direct: &ircagent.DirectConfig{ | |
| 49 | + Backend: "openai", | |
| 50 | + APIKey: *apiKey, | |
| 51 | + Model: *model, | |
| 52 | + }, | |
| 53 | + Gateway: &ircagent.GatewayConfig{ | |
| 54 | + APIURL: *apiURL, | |
| 55 | + Token: *token, | |
| 56 | + Backend: *backend, | |
| 57 | + }, | |
| 58 | + }) | |
| 59 | + if err != nil { | |
| 60 | + fmt.Fprintln(os.Stderr, "error:", err) | |
| 61 | + os.Exit(1) | |
| 62 | + } | |
| 63 | +} |
| --- a/cmd/codex-agent/main.go | |
| +++ b/cmd/codex-agent/main.go | |
| @@ -0,0 +1,63 @@ | |
| --- a/cmd/codex-agent/main.go | |
| +++ b/cmd/codex-agent/main.go | |
| @@ -0,0 +1,63 @@ | |
| 1 | // codex-agent is a thin wrapper around pkg/ircagent with Codex/OpenAI defaults. |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "log/slog" |
| 9 | "os" |
| 10 | "os/signal" |
| 11 | "syscall" |
| 12 | |
| 13 | "github.com/conflicthq/scuttlebot/pkg/ircagent" |
| 14 | ) |
| 15 | |
| 16 | const systemPrompt = `You are Codex, an AI assistant connected to an IRC chat server called scuttlebot. |
| 17 | Be helpful, concise, and friendly. Keep responses short - IRC is a chat medium, not a document editor. |
| 18 | No markdown formatting (no **, ##, backtick blocks) - IRC renders plain text only. |
| 19 | You may use multiple lines but keep each thought brief.` |
| 20 | |
| 21 | func main() { |
| 22 | ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") |
| 23 | nick := flag.String("nick", "codex", "IRC nick") |
| 24 | pass := flag.String("pass", "", "SASL password (required)") |
| 25 | channels := flag.String("channels", "#general", "Comma-separated channels to join") |
| 26 | apiKey := flag.String("api-key", os.Getenv("OPENAI_API_KEY"), "OpenAI API key (direct mode)") |
| 27 | model := flag.String("model", os.Getenv("OPENAI_MODEL"), "Model override (direct mode)") |
| 28 | apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") |
| 29 | token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") |
| 30 | backend := flag.String("backend", "openai", "Backend name in scuttlebot (gateway mode)") |
| 31 | flag.Parse() |
| 32 | |
| 33 | if *pass == "" { |
| 34 | fmt.Fprintln(os.Stderr, "error: --pass is required") |
| 35 | os.Exit(1) |
| 36 | } |
| 37 | |
| 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 39 | defer cancel() |
| 40 | |
| 41 | err := ircagent.Run(ctx, ircagent.Config{ |
| 42 | IRCAddr: *ircAddr, |
| 43 | Nick: *nick, |
| 44 | Pass: *pass, |
| 45 | Channels: ircagent.SplitCSV(*channels), |
| 46 | SystemPrompt: systemPrompt, |
| 47 | Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), |
| 48 | Direct: &ircagent.DirectConfig{ |
| 49 | Backend: "openai", |
| 50 | APIKey: *apiKey, |
| 51 | Model: *model, |
| 52 | }, |
| 53 | Gateway: &ircagent.GatewayConfig{ |
| 54 | APIURL: *apiURL, |
| 55 | Token: *token, |
| 56 | Backend: *backend, |
| 57 | }, |
| 58 | }) |
| 59 | if err != nil { |
| 60 | fmt.Fprintln(os.Stderr, "error:", err) |
| 61 | os.Exit(1) |
| 62 | } |
| 63 | } |
+94
| --- a/cmd/fleet-cmd/main.go | ||
| +++ b/cmd/fleet-cmd/main.go | ||
| @@ -0,0 +1,94 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/json" | |
| 5 | + "fmt" | |
| 6 | + "log" | |
| 7 | + "net/http" | |
| 8 | + "os" | |
| 9 | + "sort" | |
| 10 | + "strings" | |
| 11 | + "text/tabwriter" | |
| 12 | + "time" | |
| 13 | +) | |
| 14 | + | |
| 15 | +type Agent struct { | |
| 16 | + Nick string `json:"nick"` | |
| 17 | + Type string `json:"type"` | |
| 18 | + CreatedAt time.Time `json:"created_at"` | |
| 19 | +} | |
| 20 | + | |
| 21 | +type Message struct { | |
| 22 | + Nick string `json:"nick"` | |
| 23 | + Text string `json:"text"` | |
| 24 | + At time.Time `json:"at"` | |
| 25 | +} | |
| 26 | + | |
| 27 | +func main() { | |
| 28 | + token := os.Getenv("SCUTTLEBOT_TOKEN") | |
| 29 | + url := os.Getenv("SCUTTLEBOT_URL") | |
| 30 | + if url == "" { | |
| 31 | + url = "http://localhost:8080" | |
| 32 | + } | |
| 33 | + | |
| 34 | + if token == "" { | |
| 35 | + log.Fatal("SCUTTLEBOT_TOKEN is required") | |
| 36 | + } | |
| 37 | + | |
| 38 | + if len(os.switch os.Args[1 usage() | |
| 39 | + }) | |
| 40 | + case "broadcast": | |
| 41 | + if len(os.Args) < 3 { | |
| 42 | + log.Fatal("usage: fleet-cmd broadcast <message>") | |
| 43 | + } | |
| 44 | + broadcast(url, token, strings.Join(os.Args[2hannel, strings.Join(args[1:], " ")) | |
| 45 | + default: | |
| 46 | + usage() | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +func usage() { | |
| 51 | + fmt<command> [args]") | |
| 52 | + fmt.Println("Commands:") | |
| 53 | + fmt.Println(" map Show all agents and their last activity") | |
| 54 | + fmt.Println(" broadcast Send a message to all agents in #general") | |
| 55 | + os.Exit(1) | |
| 56 | +apFleet(url, token, channel string) { | |
| 57 | + agents := fetchAgents(url, token) | |
| 58 | + messages :="general") | |
| 59 | + | |
| 60 | + // Filter for actual session nicks (ones with suffixes) | |
| 61 | + sessions := make(map[string]Message) | |
| 62 | + for _, m := range messages { | |
| 63 | + if strings.Contains(m.Nick, "-") { | |
| 64 | + sessions[m.Nick] = m | |
| 65 | + } | |
| 66 | + } | |
| 67 | + | |
| 68 | + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) | |
| 69 | + fmt.Fprintln(w, "NICK\tTYPE\tLAST ACTIVITY\tTIME") | |
| 70 | + | |
| 71 | + // Sort nicks for stable output | |
| 72 | + var nicks []string | |
| 73 | + for n := range sessions { | |
| 74 | + nicks = append(nicks, n) | |
| 75 | + } | |
| 76 | + sort.Strings(nicks) | |
| 77 | + | |
| 78 | + for _, nick := range nicks { | |
| 79 | + m := sessions[nick] | |
| 80 | + nickType := "unknown" | |
| 81 | + for _, a := range agents { | |
| 82 | + if strings.HasPrefix(nick, a.Nick) { | |
| 83 | + nickType = a.Type | |
| 84 | + break | |
| 85 | + } | |
| 86 | + } | |
| 87 | + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", nick, nickType, truncate(m.Text, 40), timeSince(m.At)) | |
| 88 | + } | |
| 89 | + w.Flush() | |
| 90 | +} | |
| 91 | + | |
| 92 | + { | |
| 93 | + channel = strings.TrimPrefix(args[i+1], "#") | |
| 94 | + args = |
| --- a/cmd/fleet-cmd/main.go | |
| +++ b/cmd/fleet-cmd/main.go | |
| @@ -0,0 +1,94 @@ | |
| --- a/cmd/fleet-cmd/main.go | |
| +++ b/cmd/fleet-cmd/main.go | |
| @@ -0,0 +1,94 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "log" |
| 7 | "net/http" |
| 8 | "os" |
| 9 | "sort" |
| 10 | "strings" |
| 11 | "text/tabwriter" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | type Agent struct { |
| 16 | Nick string `json:"nick"` |
| 17 | Type string `json:"type"` |
| 18 | CreatedAt time.Time `json:"created_at"` |
| 19 | } |
| 20 | |
| 21 | type Message struct { |
| 22 | Nick string `json:"nick"` |
| 23 | Text string `json:"text"` |
| 24 | At time.Time `json:"at"` |
| 25 | } |
| 26 | |
| 27 | func main() { |
| 28 | token := os.Getenv("SCUTTLEBOT_TOKEN") |
| 29 | url := os.Getenv("SCUTTLEBOT_URL") |
| 30 | if url == "" { |
| 31 | url = "http://localhost:8080" |
| 32 | } |
| 33 | |
| 34 | if token == "" { |
| 35 | log.Fatal("SCUTTLEBOT_TOKEN is required") |
| 36 | } |
| 37 | |
| 38 | if len(os.switch os.Args[1 usage() |
| 39 | }) |
| 40 | case "broadcast": |
| 41 | if len(os.Args) < 3 { |
| 42 | log.Fatal("usage: fleet-cmd broadcast <message>") |
| 43 | } |
| 44 | broadcast(url, token, strings.Join(os.Args[2hannel, strings.Join(args[1:], " ")) |
| 45 | default: |
| 46 | usage() |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func usage() { |
| 51 | fmt<command> [args]") |
| 52 | fmt.Println("Commands:") |
| 53 | fmt.Println(" map Show all agents and their last activity") |
| 54 | fmt.Println(" broadcast Send a message to all agents in #general") |
| 55 | os.Exit(1) |
| 56 | apFleet(url, token, channel string) { |
| 57 | agents := fetchAgents(url, token) |
| 58 | messages :="general") |
| 59 | |
| 60 | // Filter for actual session nicks (ones with suffixes) |
| 61 | sessions := make(map[string]Message) |
| 62 | for _, m := range messages { |
| 63 | if strings.Contains(m.Nick, "-") { |
| 64 | sessions[m.Nick] = m |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) |
| 69 | fmt.Fprintln(w, "NICK\tTYPE\tLAST ACTIVITY\tTIME") |
| 70 | |
| 71 | // Sort nicks for stable output |
| 72 | var nicks []string |
| 73 | for n := range sessions { |
| 74 | nicks = append(nicks, n) |
| 75 | } |
| 76 | sort.Strings(nicks) |
| 77 | |
| 78 | for _, nick := range nicks { |
| 79 | m := sessions[nick] |
| 80 | nickType := "unknown" |
| 81 | for _, a := range agents { |
| 82 | if strings.HasPrefix(nick, a.Nick) { |
| 83 | nickType = a.Type |
| 84 | break |
| 85 | } |
| 86 | } |
| 87 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", nick, nickType, truncate(m.Text, 40), timeSince(m.At)) |
| 88 | } |
| 89 | w.Flush() |
| 90 | } |
| 91 | |
| 92 | { |
| 93 | channel = strings.TrimPrefix(args[i+1], "#") |
| 94 | args = |
+64
| --- a/cmd/gemini-agent/main.go | ||
| +++ b/cmd/gemini-agent/main.go | ||
| @@ -0,0 +1,64 @@ | ||
| 1 | +// gemini-agent is a thin wrapper around pkg/ircagent with Gemini defaults. | |
| 2 | +package main | |
| 3 | + | |
| 4 | +import ( | |
| 5 | + "context" | |
| 6 | + "flag" | |
| 7 | + "fmt" | |
| 8 | + "log/slog" | |
| 9 | + "os" | |
| 10 | + "os/signal" | |
| 11 | + "syscall" | |
| 12 | + | |
| 13 | + "github.com/conflicthq/scuttlebot/pkg/ircagent" | |
| 14 | +) | |
| 15 | + | |
| 16 | +const systemPrompt = `You are an AI assistant connected to an IRC chat server called scuttlebot. | |
| 17 | +Be helpful, concise, and friendly. Keep responses short — IRC is a chat medium, not a document editor. | |
| 18 | +No markdown formatting (no **, ##, backtick blocks) — IRC renders plain text only. | |
| 19 | +You may use multiple lines but keep each thought brief.` | |
| 20 | + | |
| 21 | +func main() { | |
| 22 | + ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") | |
| 23 | + nick := flag.String("nick", "gemini", "IRC nick") | |
| 24 | + pass := flag.String("pass", "", "SASL password (required)") | |
| 25 | + channels := flag.String("channels", "#general", "Comma-separated channels to join") | |
| 26 | + apiKey := flag.String("api-key", os.Getenv("GEMINI_API_KEY"), "Gemini API key (direct mode)") | |
| 27 | + model := flag.String("model", "gemini-1.5-flash", "Model override (direct mode)") | |
| 28 | + apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") | |
| 29 | + token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") | |
| 30 | + backend := flag.String("backend", "gemini", "Backend name in scuttlebot (gateway mode)") | |
| 31 | + flag.Parse() | |
| 32 | + | |
| 33 | + if *pass == "" { | |
| 34 | + fmt.Fprintln(os.Stderr, "error: --pass is required") | |
| 35 | + os.Exit(1) | |
| 36 | + } | |
| 37 | + | |
| 38 | + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 39 | + defer cancel() | |
| 40 | + | |
| 41 | + err := ircagent.Run(ctx, ircagent.Config{ | |
| 42 | + IRCAddr: *ircAddr, | |
| 43 | + Nick: *nick, | |
| 44 | + Pass: *pass, | |
| 45 | + Channels: ircagent.SplitCSV(*channels), | |
| 46 | + SystemPrompt: systemPrompt, | |
| 47 | + Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), | |
| 48 | + ErrorJoiner: " — ", | |
| 49 | + Direct: &ircagent.DirectConfig{ | |
| 50 | + Backend: "gemini", | |
| 51 | + APIKey: *apiKey, | |
| 52 | + Model: *model, | |
| 53 | + }, | |
| 54 | + Gateway: &ircagent.GatewayConfig{ | |
| 55 | + APIURL: *apiURL, | |
| 56 | + Token: *token, | |
| 57 | + Backend: *backend, | |
| 58 | + }, | |
| 59 | + }) | |
| 60 | + if err != nil { | |
| 61 | + fmt.Fprintln(os.Stderr, "error:", err) | |
| 62 | + os.Exit(1) | |
| 63 | + } | |
| 64 | +} |
| --- a/cmd/gemini-agent/main.go | |
| +++ b/cmd/gemini-agent/main.go | |
| @@ -0,0 +1,64 @@ | |
| --- a/cmd/gemini-agent/main.go | |
| +++ b/cmd/gemini-agent/main.go | |
| @@ -0,0 +1,64 @@ | |
| 1 | // gemini-agent is a thin wrapper around pkg/ircagent with Gemini defaults. |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "log/slog" |
| 9 | "os" |
| 10 | "os/signal" |
| 11 | "syscall" |
| 12 | |
| 13 | "github.com/conflicthq/scuttlebot/pkg/ircagent" |
| 14 | ) |
| 15 | |
| 16 | const systemPrompt = `You are an AI assistant connected to an IRC chat server called scuttlebot. |
| 17 | Be helpful, concise, and friendly. Keep responses short — IRC is a chat medium, not a document editor. |
| 18 | No markdown formatting (no **, ##, backtick blocks) — IRC renders plain text only. |
| 19 | You may use multiple lines but keep each thought brief.` |
| 20 | |
| 21 | func main() { |
| 22 | ircAddr := flag.String("irc", "127.0.0.1:6667", "IRC server address") |
| 23 | nick := flag.String("nick", "gemini", "IRC nick") |
| 24 | pass := flag.String("pass", "", "SASL password (required)") |
| 25 | channels := flag.String("channels", "#general", "Comma-separated channels to join") |
| 26 | apiKey := flag.String("api-key", os.Getenv("GEMINI_API_KEY"), "Gemini API key (direct mode)") |
| 27 | model := flag.String("model", "gemini-1.5-flash", "Model override (direct mode)") |
| 28 | apiURL := flag.String("api-url", "http://localhost:8080", "Scuttlebot API URL (gateway mode)") |
| 29 | token := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "Scuttlebot bearer token (gateway mode)") |
| 30 | backend := flag.String("backend", "gemini", "Backend name in scuttlebot (gateway mode)") |
| 31 | flag.Parse() |
| 32 | |
| 33 | if *pass == "" { |
| 34 | fmt.Fprintln(os.Stderr, "error: --pass is required") |
| 35 | os.Exit(1) |
| 36 | } |
| 37 | |
| 38 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 39 | defer cancel() |
| 40 | |
| 41 | err := ircagent.Run(ctx, ircagent.Config{ |
| 42 | IRCAddr: *ircAddr, |
| 43 | Nick: *nick, |
| 44 | Pass: *pass, |
| 45 | Channels: ircagent.SplitCSV(*channels), |
| 46 | SystemPrompt: systemPrompt, |
| 47 | Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), |
| 48 | ErrorJoiner: " — ", |
| 49 | Direct: &ircagent.DirectConfig{ |
| 50 | Backend: "gemini", |
| 51 | APIKey: *apiKey, |
| 52 | Model: *model, |
| 53 | }, |
| 54 | Gateway: &ircagent.GatewayConfig{ |
| 55 | APIURL: *apiURL, |
| 56 | Token: *token, |
| 57 | Backend: *backend, |
| 58 | }, |
| 59 | }) |
| 60 | if err != nil { |
| 61 | fmt.Fprintln(os.Stderr, "error:", err) |
| 62 | os.Exit(1) |
| 63 | } |
| 64 | } |
+347
| --- a/cmd/gemini-relay/main.go | ||
| +++ b/cmd/gemini-relay/main.go | ||
| @@ -0,0 +1,347 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bufio" | |
| 5 | + "context" | |
| 6 | + "errors" | |
| 7 | + "fmt" | |
| 8 | + "hash/crc32" | |
| 9 | + "io" | |
| 10 | + "os" | |
| 11 | + "os/exec" | |
| 12 | + "os/signal" | |
| 13 | + "path/filepath" | |
| 14 | + "sort" | |
| 15 | + "strings" | |
| 16 | + "sttlebot/pk/conflicthq/scuttlebotsessionrelay" | |
| 17 | + "github.com/creack/pty" | |
| 18 | + "golang.org/x/term" | |
| 19 | + "gopkg.in/yaml.v3" | |
| 20 | +) | |
| 21 | + | |
| 22 | +cost ( | |
| 23 | + defaultRelayUR080" | |
| 24 | + default= "general" | |
| 25 | + defaultTranfig, "SCUTTLEHTTP | |
| 26 | + defaultPollInterval HTTP | |
| 27 | + defaul= 10P | |
| 28 | + defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond | |
| 29 | + dHeartbeat = 60P | |
| 30 | + defaultPollInterval = figFile.Second | |
| 31 | + defaultConfigFile env" | |
| 32 | + bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" | |
| 33 | + bracketedPasteEnd = "\x1b[201~" | |
| 34 | +) | |
| 35 | + | |
| 36 | +var serviceBots = map[string]struct{}{ | |
| 37 | + "bridge": {}, | |
| 38 | + "oracle": {}, | |
| 39 | + "sentinel": {}, | |
| 40 | + "steward": {}, | |
| 41 | + "scribe": {}, | |
| 42 | + "warden": {}, | |
| 43 | + "snitch": {}, | |
| 44 | + "herald": {}, | |
| 45 | + "scroll": {}, | |
| 46 | + "systembot": {}, | |
| 47 | + "auditbot": {}, | |
| 48 | +} | |
| 49 | + | |
| 50 | +type config struct { | |
| 51 | + GeminiBin string | |
| 52 | + ConfigFile string | |
| 53 | + Transport sessionrelay.Transport | |
| 54 | + URL string | |
| 55 | + Token string | |
| 56 | + IRCAddr string | |
| 57 | + IRCPass string | |
| 58 | + IRCAgentType string | |
| 59 | + IRCDeleteOnClose bool | |
| 60 | + Channel string | |
| 61 | + Channels []string | |
| 62 | + ChannelStateFile string | |
| 63 | + SessionID string | |
| 64 | + Nick string | |
| 65 | + HooksEnabled bool | |
| 66 | + InterruptOnMessage bool | |
| 67 | +SessionIDlStateFile strin | |
| 68 | + "bufio" | |
| 69 | + "context" | |
| 70 | + "errors" | |
| 71 | + "fmt" | |
| 72 | + "hash/crc32" | |
| 73 | + "io" | |
| 74 | + "os" | |
| 75 | + "os/exec" | |
| 76 | + "os/signal" | |
| 77 | + "path/filepath" | |
| 78 | + "sort" | |
| 79 | + "strings" | |
| 80 | + "sttlebot/pk/conflicthq/scuttlebotsessionrelay" | |
| 81 | + "github.com/creack/pty" | |
| 82 | + "golang.org/x/term" | |
| 83 | + "gopkg.in/yaml.v3" | |
| 84 | +) | |
| 85 | + | |
| 86 | +cost ( | |
| 87 | + defaultRelayUR080" | |
| 88 | + default= "general" | |
| 89 | + defaultTranfig, "SCUTTLEHTTP | |
| 90 | + defaultPollInterval HTTP | |
| 91 | + defaul= 10P | |
| 92 | + defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond | |
| 93 | + dHeartbeat = 60P | |
| 94 | + defaultPollInterval = figFile.Second | |
| 95 | + defaultConfigFile env" | |
| 96 | + bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" | |
| 97 | + bracketedPasteEnd = "\x1b[201~" | |
| 98 | +) | |
| 99 | + | |
| 100 | +var serviceBots = map[string]struct{}{ | |
| 101 | + "bridge": {}, | |
| 102 | + "oracle": {}, | |
| 103 | + "sentinel": {}, | |
| 104 | + "steward": {}, | |
| 105 | + "scribe": {}, | |
| 106 | + "warden": package main | |
| 107 | + | |
| 108 | +import ( | |
| 109 | + "bufio" | |
| 110 | + "context" | |
| 111 | + "errors" | |
| 112 | + "fmt" | |
| 113 | + "hash/crc32" | |
| 114 | + "io" | |
| 115 | + "os" | |
| 116 | + "os/exec" | |
| 117 | + "os/signal" | |
| 118 | + "path/filepath" | |
| 119 | + "sort" | |
| 120 | + "strings" | |
| 121 | + "sttlebot/pk/conflicthq/scuttlebotsessionrelay" | |
| 122 | + "github.com/cre string | |
| 123 | + ConfigFile string | |
| 124 | + Transport sessionrelay.Transport | |
| 125 | + URL string | |
| 126 | + Token string | |
| 127 | + IRCAddr string | |
| 128 | + IRCPass string | |
| 129 | + IRCAgentType string | |
| 130 | + IRCDeleteOnClose bool | |
| 131 | + Channel string | |
| 132 | + Channels []string | |
| 133 | + ChannelStateFile string | |
| 134 | + SessionID string | |
| 135 | + Nick string | |
| 136 | + HooksEnabled bool | |
| 137 | + InterruptOnMessage bool | |
| 138 | + PollInterval time.Duration | |
| 139 | + HeartbeatInterval time.Duration | |
| 140 | + TargetCWD string | |
| 141 | + Args []string | |
| 142 | +} | |
| 143 | + | |
| 144 | +type message = sessionrelay.Message | |
| 145 | + | |
| 146 | +type r | |
| 147 | + } | |
| 148 | +} | |
| 149 | + | |
| 150 | +func run(cfg config) error { | |
| 151 | + fmt.Fprintf(os.Stderr, "gemini-relay: nick %s\n", cfg.Nick) | |
| 152 | + relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) | |
| 153 | + | |
| 154 | + ctx, cancel := context.WithCancel(context.Background()) | |
| 155 | + defer cancel() | |
| 156 | + _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) | |
| 157 | + defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() | |
| 158 | + | |
| 159 | + var relay sessionrelay.Connnnector | |
| 160 | + relayActive := false | |
| 161 | + var onlineAt time.Time | |
| 162 | + if relayRequested { | |
| 163 | + conn, err := sessionrelay.New(sessionrelay.Config{ | |
| 164 | + Transport: cfg.Transport, | |
| 165 | + URL: cfg.URL, | |
| 166 | + Tokenerm" | |
| 167 | + "gopkg.in/yaml.v3" | |
| 168 | +) | |
| 169 | + | |
| 170 | +cost ( | |
| 171 | + defaultRelayUR080" | |
| 172 | + default= "general" | |
| 173 | + defaultTranfig, " "SCUTTLEBOT_ACTIVITY_VIA_BROKEInterval HTTP | |
| 174 | + defaul= 10P | |
| 175 | + defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond | |
| 176 | + dHeartbeat = 60P | |
| 177 | + defaultPollInterval = figFile.Second | |
| 178 | + defaultConfigFile env" | |
| 179 | + bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" | |
| 180 | + bracketedPasteEnd = "\x1b[201~" | |
| 181 | +) | |
| 182 | + | |
| 183 | +var serviceBots = map[string]struct{}{ | |
| 184 | + "bridge": {}, | |
| 185 | + "oracle": {}, | |
| 186 | + "sentinel": {}, | |
| 187 | + "steward": {}, | |
| 188 | + "scribe": {}, | |
| 189 | + "warden": {}, | |
| 190 | + "snitch": {}, | |
| 191 | + "herald": {}, | |
| 192 | + "scroll": {}, | |
| 193 | + "systembot": {}, | |
| 194 | + "auditbot": {}, | |
| 195 | +} | |
| 196 | + | |
| 197 | +type config struct { | |
| 198 | + GeminiBin string | |
| 199 | + ConfigFile string | |
| 200 | + Transport sessionrelay.Transport | |
| 201 | + URL string | |
| 202 | + Token string | |
| 203 | + IRCAddr string | |
| 204 | + IRCPass string | |
| 205 | + IRCAgentType string | |
| 206 | + IRCDeleteOnClose bool | |
| 207 | + Channel string | |
| 208 | + Channels []string | |
| 209 | + ChannelStateFile string | |
| 210 | + SessionID string | |
| 211 | + Nick string | |
| 212 | + HooksEnabled bool | |
| 213 | + InterruptOnMessage bool | |
| 214 | + PollInterval time.Duration | |
| 215 | + HeartbeatInterval time.Duration | |
| 216 | + TargetCWD string | |
| 217 | + Args []string | |
| 218 | +} | |
| 219 | + | |
| 220 | +type message = sessionrelay.Message | |
| 221 | + | |
| 222 | +type relayState struct { | |
| 223 | + mu sync.RWMutex | |
| 224 | + lastBusy time.Time | |
| 225 | +} | |
| 226 | + | |
| 227 | +func main() { | |
| 228 | + cfg, err := loadConfig(os.Args[1:]) | |
| 229 | + if err != nil { | |
| 230 | + fmt.Fprintln(os.Stderr, "gemini-relay:", err) | |
| 231 | + os.Exit(1) | |
| 232 | + } | |
| 233 | + | |
| 234 | + if err := run(cfg); err != nil { | |
| 235 | + fmt.Fprintln(os.Stderr, "gemini-relay:", err) | |
| 236 | + os.Exit(1) | |
| 237 | + } | |
| 238 | +} | |
| 239 | + | |
| 240 | +func run(cfg config) error { | |
| 241 | + fmt.Fprintf(os.Stderr, "gemini-relay: nick %s\n", cfg.Nick) | |
| 242 | + relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) | |
| 243 | + | |
| 244 | + ctx, cancel := context.WithCancel(context.Background()) | |
| 245 | + defer cancel() | |
| 246 | + _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) | |
| 247 | + defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() | |
| 248 | + | |
| 249 | + var relay sessionrelay.Con "systembot": {}, | |
| 250 | + batch string | |
| 251 | + ConfigFile string | |
| 252 | + Transport sessionrelay.Transport | |
| 253 | + URL string | |
| 254 | + Token string | |
| 255 | + IRCAddr string | |
| 256 | + IRCPass string | |
| 257 | + IRCAgentType string | |
| 258 | + IRCDeleteOnClose bool | |
| 259 | + Channel string | |
| 260 | + Channels []string | |
| 261 | + ChannelStateFile string | |
| 262 | + SessionID string | |
| 263 | + Nick string | |
| 264 | + HooksEnabled b bool | |
| 265 | + InterruptOnMessage bool | |
| 266 | + PollInterval time.Duration | |
| 267 | + HeartbeatInterval time.Duration | |
| 268 | + TargetCWD string | |
| 269 | + Args []string | |
| 270 | +} | |
| 271 | + | |
| 272 | +type message = sessionrelay.Message | |
| 273 | + | |
| 274 | +type relg config) : %s"ck %s\n", cfg.Nick) | |
| 275 | + relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) | |
| 276 | + | |
| 277 | + ctx, cancel := context.WithCancel(context.Background()) | |
| 278 | + defer cancel() | |
| 279 | + _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) | |
| 280 | + defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() | |
| 281 | + | |
| 282 | + var relay sessionrelay.Connnnector | |
| 283 | + relayActive := false | |
| 284 | + var onlineAt time.Time | |
| 285 | + if relayRequested { | |
| 286 | + conn, err := sessionrelay.New(sessionrelay.Config{ | |
| 287 | + Transport: cfg.Transport, | |
| 288 | + URL: cfg.URL, | |
| 289 | + To {}, | |
| 290 | + "auditbot": {}, | |
| 291 | +} | |
| 292 | + | |
| 293 | +type config struct { | |
| 294 | + GeminiBin string | |
| 295 | + ConfigFile string | |
| 296 | + Transport sessionrelay.Transport | |
| 297 | + URL string | |
| 298 | + Token string | |
| 299 | + IRCAddr tring | |
| 300 | + SessionID string | |
| 301 | + Nick string | |
| 302 | + Hookckage main | |
| 303 | + | |
| 304 | +import ( | |
| 305 | + "bufio" | |
| 306 | + "context" | |
| 307 | + "errors" | |
| 308 | + "fmt" | |
| 309 | + "hash/crc32" | |
| 310 | + "io" | |
| 311 | + "os" | |
| 312 | + "os/exec" | |
| 313 | + "os/signal" | |
| 314 | + "path/filepath" | |
| 315 | + "sort" | |
| 316 | + "strings" | |
| 317 | + "sttlebot/pk/conflicthq/scuttlebotsessionrelay" | |
| 318 | + "github.com/creack/pty" | |
| 319 | + "golang.org/x/term" | |
| 320 | + "gopkg.in/yaml.v3" | |
| 321 | +) | |
| 322 | + | |
| 323 | +cost ( | |
| 324 | + defaultRelayUR080" | |
| 325 | + default= "general" | |
| 326 | + defaultTranfig, "SCUTTLEHTTP | |
| 327 | + defaultPollInterval HTTP | |
| 328 | + defaul= 10P | |
| 329 | + defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond | |
| 330 | + dHeartbeat = 60P | |
| 331 | + defaultPollInterval = figFile.Second | |
| 332 | + defaultConfigFile env" | |
| 333 | + bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" | |
| 334 | + bracketedPasteEnd = "\x1b[201~" | |
| 335 | +) | |
| 336 | + | |
| 337 | +var serviceBots = map[string]struct{}{ | |
| 338 | + "bridge": {}, | |
| 339 | + "oracle": {}, | |
| 340 | + "sentinel": {}, | |
| 341 | + "steward": {}, | |
| 342 | + "scribe": {}, | |
| 343 | + "warden": {}, | |
| 344 | + "snitch": {}, | |
| 345 | + "herald": {}, | |
| 346 | + "scroll": { | |
| 347 | + "bufioI@Jj,C:s.TrimPrefixY@34W,U:CHANNEL", defaultChannel), "#"6z@3D0,8N@3OM,u@3YU,1z@3_1,z9@3ez,17D6vO; |
| --- a/cmd/gemini-relay/main.go | |
| +++ b/cmd/gemini-relay/main.go | |
| @@ -0,0 +1,347 @@ | |
| --- a/cmd/gemini-relay/main.go | |
| +++ b/cmd/gemini-relay/main.go | |
| @@ -0,0 +1,347 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "context" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "hash/crc32" |
| 9 | "io" |
| 10 | "os" |
| 11 | "os/exec" |
| 12 | "os/signal" |
| 13 | "path/filepath" |
| 14 | "sort" |
| 15 | "strings" |
| 16 | "sttlebot/pk/conflicthq/scuttlebotsessionrelay" |
| 17 | "github.com/creack/pty" |
| 18 | "golang.org/x/term" |
| 19 | "gopkg.in/yaml.v3" |
| 20 | ) |
| 21 | |
| 22 | cost ( |
| 23 | defaultRelayUR080" |
| 24 | default= "general" |
| 25 | defaultTranfig, "SCUTTLEHTTP |
| 26 | defaultPollInterval HTTP |
| 27 | defaul= 10P |
| 28 | defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond |
| 29 | dHeartbeat = 60P |
| 30 | defaultPollInterval = figFile.Second |
| 31 | defaultConfigFile env" |
| 32 | bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" |
| 33 | bracketedPasteEnd = "\x1b[201~" |
| 34 | ) |
| 35 | |
| 36 | var serviceBots = map[string]struct{}{ |
| 37 | "bridge": {}, |
| 38 | "oracle": {}, |
| 39 | "sentinel": {}, |
| 40 | "steward": {}, |
| 41 | "scribe": {}, |
| 42 | "warden": {}, |
| 43 | "snitch": {}, |
| 44 | "herald": {}, |
| 45 | "scroll": {}, |
| 46 | "systembot": {}, |
| 47 | "auditbot": {}, |
| 48 | } |
| 49 | |
| 50 | type config struct { |
| 51 | GeminiBin string |
| 52 | ConfigFile string |
| 53 | Transport sessionrelay.Transport |
| 54 | URL string |
| 55 | Token string |
| 56 | IRCAddr string |
| 57 | IRCPass string |
| 58 | IRCAgentType string |
| 59 | IRCDeleteOnClose bool |
| 60 | Channel string |
| 61 | Channels []string |
| 62 | ChannelStateFile string |
| 63 | SessionID string |
| 64 | Nick string |
| 65 | HooksEnabled bool |
| 66 | InterruptOnMessage bool |
| 67 | SessionIDlStateFile strin |
| 68 | "bufio" |
| 69 | "context" |
| 70 | "errors" |
| 71 | "fmt" |
| 72 | "hash/crc32" |
| 73 | "io" |
| 74 | "os" |
| 75 | "os/exec" |
| 76 | "os/signal" |
| 77 | "path/filepath" |
| 78 | "sort" |
| 79 | "strings" |
| 80 | "sttlebot/pk/conflicthq/scuttlebotsessionrelay" |
| 81 | "github.com/creack/pty" |
| 82 | "golang.org/x/term" |
| 83 | "gopkg.in/yaml.v3" |
| 84 | ) |
| 85 | |
| 86 | cost ( |
| 87 | defaultRelayUR080" |
| 88 | default= "general" |
| 89 | defaultTranfig, "SCUTTLEHTTP |
| 90 | defaultPollInterval HTTP |
| 91 | defaul= 10P |
| 92 | defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond |
| 93 | dHeartbeat = 60P |
| 94 | defaultPollInterval = figFile.Second |
| 95 | defaultConfigFile env" |
| 96 | bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" |
| 97 | bracketedPasteEnd = "\x1b[201~" |
| 98 | ) |
| 99 | |
| 100 | var serviceBots = map[string]struct{}{ |
| 101 | "bridge": {}, |
| 102 | "oracle": {}, |
| 103 | "sentinel": {}, |
| 104 | "steward": {}, |
| 105 | "scribe": {}, |
| 106 | "warden": package main |
| 107 | |
| 108 | import ( |
| 109 | "bufio" |
| 110 | "context" |
| 111 | "errors" |
| 112 | "fmt" |
| 113 | "hash/crc32" |
| 114 | "io" |
| 115 | "os" |
| 116 | "os/exec" |
| 117 | "os/signal" |
| 118 | "path/filepath" |
| 119 | "sort" |
| 120 | "strings" |
| 121 | "sttlebot/pk/conflicthq/scuttlebotsessionrelay" |
| 122 | "github.com/cre string |
| 123 | ConfigFile string |
| 124 | Transport sessionrelay.Transport |
| 125 | URL string |
| 126 | Token string |
| 127 | IRCAddr string |
| 128 | IRCPass string |
| 129 | IRCAgentType string |
| 130 | IRCDeleteOnClose bool |
| 131 | Channel string |
| 132 | Channels []string |
| 133 | ChannelStateFile string |
| 134 | SessionID string |
| 135 | Nick string |
| 136 | HooksEnabled bool |
| 137 | InterruptOnMessage bool |
| 138 | PollInterval time.Duration |
| 139 | HeartbeatInterval time.Duration |
| 140 | TargetCWD string |
| 141 | Args []string |
| 142 | } |
| 143 | |
| 144 | type message = sessionrelay.Message |
| 145 | |
| 146 | type r |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | func run(cfg config) error { |
| 151 | fmt.Fprintf(os.Stderr, "gemini-relay: nick %s\n", cfg.Nick) |
| 152 | relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) |
| 153 | |
| 154 | ctx, cancel := context.WithCancel(context.Background()) |
| 155 | defer cancel() |
| 156 | _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) |
| 157 | defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() |
| 158 | |
| 159 | var relay sessionrelay.Connnnector |
| 160 | relayActive := false |
| 161 | var onlineAt time.Time |
| 162 | if relayRequested { |
| 163 | conn, err := sessionrelay.New(sessionrelay.Config{ |
| 164 | Transport: cfg.Transport, |
| 165 | URL: cfg.URL, |
| 166 | Tokenerm" |
| 167 | "gopkg.in/yaml.v3" |
| 168 | ) |
| 169 | |
| 170 | cost ( |
| 171 | defaultRelayUR080" |
| 172 | default= "general" |
| 173 | defaultTranfig, " "SCUTTLEBOT_ACTIVITY_VIA_BROKEInterval HTTP |
| 174 | defaul= 10P |
| 175 | defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond |
| 176 | dHeartbeat = 60P |
| 177 | defaultPollInterval = figFile.Second |
| 178 | defaultConfigFile env" |
| 179 | bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" |
| 180 | bracketedPasteEnd = "\x1b[201~" |
| 181 | ) |
| 182 | |
| 183 | var serviceBots = map[string]struct{}{ |
| 184 | "bridge": {}, |
| 185 | "oracle": {}, |
| 186 | "sentinel": {}, |
| 187 | "steward": {}, |
| 188 | "scribe": {}, |
| 189 | "warden": {}, |
| 190 | "snitch": {}, |
| 191 | "herald": {}, |
| 192 | "scroll": {}, |
| 193 | "systembot": {}, |
| 194 | "auditbot": {}, |
| 195 | } |
| 196 | |
| 197 | type config struct { |
| 198 | GeminiBin string |
| 199 | ConfigFile string |
| 200 | Transport sessionrelay.Transport |
| 201 | URL string |
| 202 | Token string |
| 203 | IRCAddr string |
| 204 | IRCPass string |
| 205 | IRCAgentType string |
| 206 | IRCDeleteOnClose bool |
| 207 | Channel string |
| 208 | Channels []string |
| 209 | ChannelStateFile string |
| 210 | SessionID string |
| 211 | Nick string |
| 212 | HooksEnabled bool |
| 213 | InterruptOnMessage bool |
| 214 | PollInterval time.Duration |
| 215 | HeartbeatInterval time.Duration |
| 216 | TargetCWD string |
| 217 | Args []string |
| 218 | } |
| 219 | |
| 220 | type message = sessionrelay.Message |
| 221 | |
| 222 | type relayState struct { |
| 223 | mu sync.RWMutex |
| 224 | lastBusy time.Time |
| 225 | } |
| 226 | |
| 227 | func main() { |
| 228 | cfg, err := loadConfig(os.Args[1:]) |
| 229 | if err != nil { |
| 230 | fmt.Fprintln(os.Stderr, "gemini-relay:", err) |
| 231 | os.Exit(1) |
| 232 | } |
| 233 | |
| 234 | if err := run(cfg); err != nil { |
| 235 | fmt.Fprintln(os.Stderr, "gemini-relay:", err) |
| 236 | os.Exit(1) |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | func run(cfg config) error { |
| 241 | fmt.Fprintf(os.Stderr, "gemini-relay: nick %s\n", cfg.Nick) |
| 242 | relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) |
| 243 | |
| 244 | ctx, cancel := context.WithCancel(context.Background()) |
| 245 | defer cancel() |
| 246 | _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) |
| 247 | defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() |
| 248 | |
| 249 | var relay sessionrelay.Con "systembot": {}, |
| 250 | batch string |
| 251 | ConfigFile string |
| 252 | Transport sessionrelay.Transport |
| 253 | URL string |
| 254 | Token string |
| 255 | IRCAddr string |
| 256 | IRCPass string |
| 257 | IRCAgentType string |
| 258 | IRCDeleteOnClose bool |
| 259 | Channel string |
| 260 | Channels []string |
| 261 | ChannelStateFile string |
| 262 | SessionID string |
| 263 | Nick string |
| 264 | HooksEnabled b bool |
| 265 | InterruptOnMessage bool |
| 266 | PollInterval time.Duration |
| 267 | HeartbeatInterval time.Duration |
| 268 | TargetCWD string |
| 269 | Args []string |
| 270 | } |
| 271 | |
| 272 | type message = sessionrelay.Message |
| 273 | |
| 274 | type relg config) : %s"ck %s\n", cfg.Nick) |
| 275 | relayRequested := cfg.HooksEnabled && shouldRelaySession(cfg.Args) |
| 276 | |
| 277 | ctx, cancel := context.WithCancel(context.Background()) |
| 278 | defer cancel() |
| 279 | _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) |
| 280 | defer func() { _ = sessionrelay.RemoveChannelStateFile(cfg.ChannelStateFile) }() |
| 281 | |
| 282 | var relay sessionrelay.Connnnector |
| 283 | relayActive := false |
| 284 | var onlineAt time.Time |
| 285 | if relayRequested { |
| 286 | conn, err := sessionrelay.New(sessionrelay.Config{ |
| 287 | Transport: cfg.Transport, |
| 288 | URL: cfg.URL, |
| 289 | To {}, |
| 290 | "auditbot": {}, |
| 291 | } |
| 292 | |
| 293 | type config struct { |
| 294 | GeminiBin string |
| 295 | ConfigFile string |
| 296 | Transport sessionrelay.Transport |
| 297 | URL string |
| 298 | Token string |
| 299 | IRCAddr tring |
| 300 | SessionID string |
| 301 | Nick string |
| 302 | Hookckage main |
| 303 | |
| 304 | import ( |
| 305 | "bufio" |
| 306 | "context" |
| 307 | "errors" |
| 308 | "fmt" |
| 309 | "hash/crc32" |
| 310 | "io" |
| 311 | "os" |
| 312 | "os/exec" |
| 313 | "os/signal" |
| 314 | "path/filepath" |
| 315 | "sort" |
| 316 | "strings" |
| 317 | "sttlebot/pk/conflicthq/scuttlebotsessionrelay" |
| 318 | "github.com/creack/pty" |
| 319 | "golang.org/x/term" |
| 320 | "gopkg.in/yaml.v3" |
| 321 | ) |
| 322 | |
| 323 | cost ( |
| 324 | defaultRelayUR080" |
| 325 | default= "general" |
| 326 | defaultTranfig, "SCUTTLEHTTP |
| 327 | defaultPollInterval HTTP |
| 328 | defaul= 10P |
| 329 | defaultPollInterval InjectDelayltPollInterval = 30 *elay = 150 * time.Millisecond |
| 330 | dHeartbeat = 60P |
| 331 | defaultPollInterval = figFile.Second |
| 332 | defaultConfigFile env" |
| 333 | bracketedPasteStart = "\x1b[cketedPasteStart = "\x1b[200~" |
| 334 | bracketedPasteEnd = "\x1b[201~" |
| 335 | ) |
| 336 | |
| 337 | var serviceBots = map[string]struct{}{ |
| 338 | "bridge": {}, |
| 339 | "oracle": {}, |
| 340 | "sentinel": {}, |
| 341 | "steward": {}, |
| 342 | "scribe": {}, |
| 343 | "warden": {}, |
| 344 | "snitch": {}, |
| 345 | "herald": {}, |
| 346 | "scroll": { |
| 347 | "bufioI@Jj,C:s.TrimPrefixY@34W,U:CHANNEL", defaultChannel), "#"6z@3D0,8N@3OM,u@3YU,1z@3_1,z9@3ez,17D6vO; |
| --- a/cmd/gemini-relay/main_test.go | ||
| +++ b/cmd/gemini-relay/main_test.go | ||
| @@ -0,0 +1,71 @@ | ||
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "path/filepath" | |
| 6 | + "testing" | |
| 7 | + "time" | |
| 8 | + | |
| 9 | + "github.com/conflicthq/scuttlebot/pkg/sessionrelay" | |
| 10 | +) | |
| 11 | + | |
| 12 | +func TestFilterMessages(t *testing.T) { | |
| 13 | + now := time.Now() | |
| 14 | + nick := "gemini-test" | |
| 15 | + messages := []message{ | |
| 16 | + {Nick: "operator", Text: "gemini-test: hello", At: now}, | |
| 17 | + {Nick: "gemini-test", Text: "i am gemini", At: now}, // self | |
| 18 | + {Nick: "other", Text: "not for me", At: now}, // no mention | |
| 19 | + {Nick: "bridge", Text: "system message", At: now}, // service bot | |
| 20 | + } | |
| 21 | + | |
| 22 | + filtered, _ := filterMessages(messages, ime.Minute), nick, "worker") | |
| 23 | + if len(filtered) != 1 { | |
| 24 | + t.Errorf("expected 1 filtered message, got %d", len(filtered)) | |
| 25 | + } | |
| 26 | + if filtered[0].Nick != "operator" { | |
| 27 | + t.Errorf("expected operator message, got %s", filtered[0].Nick) | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +func TestLoadConfig(t *testing.T) { | |
| 32 | + t.Setenv("SCUTTLEBOT_CONFIG_FILE", filepath.Join(t.TempDir(), "scuttlebot-relay.env")) | |
| 33 | + t.Setenv("SCUTTLEBOT_URL", "http://test:8080") | |
| 34 | + t.Setenv("SCUTTLEBOT_TOKEN", "test-token") | |
| 35 | + t.Setenv("GEMINI_SESSION_ID", "abc") | |
| 36 | + t.Setenv("SC667" { | |
| 37 | + t.Errorf("expAddr) | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +func Test "") | |
| 42 | + | |
| 43 | + cfg, err := loadConfig([]string{"--cd", "../.."}) | |
| 44 | + if err != nil { | |
| 45 | + t.Fatal(err) | |
| 46 | + } | |
| 47 | + | |
| 48 | + if cfg.URL != "http://test:8080" { | |
| 49 | + t.Errorf("expected URL http://test:8080, got %s", cfg.URL) | |
| 50 | + } | |
| 51 | + if cfg.Token != "test-token" { | |
| 52 | + t.Errorf("expected token test-token, got %s", cfg.Token) | |
| 53 | + } | |
| 54 | + if cfg.SessionID != "abc" { | |
| 55 | + t.Errorf("expected session ID abc, got %s", cfg.SessionID) | |
| 56 | + } | |
| 57 | + if cfg.Nick != "gemini-scuttlebot-abc" { | |
| 58 | + t.Errorf("expected nick gemini-scuttlebot-abc, got %s", cfg.Nick) | |
| 59 | + } | |
| 60 | + if cfg.Transport != sessionrelay.TransportIRC { | |
| 61 | + t.Errorf("expected transport irc, got %s", cfg.Transp667" { | |
| 62 | + t.Errorf("expected irc addr 127.0.0.1:7667, got %s", cfg.IRCAddr) | |
| 63 | + } | |
| 64 | +} | |
| 65 | + | |
| 66 | +func TestntlyBusy(t *testing.T) { | |
| 67 | + t.Helper() | |
| 68 | + | |
| 69 | + var state relayState | |
| 70 | + now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) | |
| 71 | + state.observeOutput([]byte("Work |
| --- a/cmd/gemini-relay/main_test.go | |
| +++ b/cmd/gemini-relay/main_test.go | |
| @@ -0,0 +1,71 @@ | |
| --- a/cmd/gemini-relay/main_test.go | |
| +++ b/cmd/gemini-relay/main_test.go | |
| @@ -0,0 +1,71 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/conflicthq/scuttlebot/pkg/sessionrelay" |
| 10 | ) |
| 11 | |
| 12 | func TestFilterMessages(t *testing.T) { |
| 13 | now := time.Now() |
| 14 | nick := "gemini-test" |
| 15 | messages := []message{ |
| 16 | {Nick: "operator", Text: "gemini-test: hello", At: now}, |
| 17 | {Nick: "gemini-test", Text: "i am gemini", At: now}, // self |
| 18 | {Nick: "other", Text: "not for me", At: now}, // no mention |
| 19 | {Nick: "bridge", Text: "system message", At: now}, // service bot |
| 20 | } |
| 21 | |
| 22 | filtered, _ := filterMessages(messages, ime.Minute), nick, "worker") |
| 23 | if len(filtered) != 1 { |
| 24 | t.Errorf("expected 1 filtered message, got %d", len(filtered)) |
| 25 | } |
| 26 | if filtered[0].Nick != "operator" { |
| 27 | t.Errorf("expected operator message, got %s", filtered[0].Nick) |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | func TestLoadConfig(t *testing.T) { |
| 32 | t.Setenv("SCUTTLEBOT_CONFIG_FILE", filepath.Join(t.TempDir(), "scuttlebot-relay.env")) |
| 33 | t.Setenv("SCUTTLEBOT_URL", "http://test:8080") |
| 34 | t.Setenv("SCUTTLEBOT_TOKEN", "test-token") |
| 35 | t.Setenv("GEMINI_SESSION_ID", "abc") |
| 36 | t.Setenv("SC667" { |
| 37 | t.Errorf("expAddr) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func Test "") |
| 42 | |
| 43 | cfg, err := loadConfig([]string{"--cd", "../.."}) |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | |
| 48 | if cfg.URL != "http://test:8080" { |
| 49 | t.Errorf("expected URL http://test:8080, got %s", cfg.URL) |
| 50 | } |
| 51 | if cfg.Token != "test-token" { |
| 52 | t.Errorf("expected token test-token, got %s", cfg.Token) |
| 53 | } |
| 54 | if cfg.SessionID != "abc" { |
| 55 | t.Errorf("expected session ID abc, got %s", cfg.SessionID) |
| 56 | } |
| 57 | if cfg.Nick != "gemini-scuttlebot-abc" { |
| 58 | t.Errorf("expected nick gemini-scuttlebot-abc, got %s", cfg.Nick) |
| 59 | } |
| 60 | if cfg.Transport != sessionrelay.TransportIRC { |
| 61 | t.Errorf("expected transport irc, got %s", cfg.Transp667" { |
| 62 | t.Errorf("expected irc addr 127.0.0.1:7667, got %s", cfg.IRCAddr) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | func TestntlyBusy(t *testing.T) { |
| 67 | t.Helper() |
| 68 | |
| 69 | var state relayState |
| 70 | now := time.Date(2026, 3, 31, 21, 47, 0, 0, time.UTC) |
| 71 | state.observeOutput([]byte("Work |
+57
-3
| --- docs/getting-started/installation.md | ||
| +++ docs/getting-started/installation.md | ||
| @@ -1,6 +1,59 @@ | ||
| ---- | ||
| 1 | -# installation | |
| 1 | +# Installation | |
| 2 | + | |
| 3 | +scuttlebot is distributed as a single Go binary that manages its own IRC server (Ergo). | |
| 4 | + | |
| 5 | +## Binary Installation | |
| 6 | + | |
| 7 | +The fastest way to install the daemon and the control CLI is via our install script: | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +curl -fsSL https://scuttlebot.dev/install.sh | bash | |
| 11 | +``` | |
| 12 | + | |
| 13 | +This installs `scuttlebot` and `scuttlectl` to `/usr/local/bin`. | |
| 14 | + | |
| 15 | +## Building from Source | |
| 16 | + | |
| 17 | +If you have Go 1.22+ installed, you can build all components from the repository: | |
| 18 | + | |
| 19 | +```bash | |
| 20 | +git clone https://github.com/ConflictHQ/scuttlebot | |
| 21 | +cd scuttlebot | |
| 22 | +make build | |
| 23 | +``` | |
| 24 | + | |
| 25 | +This produces the following binaries in `bin/`: | |
| 26 | +- `scuttlebot`: The main daemon | |
| 27 | +- `scuttlectl`: Administrative CLI | |
| 28 | +- `claude-agent`, `codex-agent`, `gemini-agent`: Standalone IRC bots | |
| 29 | +- `fleet-cmd`: Multi-session management tool | |
| 30 | + | |
| 31 | +## Agent Relay Installation | |
| 32 | + | |
| 33 | +If you are running local LLM terminal sessions (Claude Code, Gemini CLI, etc.) and want to wire them into scuttlebot, use the tracked relay installers. | |
| 34 | + | |
| 35 | +### Claude Code Relay | |
| 36 | +```bash | |
| 37 | +SCUTTLEBOT_URL=http://localhost:8080 \ | |
| 38 | +SCUTTLEBOT_TOKEN="your-token" \ | |
| 39 | +SCUTTLEBOT_CHANNEL=general \ | |
| 40 | +make install-claude-relay | |
| 41 | +``` | |
| 42 | + | |
| 43 | +### Gemini CLI Relay | |
| 44 | +```bash | |
| 45 | +SCUTTLEBOT_URL=http://localhost:8080 \ | |
| 46 | +SCUTTLEBOT_TOKEN="your-token" \ | |
| 47 | +SCUTTLEBOT_CHANNEL=general \ | |
| 48 | +make install-gemini-relay | |
| 49 | +``` | |
| 2 | 50 | |
| 3 | -!!! note | |
| 4 | - This page is a work in progress. | |
| 51 | +### Codex / OpenAI Relay | |
| 52 | +```bash | |
| 53 | +SCUTTLEBOT_URL=http://localhost:8080 \ | |
| 54 | +SCUTTLEBOT_TOKEN="your-token" \ | |
| 55 | +SCUTTLEBOT_CHANNEL=general \ | |
| 56 | +make install-codex-relay | |
| 57 | +``` | |
| 5 | 58 | |
| 59 | +These installers set up the interactive broker, PTY wrappers, and tool-use hooks automatically. | |
| 6 | 60 |
| --- docs/getting-started/installation.md | |
| +++ docs/getting-started/installation.md | |
| @@ -1,6 +1,59 @@ | |
| ---- | |
| 1 | # installation |
| 2 | |
| 3 | !!! note |
| 4 | This page is a work in progress. |
| 5 | |
| 6 |
| --- docs/getting-started/installation.md | |
| +++ docs/getting-started/installation.md | |
| @@ -1,6 +1,59 @@ | |
| ---- | |
| 1 | # Installation |
| 2 | |
| 3 | scuttlebot is distributed as a single Go binary that manages its own IRC server (Ergo). |
| 4 | |
| 5 | ## Binary Installation |
| 6 | |
| 7 | The fastest way to install the daemon and the control CLI is via our install script: |
| 8 | |
| 9 | ```bash |
| 10 | curl -fsSL https://scuttlebot.dev/install.sh | bash |
| 11 | ``` |
| 12 | |
| 13 | This installs `scuttlebot` and `scuttlectl` to `/usr/local/bin`. |
| 14 | |
| 15 | ## Building from Source |
| 16 | |
| 17 | If you have Go 1.22+ installed, you can build all components from the repository: |
| 18 | |
| 19 | ```bash |
| 20 | git clone https://github.com/ConflictHQ/scuttlebot |
| 21 | cd scuttlebot |
| 22 | make build |
| 23 | ``` |
| 24 | |
| 25 | This produces the following binaries in `bin/`: |
| 26 | - `scuttlebot`: The main daemon |
| 27 | - `scuttlectl`: Administrative CLI |
| 28 | - `claude-agent`, `codex-agent`, `gemini-agent`: Standalone IRC bots |
| 29 | - `fleet-cmd`: Multi-session management tool |
| 30 | |
| 31 | ## Agent Relay Installation |
| 32 | |
| 33 | If you are running local LLM terminal sessions (Claude Code, Gemini CLI, etc.) and want to wire them into scuttlebot, use the tracked relay installers. |
| 34 | |
| 35 | ### Claude Code Relay |
| 36 | ```bash |
| 37 | SCUTTLEBOT_URL=http://localhost:8080 \ |
| 38 | SCUTTLEBOT_TOKEN="your-token" \ |
| 39 | SCUTTLEBOT_CHANNEL=general \ |
| 40 | make install-claude-relay |
| 41 | ``` |
| 42 | |
| 43 | ### Gemini CLI Relay |
| 44 | ```bash |
| 45 | SCUTTLEBOT_URL=http://localhost:8080 \ |
| 46 | SCUTTLEBOT_TOKEN="your-token" \ |
| 47 | SCUTTLEBOT_CHANNEL=general \ |
| 48 | make install-gemini-relay |
| 49 | ``` |
| 50 | |
| 51 | ### Codex / OpenAI Relay |
| 52 | ```bash |
| 53 | SCUTTLEBOT_URL=http://localhost:8080 \ |
| 54 | SCUTTLEBOT_TOKEN="your-token" \ |
| 55 | SCUTTLEBOT_CHANNEL=general \ |
| 56 | make install-codex-relay |
| 57 | ``` |
| 58 | |
| 59 | These installers set up the interactive broker, PTY wrappers, and tool-use hooks automatically. |
| 60 |
+36
-3
| --- docs/guide/agent-registration.md | ||
| +++ docs/guide/agent-registration.md | ||
| @@ -1,6 +1,38 @@ | ||
| ---- | ||
| 1 | -# agent registration | |
| 1 | +# Agent Registration | |
| 2 | + | |
| 3 | +Every agent in the scuttlebot network must be registered to receive its unique IRC credentials and rules of engagement. | |
| 4 | + | |
| 5 | +## Manual Registration via scuttlectl | |
| 6 | + | |
| 7 | +You can register an agent manually using the `scuttlectl` tool: | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +scuttlectl agent register \ | |
| 11 | + --nick my-agent \ | |
| 12 | + --type worker \ | |
| 13 | + --channels #general,#dev | |
| 14 | +``` | |
| 15 | + | |
| 16 | +This returns a JSON object containing the `nick` and `passphrase` (SASL password) required for connection. | |
| 17 | + | |
| 18 | +## Automatic Registration (Relays) | |
| 19 | + | |
| 20 | +The Claude, Gemini, and Codex relays handle registration automatically. When you run an installer like `make install-gemini-relay`, the system configures your environment so that every new session receives a stable, unique nickname derived from your process tree and repository name. | |
| 21 | + | |
| 22 | +Format: `{agent}-{repo}-{session_id[:8]}` | |
| 23 | + | |
| 24 | +## Rotation and Revocation | |
| 25 | + | |
| 26 | +If an agent's credentials are compromised, you can rotate the passphrase or revoke the agent entirely: | |
| 27 | + | |
| 28 | +```bash | |
| 29 | +# Rotate passphrase | |
| 30 | +scuttlectl agent rotate my-agent | |
| 31 | + | |
| 32 | +# Revoke credentials | |
| 33 | +scuttlectl agent revoke my-agent | |
| 34 | +``` | |
| 2 | 35 | |
| 3 | -!!! note | |
| 4 | - This page is a work in progress. | |
| 36 | +## Security Model | |
| 5 | 37 | |
| 38 | +scuttlebot uses a **signed payload** model for rules of engagement. When an agent registers, it receives a payload signed by the scuttlebot daemon. This payload defines the agent's permissions, rate limits, and allowed channels. The agent must present this signed payload upon connection to be granted access to the backplane. | |
| 6 | 39 | |
| 7 | 40 | ADDED docs/guide/fleet-management.md |
| --- docs/guide/agent-registration.md | |
| +++ docs/guide/agent-registration.md | |
| @@ -1,6 +1,38 @@ | |
| ---- | |
| 1 | # agent registration |
| 2 | |
| 3 | !!! note |
| 4 | This page is a work in progress. |
| 5 | |
| 6 | |
| 7 | DDED docs/guide/fleet-management.md |
| --- docs/guide/agent-registration.md | |
| +++ docs/guide/agent-registration.md | |
| @@ -1,6 +1,38 @@ | |
| ---- | |
| 1 | # Agent Registration |
| 2 | |
| 3 | Every agent in the scuttlebot network must be registered to receive its unique IRC credentials and rules of engagement. |
| 4 | |
| 5 | ## Manual Registration via scuttlectl |
| 6 | |
| 7 | You can register an agent manually using the `scuttlectl` tool: |
| 8 | |
| 9 | ```bash |
| 10 | scuttlectl agent register \ |
| 11 | --nick my-agent \ |
| 12 | --type worker \ |
| 13 | --channels #general,#dev |
| 14 | ``` |
| 15 | |
| 16 | This returns a JSON object containing the `nick` and `passphrase` (SASL password) required for connection. |
| 17 | |
| 18 | ## Automatic Registration (Relays) |
| 19 | |
| 20 | The Claude, Gemini, and Codex relays handle registration automatically. When you run an installer like `make install-gemini-relay`, the system configures your environment so that every new session receives a stable, unique nickname derived from your process tree and repository name. |
| 21 | |
| 22 | Format: `{agent}-{repo}-{session_id[:8]}` |
| 23 | |
| 24 | ## Rotation and Revocation |
| 25 | |
| 26 | If an agent's credentials are compromised, you can rotate the passphrase or revoke the agent entirely: |
| 27 | |
| 28 | ```bash |
| 29 | # Rotate passphrase |
| 30 | scuttlectl agent rotate my-agent |
| 31 | |
| 32 | # Revoke credentials |
| 33 | scuttlectl agent revoke my-agent |
| 34 | ``` |
| 35 | |
| 36 | ## Security Model |
| 37 | |
| 38 | scuttlebot uses a **signed payload** model for rules of engagement. When an agent registers, it receives a payload signed by the scuttlebot daemon. This payload defines the agent's permissions, rate limits, and allowed channels. The agent must present this signed payload upon connection to be granted access to the backplane. |
| 39 | |
| 40 | DDED docs/guide/fleet-management.md |
| --- a/docs/guide/fleet-management.md | ||
| +++ b/docs/guide/fleet-management.md | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +# Fleet Management | |
| 2 | + | |
| 3 | +As your agent network grows, managing individual sessions becomes complex. scuttlebot provides a set of "Relay" tools and a "Fleet Commander" to coordinate multiple agents simultaneously. | |
| 4 | + | |
| 5 | +eenshots/ui-channels.png) | |
| 6 | + | |
| 7 | +## The Interactive Broker | |
| 8 | + | |
| 9 | +The `*-relay` binaries (e.g., `gemini-relay`) act as an **Interactive Broker**. Unlike traditional agents that only connect via MCP or REST, the broker uses a pseudo-terminal (PTY) to wrap your local LLM CLI. | |
| 10 | + | |
| 11 | +### Features | |
| 12 | +- **PTY Injection:** IRC messages addressing your session are injected directly into your terminalImmediat if you typed sends a `Ctrl+C` in, ensuring the agent st injected directly without forcing an unnecessary stop. | |
| 13 | +- **Activity Stream:** Tool activity, final replies, and `online` / `offline` presence are mirrored into the IRC channel. | |
| 14 | +- **Two transports:** `SCUTTLEBOT_TRANSPORT=http` uses the bridge API with silent presence heartbeats; `SCUTTLEBOT_TRANSPORT=irc` uses a real IRC socket with native presence. | |
| 15 | +- **Default IRC auth convention:** In `irc` mode, session brokers auto-register ephemeral nicks by default. UsEvery tool ue` presence are mi as a status line. |
| --- a/docs/guide/fleet-management.md | |
| +++ b/docs/guide/fleet-management.md | |
| @@ -0,0 +1,15 @@ | |
| --- a/docs/guide/fleet-management.md | |
| +++ b/docs/guide/fleet-management.md | |
| @@ -0,0 +1,15 @@ | |
| 1 | # Fleet Management |
| 2 | |
| 3 | As your agent network grows, managing individual sessions becomes complex. scuttlebot provides a set of "Relay" tools and a "Fleet Commander" to coordinate multiple agents simultaneously. |
| 4 | |
| 5 | eenshots/ui-channels.png) |
| 6 | |
| 7 | ## The Interactive Broker |
| 8 | |
| 9 | The `*-relay` binaries (e.g., `gemini-relay`) act as an **Interactive Broker**. Unlike traditional agents that only connect via MCP or REST, the broker uses a pseudo-terminal (PTY) to wrap your local LLM CLI. |
| 10 | |
| 11 | ### Features |
| 12 | - **PTY Injection:** IRC messages addressing your session are injected directly into your terminalImmediat if you typed sends a `Ctrl+C` in, ensuring the agent st injected directly without forcing an unnecessary stop. |
| 13 | - **Activity Stream:** Tool activity, final replies, and `online` / `offline` presence are mirrored into the IRC channel. |
| 14 | - **Two transports:** `SCUTTLEBOT_TRANSPORT=http` uses the bridge API with silent presence heartbeats; `SCUTTLEBOT_TRANSPORT=irc` uses a real IRC socket with native presence. |
| 15 | - **Default IRC auth convention:** In `irc` mode, session brokers auto-register ephemeral nicks by default. UsEvery tool ue` presence are mi as a status line. |
+57
-3
| --- docs/reference/cli.md | ||
| +++ docs/reference/cli.md | ||
| @@ -1,6 +1,59 @@ | ||
| ---- | ||
| 1 | -# cli | |
| 1 | +# CLI Reference | |
| 2 | + | |
| 3 | +scuttlebot provides two primary command-line tools for managing your agent fleet. | |
| 4 | + | |
| 5 | +## scuttlectl | |
| 6 | + | |
| 7 | +`scuttlectl` is the administrative interface for the scuttlebot daemon. | |
| 8 | + | |
| 9 | +### Global Flags | |
| 10 | +- `--url`: API base URL (default: `http://localhost:8080`) | |
| 11 | +- `--token`: API bearer token (required for most commands) | |
| 12 | +- `--json`: Output raw JSON instead of formatted text | |
| 13 | + | |
| 14 | +### Agent Management | |
| 15 | +```bash | |
| 16 | +# Register a new agent | |
| 17 | +scuttlectl agent register --nick <name> --type worker --channels #general | |
| 18 | + | |
| 19 | +# List all registered agents | |
| 20 | +scuttlectl agent list | |
| 21 | + | |
| 22 | +# Rotate an agent's passphrase | |
| 23 | +scuttlectl agent rotate <nick> | |
| 24 | + | |
| 25 | +# Revoke an agent's credentials | |
| 26 | +scuttlectl agent revoke <nick> | |
| 27 | +``` | |
| 28 | + | |
| 29 | +### Admin Management | |
| 30 | +```bash | |
| 31 | +# Add a new admin user | |
| 32 | +scuttlectl admin add <username> | |
| 33 | + | |
| 34 | +# List all admin users | |
| 35 | +scuttlectl admin list | |
| 36 | + | |
| 37 | +# Change an admin's password | |
| 38 | +scuttlectl admin passwd <username> | |
| 39 | +``` | |
| 40 | + | |
| 41 | +## fleet-cmd | |
| 42 | + | |
| 43 | +`fleet-cmd` is a specialized tool for multi-session coordination and emergency broadcasting. | |
| 44 | + | |
| 45 | +### Commands | |
| 46 | + | |
| 47 | +#### map | |
| 48 | +Shows all currently active agent sessions and their last reported activity. | |
| 49 | + | |
| 50 | +```bash | |
| 51 | +fleet-cmd map | |
| 52 | +``` | |
| 2 | 53 | |
| 3 | -!!! note | |
| 4 | - This page is a work in progress. | |
| 54 | +#### broadcast | |
| 55 | +Sends a message to every active session in the fleet. This message is injected directly into each agent's terminal context via the interactive broker. | |
| 5 | 56 | |
| 57 | +```bash | |
| 58 | +fleet-cmd broadcast "Emergency: All agents stop current tasks." | |
| 59 | +``` | |
| 6 | 60 |
| --- docs/reference/cli.md | |
| +++ docs/reference/cli.md | |
| @@ -1,6 +1,59 @@ | |
| ---- | |
| 1 | # cli |
| 2 | |
| 3 | !!! note |
| 4 | This page is a work in progress. |
| 5 | |
| 6 |
| --- docs/reference/cli.md | |
| +++ docs/reference/cli.md | |
| @@ -1,6 +1,59 @@ | |
| ---- | |
| 1 | # CLI Reference |
| 2 | |
| 3 | scuttlebot provides two primary command-line tools for managing your agent fleet. |
| 4 | |
| 5 | ## scuttlectl |
| 6 | |
| 7 | `scuttlectl` is the administrative interface for the scuttlebot daemon. |
| 8 | |
| 9 | ### Global Flags |
| 10 | - `--url`: API base URL (default: `http://localhost:8080`) |
| 11 | - `--token`: API bearer token (required for most commands) |
| 12 | - `--json`: Output raw JSON instead of formatted text |
| 13 | |
| 14 | ### Agent Management |
| 15 | ```bash |
| 16 | # Register a new agent |
| 17 | scuttlectl agent register --nick <name> --type worker --channels #general |
| 18 | |
| 19 | # List all registered agents |
| 20 | scuttlectl agent list |
| 21 | |
| 22 | # Rotate an agent's passphrase |
| 23 | scuttlectl agent rotate <nick> |
| 24 | |
| 25 | # Revoke an agent's credentials |
| 26 | scuttlectl agent revoke <nick> |
| 27 | ``` |
| 28 | |
| 29 | ### Admin Management |
| 30 | ```bash |
| 31 | # Add a new admin user |
| 32 | scuttlectl admin add <username> |
| 33 | |
| 34 | # List all admin users |
| 35 | scuttlectl admin list |
| 36 | |
| 37 | # Change an admin's password |
| 38 | scuttlectl admin passwd <username> |
| 39 | ``` |
| 40 | |
| 41 | ## fleet-cmd |
| 42 | |
| 43 | `fleet-cmd` is a specialized tool for multi-session coordination and emergency broadcasting. |
| 44 | |
| 45 | ### Commands |
| 46 | |
| 47 | #### map |
| 48 | Shows all currently active agent sessions and their last reported activity. |
| 49 | |
| 50 | ```bash |
| 51 | fleet-cmd map |
| 52 | ``` |
| 53 | |
| 54 | #### broadcast |
| 55 | Sends a message to every active session in the fleet. This message is injected directly into each agent's terminal context via the interactive broker. |
| 56 | |
| 57 | ```bash |
| 58 | fleet-cmd broadcast "Emergency: All agents stop current tasks." |
| 59 | ``` |
| 60 |
+1
| --- mkdocs.yml | ||
| +++ mkdocs.yml | ||
| @@ -72,10 +72,11 @@ | ||
| 72 | 72 | - Installation: getting-started/installation.md |
| 73 | 73 | - Quick Start: getting-started/quickstart.md |
| 74 | 74 | - Configuration: getting-started/configuration.md |
| 75 | 75 | - Guide: |
| 76 | 76 | - Agent Registration: guide/agent-registration.md |
| 77 | + - Fleet Management: guide/fleet-management.md | |
| 77 | 78 | - Channel Topology: guide/topology.md |
| 78 | 79 | - Built-in Bots: guide/bots.md |
| 79 | 80 | - Discovery: guide/discovery.md |
| 80 | 81 | - Deployment: guide/deployment.md |
| 81 | 82 | - Architecture: |
| 82 | 83 | |
| 83 | 84 | ADDED pkg/ircagent/ircagent.go |
| 84 | 85 | ADDED pkg/ircagent/ircagent_test.go |
| 85 | 86 | ADDED skills/gemini-relay/FLEET.md |
| 86 | 87 | ADDED skills/gemini-relay/SKILL.md |
| 87 | 88 | ADDED skills/gemini-relay/hooks/README.md |
| 88 | 89 | ADDED skills/gemini-relay/hooks/scuttlebot-after-agent.sh |
| 89 | 90 | ADDED skills/gemini-relay/hooks/scuttlebot-check.sh |
| 90 | 91 | ADDED skills/gemini-relay/hooks/scuttlebot-post.sh |
| 91 | 92 | ADDED skills/gemini-relay/install.md |
| 92 | 93 | ADDED skills/gemini-relay/scripts/gemini-relay.sh |
| 93 | 94 | ADDED skills/gemini-relay/scripts/install-gemini-relay.sh |
| 94 | 95 | ADDED skills/scuttlebot-relay/FLEET.md |
| 95 | 96 | ADDED skills/scuttlebot-relay/hooks/README.md |
| 96 | 97 | ADDED skills/scuttlebot-relay/hooks/scuttlebot-check.sh |
| 97 | 98 | ADDED skills/scuttlebot-relay/hooks/scuttlebot-post.sh |
| 98 | 99 | ADDED skills/scuttlebot-relay/install.md |
| 99 | 100 | ADDED skills/scuttlebot-relay/scripts/claude-relay.sh |
| 100 | 101 | ADDED skills/scuttlebot-relay/scripts/install-claude-relay.sh |
| 101 | 102 | ADDED tests/smoke/test-installers.sh |
| --- mkdocs.yml | |
| +++ mkdocs.yml | |
| @@ -72,10 +72,11 @@ | |
| 72 | - Installation: getting-started/installation.md |
| 73 | - Quick Start: getting-started/quickstart.md |
| 74 | - Configuration: getting-started/configuration.md |
| 75 | - Guide: |
| 76 | - Agent Registration: guide/agent-registration.md |
| 77 | - Channel Topology: guide/topology.md |
| 78 | - Built-in Bots: guide/bots.md |
| 79 | - Discovery: guide/discovery.md |
| 80 | - Deployment: guide/deployment.md |
| 81 | - Architecture: |
| 82 | |
| 83 | DDED pkg/ircagent/ircagent.go |
| 84 | DDED pkg/ircagent/ircagent_test.go |
| 85 | DDED skills/gemini-relay/FLEET.md |
| 86 | DDED skills/gemini-relay/SKILL.md |
| 87 | DDED skills/gemini-relay/hooks/README.md |
| 88 | DDED skills/gemini-relay/hooks/scuttlebot-after-agent.sh |
| 89 | DDED skills/gemini-relay/hooks/scuttlebot-check.sh |
| 90 | DDED skills/gemini-relay/hooks/scuttlebot-post.sh |
| 91 | DDED skills/gemini-relay/install.md |
| 92 | DDED skills/gemini-relay/scripts/gemini-relay.sh |
| 93 | DDED skills/gemini-relay/scripts/install-gemini-relay.sh |
| 94 | DDED skills/scuttlebot-relay/FLEET.md |
| 95 | DDED skills/scuttlebot-relay/hooks/README.md |
| 96 | DDED skills/scuttlebot-relay/hooks/scuttlebot-check.sh |
| 97 | DDED skills/scuttlebot-relay/hooks/scuttlebot-post.sh |
| 98 | DDED skills/scuttlebot-relay/install.md |
| 99 | DDED skills/scuttlebot-relay/scripts/claude-relay.sh |
| 100 | DDED skills/scuttlebot-relay/scripts/install-claude-relay.sh |
| 101 | DDED tests/smoke/test-installers.sh |
| --- mkdocs.yml | |
| +++ mkdocs.yml | |
| @@ -72,10 +72,11 @@ | |
| 72 | - Installation: getting-started/installation.md |
| 73 | - Quick Start: getting-started/quickstart.md |
| 74 | - Configuration: getting-started/configuration.md |
| 75 | - Guide: |
| 76 | - Agent Registration: guide/agent-registration.md |
| 77 | - Fleet Management: guide/fleet-management.md |
| 78 | - Channel Topology: guide/topology.md |
| 79 | - Built-in Bots: guide/bots.md |
| 80 | - Discovery: guide/discovery.md |
| 81 | - Deployment: guide/deployment.md |
| 82 | - Architecture: |
| 83 | |
| 84 | DDED pkg/ircagent/ircagent.go |
| 85 | DDED pkg/ircagent/ircagent_test.go |
| 86 | DDED skills/gemini-relay/FLEET.md |
| 87 | DDED skills/gemini-relay/SKILL.md |
| 88 | DDED skills/gemini-relay/hooks/README.md |
| 89 | DDED skills/gemini-relay/hooks/scuttlebot-after-agent.sh |
| 90 | DDED skills/gemini-relay/hooks/scuttlebot-check.sh |
| 91 | DDED skills/gemini-relay/hooks/scuttlebot-post.sh |
| 92 | DDED skills/gemini-relay/install.md |
| 93 | DDED skills/gemini-relay/scripts/gemini-relay.sh |
| 94 | DDED skills/gemini-relay/scripts/install-gemini-relay.sh |
| 95 | DDED skills/scuttlebot-relay/FLEET.md |
| 96 | DDED skills/scuttlebot-relay/hooks/README.md |
| 97 | DDED skills/scuttlebot-relay/hooks/scuttlebot-check.sh |
| 98 | DDED skills/scuttlebot-relay/hooks/scuttlebot-post.sh |
| 99 | DDED skills/scuttlebot-relay/install.md |
| 100 | DDED skills/scuttlebot-relay/scripts/claude-relay.sh |
| 101 | DDED skills/scuttlebot-relay/scripts/install-claude-relay.sh |
| 102 | DDED tests/smoke/test-installers.sh |
+17
| --- a/pkg/ircagent/ircagent.go | ||
| +++ b/pkg/ircagent/ircagent.go | ||
| @@ -0,0 +1,17 @@ | ||
| 1 | +package ircagent | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "context" | |
| 6 | + "encoding/json" | |
| 7 | + "fmt" | |
| 8 | + "io" | |
| 9 | + "log/slog" | |
| 10 | + "net" | |
| 11 | + "net/http" | |
| 12 | + "strconv" | |
| 13 | + "strings" | |
| 14 | + "sync" | |
| 15 | + "time" | |
| 16 | + | |
| 17 | + "github.com/conflicthq/scuttlebot/internal/llm" |
| --- a/pkg/ircagent/ircagent.go | |
| +++ b/pkg/ircagent/ircagent.go | |
| @@ -0,0 +1,17 @@ | |
| --- a/pkg/ircagent/ircagent.go | |
| +++ b/pkg/ircagent/ircagent.go | |
| @@ -0,0 +1,17 @@ | |
| 1 | package ircagent |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "log/slog" |
| 10 | "net" |
| 11 | "net/http" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | "sync" |
| 15 | "time" |
| 16 | |
| 17 | "github.com/conflicthq/scuttlebot/internal/llm" |
| --- a/pkg/ircagent/ircagent_test.go | ||
| +++ b/pkg/ircagent/ircagent_test.go | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +package ircagent | |
| 2 | + | |
| 3 | +import "testing" | |
| 4 | + | |
| 5 | +func TestMentionsNick(t *testing.T) { | |
| 6 | + t.Helper() | |
| 7 | + | |
| 8 | + tests := []struct { | |
| 9 | + name string | |
| 10 | + text string | |
| 11 | + nick string | |
| 12 | + want bool | |
| 13 | + }{ | |
| 14 | + {name: "simple mention", text: "codex: hello", nick: "codex", want: true}, | |
| 15 | + {name: "mention in sentence", text: "hey codex can you help", nick: "codex", want: true}, | |
| 16 | + {name: "path does not trigger", text: "look at .claude/hooks/settings.json", nick: "claude", want: false}, | |
| 17 | + {name: "windows path does not trigger", text: `check C:\Users\me\.codex\hooks`, nick: "codex", want: false}, | |
| 18 | + {name: "substring does not trigger", text: "codexagent is a process", nick: "codex", want: false}, | |
| 19 | + {name: "hyphen boundary does not trigger", text: "claude-scuttlebot-a1b2c3d4 posted status", nick: "claude", want: false}, | |
| 20 | + } | |
| 21 | + | |
| 22 | + for _, |
| --- a/pkg/ircagent/ircagent_test.go | |
| +++ b/pkg/ircagent/ircagent_test.go | |
| @@ -0,0 +1,22 @@ | |
| --- a/pkg/ircagent/ircagent_test.go | |
| +++ b/pkg/ircagent/ircagent_test.go | |
| @@ -0,0 +1,22 @@ | |
| 1 | package ircagent |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | func TestMentionsNick(t *testing.T) { |
| 6 | t.Helper() |
| 7 | |
| 8 | tests := []struct { |
| 9 | name string |
| 10 | text string |
| 11 | nick string |
| 12 | want bool |
| 13 | }{ |
| 14 | {name: "simple mention", text: "codex: hello", nick: "codex", want: true}, |
| 15 | {name: "mention in sentence", text: "hey codex can you help", nick: "codex", want: true}, |
| 16 | {name: "path does not trigger", text: "look at .claude/hooks/settings.json", nick: "claude", want: false}, |
| 17 | {name: "windows path does not trigger", text: `check C:\Users\me\.codex\hooks`, nick: "codex", want: false}, |
| 18 | {name: "substring does not trigger", text: "codexagent is a process", nick: "codex", want: false}, |
| 19 | {name: "hyphen boundary does not trigger", text: "claude-scuttlebot-a1b2c3d4 posted status", nick: "claude", want: false}, |
| 20 | } |
| 21 | |
| 22 | for _, |
+105
| --- a/skills/gemini-relay/FLEET.md | ||
| +++ b/skills/gemini-relay/FLEET.md | ||
| @@ -0,0 +1,105 @@ | ||
| 1 | +# Gemini Relay Fleet Launch | |
| 2 | + | |
| 3 | +This is the rollout guide for making local Gemini CLI terminal sessions IRC-visible and | |
| 4 | +operator-addressable through scuttlebot. | |
| 5 | + | |
| 6 | +ini Relay Fleet Launch | |
| 7 | + | |
| 8 | +This is the rollout guide for making local Gemini CLI terminal sessions IRC-visible and | |
| 9 | +operator-addressable through scuttlebot. | |
| 10 | + | |
| 11 | +Gemini and Codex are the canonical terminal-broker reference implementations in | |
| 12 | +this repo. The normative path and convention contract lives in | |
| 13 | +[`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttl# Gemini Rel | |
| 14 | +- shared runtimeEADME.md) | |
| 15 | +- canonical relay contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) | |
| 16 | + | |
| 17 | +Installed files under `~/.gemini/`, `~/.local/bin/`, and `~/.config/` are generated | |
| 18 | +copies. Point other engineers and agents at the repo docs and installer, not at one | |
| 19 | +person's home directory. | |
| 20 | + | |
| 21 | +Runtime prerequisites: | |
| 22 | +- `gemini` | |
| 23 | +his gives you | |
| 24 | + | |
| 25 | +For each local Gemini session launched through `gemini-relay`: | |
| 26 | +- a stable nick: `gemini-{repo}-{session}` | |
| 27 | +- immediate `online` post when the session starts | |
| 28 | +- real-time tool activity posts via hooks | |
| 29 | +- final tlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) | |
| 30 | +- reply hook: [`hooks/scuttlebot-after-agent.sh`](hooks/scuttlebot-after-agent.sh) | |
| 31 | +- runtime docs: [`install.md`](install.md), [`hooks/README.md`](hooks/README.md) | |
| 32 | +- canonical relay contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) | |
| 33 | + | |
| 34 | +Installed files under `~/.gemini/`, `~/.local/bin/`, and `~/.config/` are generated | |
| 35 | +copies. Point other engineers and agents at the repo docs and installer, not at one | |
| 36 | +person's home directory. | |
| 37 | + | |
| 38 | +Runtime prerequisites: | |
| 39 | +- `gemini` | |
| 40 | +- `go` | |
| 41 | +- `curl` | |
| 42 | +- `jq` | |
| 43 | + | |
| 44 | +## Canonical pattern | |
| 45 | + | |
| 46 | +Future terminal runtimes should copy this shape: | |
| 47 | +- broker entrypoint in `cmd/{runtime}-relay/main.go` | |
| 48 | +- tracked installer in `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh` | |
| 49 | +- rollout guide in `skills/{runtime}-relay/FLEET.md` | |
| 50 | +- install primer in `skills/{runtime}-relay/install.md` | |
| 51 | +- runtime hook docs in `skills/{runtime}-relay/hooks/README.md` | |
| 52 | +- shared transport and presence logic in `pkg/sessionrelay/` | |
| 53 | + | |
| 54 | +Ownership conventiointernalroker owns `online` / `offline` | |
| 55 | +- the broker owns addressed operator message internal broker owns transport selection and presence semantics in `http` and `irc` modes | |
| 56 | +- hooks remain the pre-action fallback, tool summary path, and final-reply mirror path where the runtime does not expose a better broker-native reply stream | |
| 57 | + | |
| 58 | +## What this gives you | |
| 59 | + | |
| 60 | +For each local Gemini session launched through `gemini-relay`: | |
| 61 | +- a stable nick: `gemini-{repo}-{session}` | |
| 62 | +- immediate `online` post when the session starts | |
| 63 | +- real-time tool activity posts via hooks | |
| 64 | +- final assistant replies mirrored via `AfterAgent` | |
| 65 | +- continuous addressed IRC input injection into the live terminal session | |
| 66 | +- explicit pre-tool fallback interrupts before the next action | |
| 67 | +- `offline` post on exit | |
| 68 | + | |
| 69 | +Transport choice: | |
| 70 | +- `SCUTTLEBOT_TRANSPORT=http` keeps the bridge/API path and now uses presence heartbeats | |
| 71 | +- `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence | |
| 72 | + | |
| 73 | +This is the production control pathglengoolie: gemini-scuttlebot-a1b2c3d4 glengoolie: gemini-scuttlebot-a1b2c3d4 wrong file, inspect policies.go first | |
| 74 | +``` | |
| 75 | + | |
| 76 | +Ambient channel chat does not block the loop. Only explicit nick mentions do. | |
| 77 | + | |
| 78 | +## When IRC/scuttlebot is down | |
| 79 | + | |
| 80 | +Disable without uninstalling: | |
| 81 | + | |
| 82 | +```bash | |
| 83 | +SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/gemini-relay | |
| 84 | +``` | |
| 85 | + | |
| 86 | +Or persist the disabled state in the shared env file: | |
| 87 | + | |
| 88 | +```bash | |
| 89 | +bash skills/gemini-relay/scripts/install-gemini-relay.sh --disabled | |
| 90 | +``` | |
| 91 | + | |
| 92 | +The hooks and broker soft-fail if the HTTP API is unavailable. Gemini still runs; | |
| 93 | +you just lose the IRC coordination layer until the server comes back. | |
| 94 | + | |
| 95 | +## Adding more runtimes | |
| 96 | + | |
| 97 | +Do not fork the protocol. Reuse the same control contract: | |
| 98 | +- post activity out after each action | |
| 99 | +- accept addressed operator instructions back in before the next action | |
| 100 | +- use stable, human-addressable session nicks | |
| 101 | +- keep the repo as the source of truth | |
| 102 | + | |
| 103 | +The shared authoring contract lives in | |
| 104 | +[`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md). | |
| 105 | +- `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` tells `scuttlebot-post.sh` to stay quiet so broker-launched sessions do not duplicate activity posts |
| --- a/skills/gemini-relay/FLEET.md | |
| +++ b/skills/gemini-relay/FLEET.md | |
| @@ -0,0 +1,105 @@ | |
| --- a/skills/gemini-relay/FLEET.md | |
| +++ b/skills/gemini-relay/FLEET.md | |
| @@ -0,0 +1,105 @@ | |
| 1 | # Gemini Relay Fleet Launch |
| 2 | |
| 3 | This is the rollout guide for making local Gemini CLI terminal sessions IRC-visible and |
| 4 | operator-addressable through scuttlebot. |
| 5 | |
| 6 | ini Relay Fleet Launch |
| 7 | |
| 8 | This is the rollout guide for making local Gemini CLI terminal sessions IRC-visible and |
| 9 | operator-addressable through scuttlebot. |
| 10 | |
| 11 | Gemini and Codex are the canonical terminal-broker reference implementations in |
| 12 | this repo. The normative path and convention contract lives in |
| 13 | [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttl# Gemini Rel |
| 14 | - shared runtimeEADME.md) |
| 15 | - canonical relay contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| 16 | |
| 17 | Installed files under `~/.gemini/`, `~/.local/bin/`, and `~/.config/` are generated |
| 18 | copies. Point other engineers and agents at the repo docs and installer, not at one |
| 19 | person's home directory. |
| 20 | |
| 21 | Runtime prerequisites: |
| 22 | - `gemini` |
| 23 | his gives you |
| 24 | |
| 25 | For each local Gemini session launched through `gemini-relay`: |
| 26 | - a stable nick: `gemini-{repo}-{session}` |
| 27 | - immediate `online` post when the session starts |
| 28 | - real-time tool activity posts via hooks |
| 29 | - final tlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) |
| 30 | - reply hook: [`hooks/scuttlebot-after-agent.sh`](hooks/scuttlebot-after-agent.sh) |
| 31 | - runtime docs: [`install.md`](install.md), [`hooks/README.md`](hooks/README.md) |
| 32 | - canonical relay contract: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| 33 | |
| 34 | Installed files under `~/.gemini/`, `~/.local/bin/`, and `~/.config/` are generated |
| 35 | copies. Point other engineers and agents at the repo docs and installer, not at one |
| 36 | person's home directory. |
| 37 | |
| 38 | Runtime prerequisites: |
| 39 | - `gemini` |
| 40 | - `go` |
| 41 | - `curl` |
| 42 | - `jq` |
| 43 | |
| 44 | ## Canonical pattern |
| 45 | |
| 46 | Future terminal runtimes should copy this shape: |
| 47 | - broker entrypoint in `cmd/{runtime}-relay/main.go` |
| 48 | - tracked installer in `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh` |
| 49 | - rollout guide in `skills/{runtime}-relay/FLEET.md` |
| 50 | - install primer in `skills/{runtime}-relay/install.md` |
| 51 | - runtime hook docs in `skills/{runtime}-relay/hooks/README.md` |
| 52 | - shared transport and presence logic in `pkg/sessionrelay/` |
| 53 | |
| 54 | Ownership conventiointernalroker owns `online` / `offline` |
| 55 | - the broker owns addressed operator message internal broker owns transport selection and presence semantics in `http` and `irc` modes |
| 56 | - hooks remain the pre-action fallback, tool summary path, and final-reply mirror path where the runtime does not expose a better broker-native reply stream |
| 57 | |
| 58 | ## What this gives you |
| 59 | |
| 60 | For each local Gemini session launched through `gemini-relay`: |
| 61 | - a stable nick: `gemini-{repo}-{session}` |
| 62 | - immediate `online` post when the session starts |
| 63 | - real-time tool activity posts via hooks |
| 64 | - final assistant replies mirrored via `AfterAgent` |
| 65 | - continuous addressed IRC input injection into the live terminal session |
| 66 | - explicit pre-tool fallback interrupts before the next action |
| 67 | - `offline` post on exit |
| 68 | |
| 69 | Transport choice: |
| 70 | - `SCUTTLEBOT_TRANSPORT=http` keeps the bridge/API path and now uses presence heartbeats |
| 71 | - `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence |
| 72 | |
| 73 | This is the production control pathglengoolie: gemini-scuttlebot-a1b2c3d4 glengoolie: gemini-scuttlebot-a1b2c3d4 wrong file, inspect policies.go first |
| 74 | ``` |
| 75 | |
| 76 | Ambient channel chat does not block the loop. Only explicit nick mentions do. |
| 77 | |
| 78 | ## When IRC/scuttlebot is down |
| 79 | |
| 80 | Disable without uninstalling: |
| 81 | |
| 82 | ```bash |
| 83 | SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/gemini-relay |
| 84 | ``` |
| 85 | |
| 86 | Or persist the disabled state in the shared env file: |
| 87 | |
| 88 | ```bash |
| 89 | bash skills/gemini-relay/scripts/install-gemini-relay.sh --disabled |
| 90 | ``` |
| 91 | |
| 92 | The hooks and broker soft-fail if the HTTP API is unavailable. Gemini still runs; |
| 93 | you just lose the IRC coordination layer until the server comes back. |
| 94 | |
| 95 | ## Adding more runtimes |
| 96 | |
| 97 | Do not fork the protocol. Reuse the same control contract: |
| 98 | - post activity out after each action |
| 99 | - accept addressed operator instructions back in before the next action |
| 100 | - use stable, human-addressable session nicks |
| 101 | - keep the repo as the source of truth |
| 102 | |
| 103 | The shared authoring contract lives in |
| 104 | [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md). |
| 105 | - `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` tells `scuttlebot-post.sh` to stay quiet so broker-launched sessions do not duplicate activity posts |
+107
| --- a/skills/gemini-relay/SKILL.md | ||
| +++ b/skills/gemini-relay/SKILL.md | ||
| @@ -0,0 +1,107 @@ | ||
| 1 | +--- | |
| 2 | +name: gemini-relay | |
| 3 | +description: Bidirectional Gemini integration for scuttlebot. Local terminal path: run the compiled `gemini-relay` broker with shared `http|irc` transports. IRC-resident bot path: run `gemini-agent`. Use when wiring Gemini-based agents or live Gemini CLI sessions into scuttlebot locally or over the internet. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# Gemini Relay | |
| 7 | + | |
| 8 | +There are two supported production paths: | |
| 9 | +- local Gemini terminal session: `cmd/gemini-relay` | |
| 10 | +- IRC-resident autonomous agent: `cmd/gemini-agent` | |
| 11 | + | |
| 12 | +`cmd/gemini-relay` is the broker path for a live Gemini terminal. It keeps a stable | |
| 13 | +session nick, posts `online`/`offline`, injects addressed IRC operator messages | |
| 14 | +into the running terminal session, and uses the shared `pkg/sessionrelay` | |
| 15 | +connector with `http` and `irc` transports. | |
| 16 | + | |
| 17 | +Gemini CLI itself supports a broad native hook surface, including | |
| 18 | +`SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeToolSelection`, | |
| 19 | +`BeforeTool`, `AfterTool`, `BeforeModel`, `AfterModel`, `Notification`, and | |
| 20 | +`PreCompress`. In this repo, the relay integration intentionally uses the broker | |
| 21 | +for session-lifetime presence and live input injection, while Gemini hooks remain | |
| 22 | +the pre-tool fallback plus outbound tool/reply path. | |
| 23 | + | |
| 24 | +`cmd/gemini-agent` is the always-on IRC client path. It is a thin wrapper over | |
| 25 | +the shared `pkg/ircagent` runtime with `gemini` defaults. It logs into Ergo with | |
| 26 | +SASL, joins channels, responds to mentions/DMs, and uses `/v1/llm/complete` with | |
| 27 | +backend `gemini`. | |
| 28 | + | |
| 29 | +## Setup | |
| 30 | +- Export gateway env vars: | |
| 31 | + - `SCUTTLEBOT_URL` e.g. `http://localhost:8080` | |
| 32 | + - `SCUTTLEBOT_TOKEN` bearer token | |
| 33 | +- `SCUTTLEBOT_CHANNEL` channel slug, e.g. `general` | |
| 34 | +- Ensure the daemon has a `gemini` backend configured. | |
| 35 | +- Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. | |
| 36 | + | |
| 37 | +## Preferred For Local Gemini CLI: gemini-relay | |
| 38 | +Tracked files: | |
| 39 | +- broker: `cmd/gemini-relay/main.go` | |
| 40 | +- shared transport layer: `pkg/sessionrelay/` | |
| 41 | +- installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` | |
| 42 | +- launcher: `skills/gemini-relay/scripts/gemini-relay.sh` | |
| 43 | +- hooks: `skills/gemini-relay/hooks/` | |
| 44 | +ssionrelay` | |
| 45 | +connector with `http` and `irc` transports. | |
| 46 | + | |
| 47 | +Gemini and Codex are the canonical terminal-broker reference implementations in | |
| 48 | +this repo. The shared path and convention contract lives in | |
| 49 | +`skills/scuttlebot-relay/ADDING_AGENTS.md`. | |
| 50 | + | |
| 51 | +Gemini CLI itself supports a broad native hook surface, including | |
| 52 | +`SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeToolSelection`, | |
| 53 | +`BeforeTool`, `AfterTool`, `BeforeModel`, `AfterModel`, `Notification`, and | |
| 54 | +`PreCompress`. In this repo, the relay integration intentionally uses the broker | |
| 55 | +for session-lifetime presence and live input injection, while Gemini hooks remain | |
| 56 | +the pre-tool fallback plus outbound tool/reply path. | |
| 57 | + | |
| 58 | +`cmd/gemini-agent` is the always- - `SCUTTLEBOT_TOKEN` bearer token | |
| 59 | +- `SCUTTLEBOT_CHANNEL` channel slug, e.g. `general` | |
| 60 | +- Ensure the daemon has a `gemini` backend configured. | |
| 61 | +- Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. | |
| 62 | + | |
| 63 | +## Preferred For Local Gemini CLI: gemini-relay | |
| 64 | +Tracked files: | |
| 65 | +- broker: `cmd/gemini-relay/main.go` | |
| 66 | +- shared transport layer: `pkg/sessionrelay/` | |
| 67 | +- installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` | |
| 68 | +- launcher: `skills/gemini-relay/scripts/gemini-relay.sh` | |
| 69 | +- hooks: `skills/gemini-relay/hooks/` | |
| 70 | +- fleet rollout doc: `skills/gemini-relay/FLEET.md` | |
| 71 | +- canonical relay contract: `skills/scuttlebot-relay/ADDING_AGENTS.md` | |
| 72 | + | |
| 73 | +Install: | |
| 74 | +```bash | |
| 75 | +bash skills/gemini-relay/scripts/install-gemini-relay.sh \ | |
| 76 | + --url http://localhost:8080 \ | |
| 77 | + --token "$(./run.sh token)" \ | |
| 78 | + --channel general | |
| 79 | +``` | |
| 80 | + | |
| 81 | +Launch: | |
| 82 | +```bash | |
| 83 | +~/.local/bin/gemini-relay | |
| 84 | +``` | |
| 85 | + | |
| 86 | +Behavior: | |
| 87 | +- posts `online` immediately | |
| 88 | +- keeps a stable nick `gemini-{basename}-{session}` | |
| 89 | +- continuously injects addressed IRC instructions into the live Gemini session | |
| 90 | +- uses bracketed paste for injected operator text so Gemini treats `!`, `??`, and similar input literally | |
| 91 | +- posts `offline` on exit | |
| 92 | +- supports `SCUTTLEBOT_TRANSPORT=http` and `SCUTTLEBOT_TRANSPORT=irc` | |
| 93 | +- in `http` mode, uses silent presence heartbeats | |
| 94 | +- in `irc` mode, connects the session nick directly to Ergo and can auto-register ephemeral session nicks | |
| 95 | + | |
| 96 | +Canonical pattern summary: | |
| 97 | +- broker entrypoint: `cmd/gemini-relay/main.go` | |
| 98 | +- tracked installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` | |
| 99 | +- runtime docs: `skills/gemini-relay/install.md` and `skills/gemini-relay/FLEET.md` | |
| 100 | +- hooks: `skills/gemini-relay/hooks/` | |
| 101 | +- shared transport: `pkg/sessionrelay/` | |
| 102 | + | |
| 103 | +Current boundary: | |
| 104 | +- Gemini has hook parity for pre-action blocking, post-tool activity hooks, and final reply hooks | |
| 105 | +- Gemini does not yet have Codex-style broker-owned activity mirroring from a richer session log | |
| 106 | +- tool activity is emitted by `skills/gemini-relay/hooks/scuttlebot-post.sh` | |
| 107 | +- final assistant replies are emitted by `skills/gemini-relay/hooks/scuttlebo |
| --- a/skills/gemini-relay/SKILL.md | |
| +++ b/skills/gemini-relay/SKILL.md | |
| @@ -0,0 +1,107 @@ | |
| --- a/skills/gemini-relay/SKILL.md | |
| +++ b/skills/gemini-relay/SKILL.md | |
| @@ -0,0 +1,107 @@ | |
| 1 | --- |
| 2 | name: gemini-relay |
| 3 | description: Bidirectional Gemini integration for scuttlebot. Local terminal path: run the compiled `gemini-relay` broker with shared `http|irc` transports. IRC-resident bot path: run `gemini-agent`. Use when wiring Gemini-based agents or live Gemini CLI sessions into scuttlebot locally or over the internet. |
| 4 | --- |
| 5 | |
| 6 | # Gemini Relay |
| 7 | |
| 8 | There are two supported production paths: |
| 9 | - local Gemini terminal session: `cmd/gemini-relay` |
| 10 | - IRC-resident autonomous agent: `cmd/gemini-agent` |
| 11 | |
| 12 | `cmd/gemini-relay` is the broker path for a live Gemini terminal. It keeps a stable |
| 13 | session nick, posts `online`/`offline`, injects addressed IRC operator messages |
| 14 | into the running terminal session, and uses the shared `pkg/sessionrelay` |
| 15 | connector with `http` and `irc` transports. |
| 16 | |
| 17 | Gemini CLI itself supports a broad native hook surface, including |
| 18 | `SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeToolSelection`, |
| 19 | `BeforeTool`, `AfterTool`, `BeforeModel`, `AfterModel`, `Notification`, and |
| 20 | `PreCompress`. In this repo, the relay integration intentionally uses the broker |
| 21 | for session-lifetime presence and live input injection, while Gemini hooks remain |
| 22 | the pre-tool fallback plus outbound tool/reply path. |
| 23 | |
| 24 | `cmd/gemini-agent` is the always-on IRC client path. It is a thin wrapper over |
| 25 | the shared `pkg/ircagent` runtime with `gemini` defaults. It logs into Ergo with |
| 26 | SASL, joins channels, responds to mentions/DMs, and uses `/v1/llm/complete` with |
| 27 | backend `gemini`. |
| 28 | |
| 29 | ## Setup |
| 30 | - Export gateway env vars: |
| 31 | - `SCUTTLEBOT_URL` e.g. `http://localhost:8080` |
| 32 | - `SCUTTLEBOT_TOKEN` bearer token |
| 33 | - `SCUTTLEBOT_CHANNEL` channel slug, e.g. `general` |
| 34 | - Ensure the daemon has a `gemini` backend configured. |
| 35 | - Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. |
| 36 | |
| 37 | ## Preferred For Local Gemini CLI: gemini-relay |
| 38 | Tracked files: |
| 39 | - broker: `cmd/gemini-relay/main.go` |
| 40 | - shared transport layer: `pkg/sessionrelay/` |
| 41 | - installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` |
| 42 | - launcher: `skills/gemini-relay/scripts/gemini-relay.sh` |
| 43 | - hooks: `skills/gemini-relay/hooks/` |
| 44 | ssionrelay` |
| 45 | connector with `http` and `irc` transports. |
| 46 | |
| 47 | Gemini and Codex are the canonical terminal-broker reference implementations in |
| 48 | this repo. The shared path and convention contract lives in |
| 49 | `skills/scuttlebot-relay/ADDING_AGENTS.md`. |
| 50 | |
| 51 | Gemini CLI itself supports a broad native hook surface, including |
| 52 | `SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeToolSelection`, |
| 53 | `BeforeTool`, `AfterTool`, `BeforeModel`, `AfterModel`, `Notification`, and |
| 54 | `PreCompress`. In this repo, the relay integration intentionally uses the broker |
| 55 | for session-lifetime presence and live input injection, while Gemini hooks remain |
| 56 | the pre-tool fallback plus outbound tool/reply path. |
| 57 | |
| 58 | `cmd/gemini-agent` is the always- - `SCUTTLEBOT_TOKEN` bearer token |
| 59 | - `SCUTTLEBOT_CHANNEL` channel slug, e.g. `general` |
| 60 | - Ensure the daemon has a `gemini` backend configured. |
| 61 | - Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`. |
| 62 | |
| 63 | ## Preferred For Local Gemini CLI: gemini-relay |
| 64 | Tracked files: |
| 65 | - broker: `cmd/gemini-relay/main.go` |
| 66 | - shared transport layer: `pkg/sessionrelay/` |
| 67 | - installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` |
| 68 | - launcher: `skills/gemini-relay/scripts/gemini-relay.sh` |
| 69 | - hooks: `skills/gemini-relay/hooks/` |
| 70 | - fleet rollout doc: `skills/gemini-relay/FLEET.md` |
| 71 | - canonical relay contract: `skills/scuttlebot-relay/ADDING_AGENTS.md` |
| 72 | |
| 73 | Install: |
| 74 | ```bash |
| 75 | bash skills/gemini-relay/scripts/install-gemini-relay.sh \ |
| 76 | --url http://localhost:8080 \ |
| 77 | --token "$(./run.sh token)" \ |
| 78 | --channel general |
| 79 | ``` |
| 80 | |
| 81 | Launch: |
| 82 | ```bash |
| 83 | ~/.local/bin/gemini-relay |
| 84 | ``` |
| 85 | |
| 86 | Behavior: |
| 87 | - posts `online` immediately |
| 88 | - keeps a stable nick `gemini-{basename}-{session}` |
| 89 | - continuously injects addressed IRC instructions into the live Gemini session |
| 90 | - uses bracketed paste for injected operator text so Gemini treats `!`, `??`, and similar input literally |
| 91 | - posts `offline` on exit |
| 92 | - supports `SCUTTLEBOT_TRANSPORT=http` and `SCUTTLEBOT_TRANSPORT=irc` |
| 93 | - in `http` mode, uses silent presence heartbeats |
| 94 | - in `irc` mode, connects the session nick directly to Ergo and can auto-register ephemeral session nicks |
| 95 | |
| 96 | Canonical pattern summary: |
| 97 | - broker entrypoint: `cmd/gemini-relay/main.go` |
| 98 | - tracked installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` |
| 99 | - runtime docs: `skills/gemini-relay/install.md` and `skills/gemini-relay/FLEET.md` |
| 100 | - hooks: `skills/gemini-relay/hooks/` |
| 101 | - shared transport: `pkg/sessionrelay/` |
| 102 | |
| 103 | Current boundary: |
| 104 | - Gemini has hook parity for pre-action blocking, post-tool activity hooks, and final reply hooks |
| 105 | - Gemini does not yet have Codex-style broker-owned activity mirroring from a richer session log |
| 106 | - tool activity is emitted by `skills/gemini-relay/hooks/scuttlebot-post.sh` |
| 107 | - final assistant replies are emitted by `skills/gemini-relay/hooks/scuttlebo |
| --- a/skills/gemini-relay/hooks/README.md | ||
| +++ b/skills/gemini-relay/hooks/README.md | ||
| @@ -0,0 +1,205 @@ | ||
| 1 | +# Gemini Hook Primer | |
| 2 | + | |
| 3 | +These hooks are the activity and pre-tool fallback path for a live Gemini tool loop. | |
| 4 | +Continuous IRC-to-terminal input plus `online` / `offline` presence are handled by | |
| 5 | +the compiled `cmd/gemini-relay` broker, which now sits on the shared | |
| 6 | +`pkg/sessionrelay` connector package. | |
| 7 | + | |
| 8 | +Upstream Gemini CLI has a richer native hook surface than just tool hooks: | |
| 9 | +`SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, | |
| 10 | +`AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, | |
| 11 | +and `Notification`. In this repo we intentionally wire `BeforeTool`, | |
| 12 | +`AfterTool`, and `AfterAgent` for the relay hooks, while the broker owns | |
| 13 | +session presence and continuous live operator message injection. | |
| 14 | + | |
| 15 | +If you need to add another runtime later, use | |
| 16 | +[`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) | |
| 17 | +as the shared authoring contract. | |
| 18 | + | |
| 19 | +Files in this directory: | |
| 20 | +- `scuttlebot-post.sh` | |
| 21 | +- `scuttlebot-check.sh` | |
| 22 | +- `scuttlebot-after-agent.sh` | |
| 23 | + | |
| 24 | +Related launcher: | |
| 25 | +- `../../../cmd/gemini-relay/main.go` | |
| 26 | +- `../scripts/gemini-relay.sh` | |
| 27 | +- `../scripts/install-gemini-relay.sh` | |
| 28 | + | |
| 29 | +Source of truth: | |
| 30 | +- the repo copies in this directory and `../scripts/` | |
| 31 | +- not the installed copies under `~/.gemini/` or `~/.local/bin/` | |
| 32 | + | |
| 33 | +## What they do | |
| 34 | + | |
| 35 | +`scuttlebot-post.sh` | |
| 36 | +- runs on Gemini CLI `AfterTool` | |
| 37 | +- posts a one-line activity summary into a scuttlebot channel | |
| 38 | +- remains the primary Gemini activity path today, even when launched through `gemini-relay` | |
| 39 | +- returns valid JSON success output (`{}`), which Gemini CLI expects for hook success | |
| 40 | + | |
| 41 | +`scuttlebot-check.sh` | |
| 42 | +- runs on Gemini CLI `BeforeTool`es bots and agent status nicks | |
| 43 | +- blocks only when a human explicitly mentions this session nick | |
| 44 | +- returns valid JSON success output (`{}`) when no block is needed | |
| 45 | + | |
| 46 | +`scuttlebot-after-agent.sh` | |
| 47 | +- runs on Gemini CLI `AfterAgent` | |
| 48 | +- posts the final assistant reply for each completed turn into scuttlebot | |
| 49 | +- normalizes whitespace and splits long replies into IRC-safe lines | |
| 50 | +- returns valid JSON success output (`{}`), which Gemini CLI expects for hook success | |
| 51 | + | |
| 52 | +With the rent control loop: | |
| 53 | +1. `cmd/gemini-relay` posts `online`. | |
| 54 | +2. The operator mentions the Gemini session nick. | |
| 55 | +3. `cmd/gemini-relay` injects that IRC message into the live terminal session immediately using bracketed paste,HANNELS=general | |
| 56 | +SCUTTLEBOT_TRANSPORT=http | |
| 57 | +SCUTTLEBOT_IRC_ADDR=127.0.0.1:6667 | |
| 58 | +SCUTTLEBOT_HOOKS_ENABLED=1 | |
| 59 | +SCUTTLEBOT_INTERRUPT_ON_MESSAGE=1 | |
| 60 | +SCUTTLEBOT_POLL_INTERVAL=2s | |
| 61 | +SCUTTLEBOT_PRESENCE_HEARTBEAT=60s | |
| 62 | +EOF2 | |
| 63 | +``` | |
| 64 | + | |
| 65 | +Leave `SCUTTLEBOT_IRC_PASS` unset for the default broker convention so IRC mode | |
| 66 | +auto-registers ephemeral session nicks. Use `--irc-pass <passphrase>` only when | |
| 67 | +you intentionally want a fixed identity. | |
| 68 | + | |
| 69 | +Disable the hooks entirely: | |
| 70 | + | |
| 71 | +```bash | |
| 72 | +export SCUTTLEBOT_HOOKS_ENABLED=0 | |
| 73 | +``` | |
| 74 | + | |
| 75 | +## Hook config | |
| 76 | + | |
| 77 | +Preferred path: run the tracked installer and let it wire the files up for you. | |
| 78 | + | |
| 79 | +```bash | |
| 80 | +bash skills/gemini-relay/scripts/install-gemini-relay.sh \ | |
| 81 | + --url http://localhost:8080 \ | |
| 82 | + --token "$(./run.sh to | |
| 83 | +``` | |
| 84 | + | |
| 85 | +Manual path: | |
| 86 | + | |
| 87 | +Install the scripts: | |
| 88 | + | |
| 89 | +```bash | |
| 90 | +mkdir -p ~/.gemini/hooks | |
| 91 | +cp skills/gemini-relay/hooks/scuttlebot-post.sh ~/.gemini/hooks/ | |
| 92 | +cp skills/gemini-relay/hooks/scuttlebot-check.sh ~/.gemini/hooks/ | |
| 93 | +cp skills/gemini-relay/hooks/scuttlebot-after-agent.sh ~/.gemini/hooks/ | |
| 94 | +chmod +x ~/.gemini/hooks/scuttlebot-post.sh ~/.gemini/hooks/scuttlebot-check.sh ~/.gemini/hooks/scuttlebot-after-agent.sh | |
| 95 | +``` | |
| 96 | + | |
| 97 | +Configure Gemini hooks in `~/.gemini/settings.json`: | |
| 98 | + | |
| 99 | +```json | |
| 100 | +{ | |
| 101 | + "hooks": { | |
| 102 | + "BeforeTool": [ | |
| 103 | + { | |
| 104 | + "matcher": ".*", | |
| 105 | + "hooks": [ | |
| 106 | + { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-check.sh" } | |
| 107 | + ] | |
| 108 | + } | |
| 109 | + ], | |
| 110 | + "AfterTool": [ | |
| 111 | + { | |
| 112 | + "matcher": ".*", | |
| 113 | + "hooks": [ | |
| 114 | + { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-post.sh" } | |
| 115 | + ] | |
| 116 | + } | |
| 117 | + ], | |
| 118 | + "AfterAgent": [ | |
| 119 | + { | |
| 120 | + "matcher": "*", | |
| 121 | + "hooks": [ | |
| 122 | + { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-after-agent.sh" } | |
| 123 | + ] | |
| 124 | + } | |
| 125 | + ] | |
| 126 | + } | |
| 127 | +} | |
| 128 | +``` | |
| 129 | + | |
| 130 | +Install the compiled broker if you want startup/offline presence plus continuous | |
| 131 | +IRC input injection: | |
| 132 | + | |
| 133 | +```bash | |
| 134 | +mkdir -p ~/.local/bin | |
| 135 | +go build -o ~/.local/bin/gemini-relay ./cmd/gemini-reisfy all of the following: | |
| 136 | +- newer than the last check for this session | |
| 137 | +- not posted by this session nick | |
| 138 | +- not posted by known service bots | |
| 139 | +- not posted by `claude-*`, `codex-*`, or `gemini-*` status nicks | |
| 140 | +- explicitly mention this session nick | |
| 141 | + | |
| 142 | +Ambient channel chat must not halt a live tool loop. | |
| 143 | + | |
| 144 | +## Operational notes | |
| 145 | + | |
| 146 | +- `cmd/gemini-relay` can use either the HTTP bridge API or a real IRC socket. | |
| 147 | +- `SCUTTLEBOT_emini/hooks/scuttlebot-check.sh ~/.gemini/hooks/scuttlebot-after-agent.sh | |
| 148 | +``` | |
| 149 | + | |
| 150 | +Configure Gemini hooks in `~/.gemini/settings.json`: | |
| 151 | + | |
| 152 | +```json | |
| 153 | +{ | |
| 154 | + "hooks": { | |
| 155 | + "BeforeTool": [ | |
| 156 | + { | |
| 157 | + "matcher": ".*", | |
| 158 | + "hooks": [ | |
| 159 | + { "type": "command", "command": "$HOME/.ccess output (`{}`)hooks": [ | |
| 160 | + { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-post.sh" } | |
| 161 | + ] | |
| 162 | + } | |
| 163 | + ], | |
| 164 | + "AfterAgent": [ | |
| 165 | + { | |
| 166 | + "matcher": "*", | |
| 167 | + "hooks": [ | |
| 168 | + { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-after-agent.sh" } | |
| 169 | + ] | |
| 170 | + } | |
| 171 | + ] | |
| 172 | + } | |
| 173 | +} | |
| 174 | +``` | |
| 175 | + | |
| 176 | +Install the compiled broker if you want startup/offline presence plus continuous | |
| 177 | +IRC input injection: | |
| 178 | + | |
| 179 | +```bash | |
| 180 | +mkdir -p ~/.local/bin | |
| 181 | +go build -o ~/.local/bin/gemini-relay ./cmd/gemini-relay | |
| 182 | +chmod +x ~/.local/bin/gemini-relay | |
| 183 | +``` | |
| 184 | + | |
| 185 | +Launch with: | |
| 186 | + | |
| 187 | +```bash | |
| 188 | +~/.local/bin/gemini-relay | |
| 189 | +``` | |
| 190 | + | |
| 191 | +## Message filtering semantics | |
| 192 | + | |
| 193 | +The check hook only surfaces messages that satisfy all of the following: | |
| 194 | +- newer than the last check for this session | |
| 195 | +- not posted by this session nick | |
| 196 | +- not posted by known service bots | |
| 197 | +- not posted by `claude-*`, `codex-*`, or `gemini-*` status nicks | |
| 198 | +- explicitly mention this session nick | |
| 199 | + | |
| 200 | +Ambient channel chat must not halt a live tool loop. | |
| 201 | + | |
| 202 | +## Operational notes | |
| 203 | + | |
| 204 | +- `cmd/gemini-relay` can use either the HTTP bridge API or a real IRC socket. | |
| 205 | +- `S |
| --- a/skills/gemini-relay/hooks/README.md | |
| +++ b/skills/gemini-relay/hooks/README.md | |
| @@ -0,0 +1,205 @@ | |
| --- a/skills/gemini-relay/hooks/README.md | |
| +++ b/skills/gemini-relay/hooks/README.md | |
| @@ -0,0 +1,205 @@ | |
| 1 | # Gemini Hook Primer |
| 2 | |
| 3 | These hooks are the activity and pre-tool fallback path for a live Gemini tool loop. |
| 4 | Continuous IRC-to-terminal input plus `online` / `offline` presence are handled by |
| 5 | the compiled `cmd/gemini-relay` broker, which now sits on the shared |
| 6 | `pkg/sessionrelay` connector package. |
| 7 | |
| 8 | Upstream Gemini CLI has a richer native hook surface than just tool hooks: |
| 9 | `SessionStart`, `SessionEnd`, `BeforeAgent`, `AfterAgent`, `BeforeModel`, |
| 10 | `AfterModel`, `BeforeToolSelection`, `BeforeTool`, `AfterTool`, `PreCompress`, |
| 11 | and `Notification`. In this repo we intentionally wire `BeforeTool`, |
| 12 | `AfterTool`, and `AfterAgent` for the relay hooks, while the broker owns |
| 13 | session presence and continuous live operator message injection. |
| 14 | |
| 15 | If you need to add another runtime later, use |
| 16 | [`../../scuttlebot-relay/ADDING_AGENTS.md`](../../scuttlebot-relay/ADDING_AGENTS.md) |
| 17 | as the shared authoring contract. |
| 18 | |
| 19 | Files in this directory: |
| 20 | - `scuttlebot-post.sh` |
| 21 | - `scuttlebot-check.sh` |
| 22 | - `scuttlebot-after-agent.sh` |
| 23 | |
| 24 | Related launcher: |
| 25 | - `../../../cmd/gemini-relay/main.go` |
| 26 | - `../scripts/gemini-relay.sh` |
| 27 | - `../scripts/install-gemini-relay.sh` |
| 28 | |
| 29 | Source of truth: |
| 30 | - the repo copies in this directory and `../scripts/` |
| 31 | - not the installed copies under `~/.gemini/` or `~/.local/bin/` |
| 32 | |
| 33 | ## What they do |
| 34 | |
| 35 | `scuttlebot-post.sh` |
| 36 | - runs on Gemini CLI `AfterTool` |
| 37 | - posts a one-line activity summary into a scuttlebot channel |
| 38 | - remains the primary Gemini activity path today, even when launched through `gemini-relay` |
| 39 | - returns valid JSON success output (`{}`), which Gemini CLI expects for hook success |
| 40 | |
| 41 | `scuttlebot-check.sh` |
| 42 | - runs on Gemini CLI `BeforeTool`es bots and agent status nicks |
| 43 | - blocks only when a human explicitly mentions this session nick |
| 44 | - returns valid JSON success output (`{}`) when no block is needed |
| 45 | |
| 46 | `scuttlebot-after-agent.sh` |
| 47 | - runs on Gemini CLI `AfterAgent` |
| 48 | - posts the final assistant reply for each completed turn into scuttlebot |
| 49 | - normalizes whitespace and splits long replies into IRC-safe lines |
| 50 | - returns valid JSON success output (`{}`), which Gemini CLI expects for hook success |
| 51 | |
| 52 | With the rent control loop: |
| 53 | 1. `cmd/gemini-relay` posts `online`. |
| 54 | 2. The operator mentions the Gemini session nick. |
| 55 | 3. `cmd/gemini-relay` injects that IRC message into the live terminal session immediately using bracketed paste,HANNELS=general |
| 56 | SCUTTLEBOT_TRANSPORT=http |
| 57 | SCUTTLEBOT_IRC_ADDR=127.0.0.1:6667 |
| 58 | SCUTTLEBOT_HOOKS_ENABLED=1 |
| 59 | SCUTTLEBOT_INTERRUPT_ON_MESSAGE=1 |
| 60 | SCUTTLEBOT_POLL_INTERVAL=2s |
| 61 | SCUTTLEBOT_PRESENCE_HEARTBEAT=60s |
| 62 | EOF2 |
| 63 | ``` |
| 64 | |
| 65 | Leave `SCUTTLEBOT_IRC_PASS` unset for the default broker convention so IRC mode |
| 66 | auto-registers ephemeral session nicks. Use `--irc-pass <passphrase>` only when |
| 67 | you intentionally want a fixed identity. |
| 68 | |
| 69 | Disable the hooks entirely: |
| 70 | |
| 71 | ```bash |
| 72 | export SCUTTLEBOT_HOOKS_ENABLED=0 |
| 73 | ``` |
| 74 | |
| 75 | ## Hook config |
| 76 | |
| 77 | Preferred path: run the tracked installer and let it wire the files up for you. |
| 78 | |
| 79 | ```bash |
| 80 | bash skills/gemini-relay/scripts/install-gemini-relay.sh \ |
| 81 | --url http://localhost:8080 \ |
| 82 | --token "$(./run.sh to |
| 83 | ``` |
| 84 | |
| 85 | Manual path: |
| 86 | |
| 87 | Install the scripts: |
| 88 | |
| 89 | ```bash |
| 90 | mkdir -p ~/.gemini/hooks |
| 91 | cp skills/gemini-relay/hooks/scuttlebot-post.sh ~/.gemini/hooks/ |
| 92 | cp skills/gemini-relay/hooks/scuttlebot-check.sh ~/.gemini/hooks/ |
| 93 | cp skills/gemini-relay/hooks/scuttlebot-after-agent.sh ~/.gemini/hooks/ |
| 94 | chmod +x ~/.gemini/hooks/scuttlebot-post.sh ~/.gemini/hooks/scuttlebot-check.sh ~/.gemini/hooks/scuttlebot-after-agent.sh |
| 95 | ``` |
| 96 | |
| 97 | Configure Gemini hooks in `~/.gemini/settings.json`: |
| 98 | |
| 99 | ```json |
| 100 | { |
| 101 | "hooks": { |
| 102 | "BeforeTool": [ |
| 103 | { |
| 104 | "matcher": ".*", |
| 105 | "hooks": [ |
| 106 | { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-check.sh" } |
| 107 | ] |
| 108 | } |
| 109 | ], |
| 110 | "AfterTool": [ |
| 111 | { |
| 112 | "matcher": ".*", |
| 113 | "hooks": [ |
| 114 | { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-post.sh" } |
| 115 | ] |
| 116 | } |
| 117 | ], |
| 118 | "AfterAgent": [ |
| 119 | { |
| 120 | "matcher": "*", |
| 121 | "hooks": [ |
| 122 | { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-after-agent.sh" } |
| 123 | ] |
| 124 | } |
| 125 | ] |
| 126 | } |
| 127 | } |
| 128 | ``` |
| 129 | |
| 130 | Install the compiled broker if you want startup/offline presence plus continuous |
| 131 | IRC input injection: |
| 132 | |
| 133 | ```bash |
| 134 | mkdir -p ~/.local/bin |
| 135 | go build -o ~/.local/bin/gemini-relay ./cmd/gemini-reisfy all of the following: |
| 136 | - newer than the last check for this session |
| 137 | - not posted by this session nick |
| 138 | - not posted by known service bots |
| 139 | - not posted by `claude-*`, `codex-*`, or `gemini-*` status nicks |
| 140 | - explicitly mention this session nick |
| 141 | |
| 142 | Ambient channel chat must not halt a live tool loop. |
| 143 | |
| 144 | ## Operational notes |
| 145 | |
| 146 | - `cmd/gemini-relay` can use either the HTTP bridge API or a real IRC socket. |
| 147 | - `SCUTTLEBOT_emini/hooks/scuttlebot-check.sh ~/.gemini/hooks/scuttlebot-after-agent.sh |
| 148 | ``` |
| 149 | |
| 150 | Configure Gemini hooks in `~/.gemini/settings.json`: |
| 151 | |
| 152 | ```json |
| 153 | { |
| 154 | "hooks": { |
| 155 | "BeforeTool": [ |
| 156 | { |
| 157 | "matcher": ".*", |
| 158 | "hooks": [ |
| 159 | { "type": "command", "command": "$HOME/.ccess output (`{}`)hooks": [ |
| 160 | { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-post.sh" } |
| 161 | ] |
| 162 | } |
| 163 | ], |
| 164 | "AfterAgent": [ |
| 165 | { |
| 166 | "matcher": "*", |
| 167 | "hooks": [ |
| 168 | { "type": "command", "command": "$HOME/.gemini/hooks/scuttlebot-after-agent.sh" } |
| 169 | ] |
| 170 | } |
| 171 | ] |
| 172 | } |
| 173 | } |
| 174 | ``` |
| 175 | |
| 176 | Install the compiled broker if you want startup/offline presence plus continuous |
| 177 | IRC input injection: |
| 178 | |
| 179 | ```bash |
| 180 | mkdir -p ~/.local/bin |
| 181 | go build -o ~/.local/bin/gemini-relay ./cmd/gemini-relay |
| 182 | chmod +x ~/.local/bin/gemini-relay |
| 183 | ``` |
| 184 | |
| 185 | Launch with: |
| 186 | |
| 187 | ```bash |
| 188 | ~/.local/bin/gemini-relay |
| 189 | ``` |
| 190 | |
| 191 | ## Message filtering semantics |
| 192 | |
| 193 | The check hook only surfaces messages that satisfy all of the following: |
| 194 | - newer than the last check for this session |
| 195 | - not posted by this session nick |
| 196 | - not posted by known service bots |
| 197 | - not posted by `claude-*`, `codex-*`, or `gemini-*` status nicks |
| 198 | - explicitly mention this session nick |
| 199 | |
| 200 | Ambient channel chat must not halt a live tool loop. |
| 201 | |
| 202 | ## Operational notes |
| 203 | |
| 204 | - `cmd/gemini-relay` can use either the HTTP bridge API or a real IRC socket. |
| 205 | - `S |
| --- a/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | ||
| +++ b/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | ||
| @@ -0,0 +1,29 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# AfterAgent hook for Gemini agents. Posts final assistant replies 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_AFTER_AGENT_MAX_POSTS="${SCUTTLEBOT_AFTER_AGENT_MAX_POSTS:-6}" | |
| 19 | +SCUTTLEBOT_AFTER_AGENT_CHUNsanitize() { | |
| 20 | + local input="$1" | |
| 21 | + if [ -z "$input" ]; then | |
| 22 | + input=$(cat) | |
| 23 | + fi | |
| 24 | + printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' | |
| 25 | +} | |
| 26 | + | |
| 27 | +post_liext="$1" | |
| 28 | + local payload | |
| 29 | + [ curl -sf -X POST "$SSCUTTLEBOT_CHANNEL/messages" \ --connect-timeout 1 -H "Authorization: Beare-H "Content-Typ-d "{\"text\": $( |
| --- a/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | |
| @@ -0,0 +1,29 @@ | |
| --- a/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-after-agent.sh | |
| @@ -0,0 +1,29 @@ | |
| 1 | #!/bin/bash |
| 2 | # AfterAgent hook for Gemini agents. Posts final assistant replies 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_AFTER_AGENT_MAX_POSTS="${SCUTTLEBOT_AFTER_AGENT_MAX_POSTS:-6}" |
| 19 | SCUTTLEBOT_AFTER_AGENT_CHUNsanitize() { |
| 20 | local input="$1" |
| 21 | if [ -z "$input" ]; then |
| 22 | input=$(cat) |
| 23 | fi |
| 24 | printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' |
| 25 | } |
| 26 | |
| 27 | post_liext="$1" |
| 28 | local payload |
| 29 | [ curl -sf -X POST "$SSCUTTLEBOT_CHANNEL/messages" \ --connect-timeout 1 -H "Authorization: Beare-H "Content-Typ-d "{\"text\": $( |
| --- a/skills/gemini-relay/hooks/scuttlebot-check.sh | ||
| +++ b/skills/gemini-relay/hooks/scuttlebot-check.sh | ||
| @@ -0,0 +1,44 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# BeforeTool hook for Gemini. 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 | + local input="$1" | |
| 17 | + if [ -z "$input" ]; then | |
| 18 | + input=$(cat) | |
| 19 | + fi | |
| 20 | + printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' | |
| 21 | +} | |
| 22 | + | |
| 23 | +f '%s000' "$(date +%s)" | |
| 24 | +} | |
| 25 | + | |
| 26 | +base_name=$(basename "$(pwd)") | |
| 27 | +base_name=$(sanitize "$base_name") | |
| 28 | +session_raw="${SCUTTLEBOT_SESSION_ID:-${GEMINI_SESSION_ID:-$PPID}}" | |
| 29 | +if [ -z "$session_raw" ] || [ "$session_raw" = "0" ]; then | |
| 30 | + session_raw=$(date +%s) | |
| 31 | +fi | |
| 32 | +session_suffix=$(printf '%s' "$session_raw" | sanitize | cut -c 1-8) | |
| 33 | +default_nick="gemini-${base_name}-${session_suffix}" | |
| 34 | +SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" | |
| 35 | + | |
| 36 | +[ "$SCUTTLEBOT_HOOKS_ENABLED" | |
| 37 | + set |KS_ENABLED:-1}" | |
| 38 | + | |
| 39 | +sanitize() { | |
| 40 | + local input="$1" | |
| 41 | + if [ -z "$input" ]; then | |
| 42 | + inpucontains_mention() { | |
| 43 | + local text="$1" | |
| 44 | + [[ "$text" =~ (^|[ |
| --- a/skills/gemini-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,44 @@ | |
| --- a/skills/gemini-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,44 @@ | |
| 1 | #!/bin/bash |
| 2 | # BeforeTool hook for Gemini. 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 | local input="$1" |
| 17 | if [ -z "$input" ]; then |
| 18 | input=$(cat) |
| 19 | fi |
| 20 | printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' |
| 21 | } |
| 22 | |
| 23 | f '%s000' "$(date +%s)" |
| 24 | } |
| 25 | |
| 26 | base_name=$(basename "$(pwd)") |
| 27 | base_name=$(sanitize "$base_name") |
| 28 | session_raw="${SCUTTLEBOT_SESSION_ID:-${GEMINI_SESSION_ID:-$PPID}}" |
| 29 | if [ -z "$session_raw" ] || [ "$session_raw" = "0" ]; then |
| 30 | session_raw=$(date +%s) |
| 31 | fi |
| 32 | session_suffix=$(printf '%s' "$session_raw" | sanitize | cut -c 1-8) |
| 33 | default_nick="gemini-${base_name}-${session_suffix}" |
| 34 | SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" |
| 35 | |
| 36 | [ "$SCUTTLEBOT_HOOKS_ENABLED" |
| 37 | set |KS_ENABLED:-1}" |
| 38 | |
| 39 | sanitize() { |
| 40 | local input="$1" |
| 41 | if [ -z "$input" ]; then |
| 42 | inpucontains_mention() { |
| 43 | local text="$1" |
| 44 | [[ "$text" =~ (^|[ |
| --- a/skills/gemini-relay/hooks/scuttlebot-post.sh | ||
| +++ b/skills/gemini-relay/hooks/scuttlebot-post.sh | ||
| @@ -0,0 +1,59 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# AfterTool hook for Gemini agents. 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 | + | |
| 11 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 12 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" | |
| 13 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" | |
| 14 | +SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 15 | +SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" | |
| 16 | + | |
| 17 | +sanitize() { | |
| 18 | + local input="$1" | |
| 19 | + if [ -z "$input" ]; then | |
| 20 | + input=$(cat) | |
| 21 | + fi | |
| 22 | + printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' | |
| 23 | +} | |
| 24 | + | |
| 25 | +input=$(cat) | |
| 26 | + | |
| 27 | +tool=$(echo "$input" | jq -r '.tool_name // empty') | |
| 28 | +cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| 29 | + | |
| 30 | +if [ -z "$cwd" ]; then | |
| 31 | + cwd=$(pwd) | |
| 32 | +fi | |
| 33 | +base_name=$(sanitize "$(basename "$cwd")") | |
| 34 | +session_raw="${SCUTTLEBOT_SESSION_ID:-${GEMINI_SESSION_ID:-$PPID}}" | |
| 35 | +if [ -z "$session_raw" ] || [ "$session_raw" = "0" ]; then | |
| 36 | + session_raw=$(date +%s) | |
| 37 | +fi | |
| 38 | +session_suffix=$(printf '%s' "$session_raw" | sanitize | cut -c 1-8) | |
| 39 | +default_nick="gemini-${base_name}-${session_suffix}" | |
| 40 | +SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" | |
| 41 | + | |
| 42 | +[ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && { echo '{}'; exit 0; } | |
| 43 | +[ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && { echo '{}'; exit 0; } | |
| 44 | +[ "$SCUTTLEBO{channel#\#}" | |
| 45 | + printf '%s' "$channel" | |
| 46 | +} | |
| 47 | + | |
| 48 | +relay_channels() { | |
| 49 | + local raw="${SCUTTLEBOT_CHANNEL[ "$SCUTTLEBOT_A{channel#\#}" | |
| 50 | + printf '%s' "$channel" | |
| 51 | +} | |
| 52 | + | |
| 53 | +relay_channels() { | |
| 54 | + local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}" | |
| 55 | + local IFS=',' | |
| 56 | + local item channel seen="" | |
| 57 | + read -r -a items <<< "$raw" | |
| 58 | + for item in "${items[@]}"; do | |
| 59 | + |
| --- a/skills/gemini-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,59 @@ | |
| --- a/skills/gemini-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/gemini-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,59 @@ | |
| 1 | #!/bin/bash |
| 2 | # AfterTool hook for Gemini agents. 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 | |
| 11 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 12 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" |
| 13 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" |
| 14 | SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 15 | SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" |
| 16 | |
| 17 | sanitize() { |
| 18 | local input="$1" |
| 19 | if [ -z "$input" ]; then |
| 20 | input=$(cat) |
| 21 | fi |
| 22 | printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' |
| 23 | } |
| 24 | |
| 25 | input=$(cat) |
| 26 | |
| 27 | tool=$(echo "$input" | jq -r '.tool_name // empty') |
| 28 | cwd=$(echo "$input" | jq -r '.cwd // empty') |
| 29 | |
| 30 | if [ -z "$cwd" ]; then |
| 31 | cwd=$(pwd) |
| 32 | fi |
| 33 | base_name=$(sanitize "$(basename "$cwd")") |
| 34 | session_raw="${SCUTTLEBOT_SESSION_ID:-${GEMINI_SESSION_ID:-$PPID}}" |
| 35 | if [ -z "$session_raw" ] || [ "$session_raw" = "0" ]; then |
| 36 | session_raw=$(date +%s) |
| 37 | fi |
| 38 | session_suffix=$(printf '%s' "$session_raw" | sanitize | cut -c 1-8) |
| 39 | default_nick="gemini-${base_name}-${session_suffix}" |
| 40 | SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}" |
| 41 | |
| 42 | [ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && { echo '{}'; exit 0; } |
| 43 | [ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && { echo '{}'; exit 0; } |
| 44 | [ "$SCUTTLEBO{channel#\#}" |
| 45 | printf '%s' "$channel" |
| 46 | } |
| 47 | |
| 48 | relay_channels() { |
| 49 | local raw="${SCUTTLEBOT_CHANNEL[ "$SCUTTLEBOT_A{channel#\#}" |
| 50 | printf '%s' "$channel" |
| 51 | } |
| 52 | |
| 53 | relay_channels() { |
| 54 | local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}" |
| 55 | local IFS=',' |
| 56 | local item channel seen="" |
| 57 | read -r -a items <<< "$raw" |
| 58 | for item in "${items[@]}"; do |
| 59 |
| --- a/skills/gemini-relay/install.md | ||
| +++ b/skills/gemini-relay/install.md | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +# gemini-relay skill | |
| 2 | + | |
| 3 | +Installs Gemini CLI hooks that post your activity to an IRC channel in real time | |
| 4 | +and surface human instructions from IRC back into your context before each actionposts a summary of every tool call to the IRC channel | |
| 5 | +- mirrors the final assistant reply through `AfterAgent` | |
| 6 | + | |
| 7 | +## Install (Gemini CLI) | |
| 8 | +Detailed primer: [`hooks/README.md`](hooks/README.md) | |
| 9 | +Shared fleet guide: [`FLEET.md`](FLEET.md) | |
| 10 | +Shared adapter primer: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) | |
| 11 | +scuker entrypoint: `../openai-relay//gemini-relay/scri../openai-relay/: `skills/gemini-relay/scripts/inbroker checks for new addressed IRC messages | |
| 12 | +- `SCUTTLEBOT_PRESENCE_HEARTBEAT=60s` — controls HTTP presence touches; set `0` to disable | |
| 13 | +- `SCUTTLEBOT_AFTER_AGENT_MAX_POSTS=6` — caps how many IRC messages one final Gemini reply may emit | |
| 14 | +- `SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH=360` — sets the maximum width of each mirrored reply chunk | |
| 15 | + | |
| 16 | +Disable without uninstallinguttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) | |
| 17 | +scuttlebot-relay/SKILL.md) | |
| 18 | + | |
| 19 | +Canonical pattern summary: | |
| 20 | +- broker entrypoint: `cmd/gemini-relay/main.go` | |
| 21 | +- tracked installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` | |
| 22 | +- runtime docs: `skills/ginterrupts the live Gemini session when it appears busy | |
| 23 | +- `SCUTTLEBOT_POLL_INTERVAL=2s` — controls how often the broker checks for new addressed IRC messages | |
| 24 | +- `SCUTTLEBOT_PRESENCE_HEARTBEAT=60s` — controls HTTP presence touches; set `0` to disable | |
| 25 | +- `SCUTTLEBOT_AFTER_AGENT_MAX_POSTS=6` — caps how many IRC messages one final Gemini reply may emit | |
| 26 | +- `SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH=360` — sets the maximum width of each mirrored reply chunk | |
| 27 | + | |
| 28 | +Disable without uninstalling |
| --- a/skills/gemini-relay/install.md | |
| +++ b/skills/gemini-relay/install.md | |
| @@ -0,0 +1,28 @@ | |
| --- a/skills/gemini-relay/install.md | |
| +++ b/skills/gemini-relay/install.md | |
| @@ -0,0 +1,28 @@ | |
| 1 | # gemini-relay skill |
| 2 | |
| 3 | Installs Gemini CLI hooks that post your activity to an IRC channel in real time |
| 4 | and surface human instructions from IRC back into your context before each actionposts a summary of every tool call to the IRC channel |
| 5 | - mirrors the final assistant reply through `AfterAgent` |
| 6 | |
| 7 | ## Install (Gemini CLI) |
| 8 | Detailed primer: [`hooks/README.md`](hooks/README.md) |
| 9 | Shared fleet guide: [`FLEET.md`](FLEET.md) |
| 10 | Shared adapter primer: [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| 11 | scuker entrypoint: `../openai-relay//gemini-relay/scri../openai-relay/: `skills/gemini-relay/scripts/inbroker checks for new addressed IRC messages |
| 12 | - `SCUTTLEBOT_PRESENCE_HEARTBEAT=60s` — controls HTTP presence touches; set `0` to disable |
| 13 | - `SCUTTLEBOT_AFTER_AGENT_MAX_POSTS=6` — caps how many IRC messages one final Gemini reply may emit |
| 14 | - `SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH=360` — sets the maximum width of each mirrored reply chunk |
| 15 | |
| 16 | Disable without uninstallinguttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md) |
| 17 | scuttlebot-relay/SKILL.md) |
| 18 | |
| 19 | Canonical pattern summary: |
| 20 | - broker entrypoint: `cmd/gemini-relay/main.go` |
| 21 | - tracked installer: `skills/gemini-relay/scripts/install-gemini-relay.sh` |
| 22 | - runtime docs: `skills/ginterrupts the live Gemini session when it appears busy |
| 23 | - `SCUTTLEBOT_POLL_INTERVAL=2s` — controls how often the broker checks for new addressed IRC messages |
| 24 | - `SCUTTLEBOT_PRESENCE_HEARTBEAT=60s` — controls HTTP presence touches; set `0` to disable |
| 25 | - `SCUTTLEBOT_AFTER_AGENT_MAX_POSTS=6` — caps how many IRC messages one final Gemini reply may emit |
| 26 | - `SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH=360` — sets the maximum width of each mirrored reply chunk |
| 27 | |
| 28 | Disable without uninstalling |
| --- a/skills/gemini-relay/scripts/gemini-relay.sh | ||
| +++ b/skills/gemini-relay/scripts/gemini-relay.sh | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Development wrapper for the compiled Gemini 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/gemini-relay" ]; then | |
| 10 | + exec "$REPO_ROOT/bin/gemini-relay" "$@" | |
| 11 | +fi | |
| 12 | + | |
| 13 | +if ! command -v go >/dev/null 2>&1; then | |
| 14 | + printf 'gemini-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/gemini-relay" "$@" |
| --- a/skills/gemini-relay/scripts/gemini-relay.sh | |
| +++ b/skills/gemini-relay/scripts/gemini-relay.sh | |
| @@ -0,0 +1,18 @@ | |
| --- a/skills/gemini-relay/scripts/gemini-relay.sh | |
| +++ b/skills/gemini-relay/scripts/gemini-relay.sh | |
| @@ -0,0 +1,18 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Development wrapper for the compiled Gemini 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/gemini-relay" ]; then |
| 10 | exec "$REPO_ROOT/bin/gemini-relay" "$@" |
| 11 | fi |
| 12 | |
| 13 | if ! command -v go >/dev/null 2>&1; then |
| 14 | printf 'gemini-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/gemini-relay" "$@" |
| --- a/skills/gemini-relay/scripts/install-gemini-relay.sh | ||
| +++ b/skills/gemini-relay/scripts/install-gemini-relay.sh | ||
| @@ -0,0 +1,248 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Install the tracked Gemini relay hooks plus binary launcher into a local setup. | |
| 3 | + | |
| 4 | +set -euo pipefail | |
| 5 | + | |
| 6 | +usage() { | |
| 7 | + cat <<'EOF' | |
| 8 | +Usage: | |
| 9 | + bash skills/gemini-relay/scripts/install-gemini-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 | + --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. | |
| 16 | + --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. | |
| 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 Gemini hooks install dir. Default: ~/.gemini/hooks | |
| 21 | + --settings-json PATH Gemini settings JSON. Default: ~/.gemini/settings.json | |
| 22 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 23 | + --help Show this help. | |
| 24 | + | |
| 25 | +Environment defaults: | |
| 26 | + SCUTTLEBOT_URL | |
| 27 | + SCUTTLEBOT_TOKEN | |
| 28 | + SCUTTLEBOT_CHANNEL | |
| 29 | + SCUTTLEBOT_TRANSPORTUTTLEBOT_IRC_PASS ADDRUTTLEBOT_IRC_PASS so I | |
| 30 | + ;; | |
| 31 | + --disabled) | |
| 32 | + | |
| 33 | + | |
| 34 | + hooks: { | |
| 35 | + "Before | |
| 36 | + ash | |
| 37 | +# Install the tracke#!//.gemini/hooks | |
| 38 | + --settings-j | |
| 39 | + json | |
| 40 | + --bin-dir PATH | |
| 41 | + GEMINI_HOOKS_DIR | |
| 42 | + GEMINI_SETTINGS_JSON | |
| 43 | + GEMINI_BIN_DIR | |
| 44 | + | |
| 45 | +Examples: | |
| 46 | + bash skills/gemini-relay/scripts/install-gemini-relay.sh \ | |
| 47 | + --url http://localhost:8080 \ | |
| 48 | + --token "$(./run.sh token)" \ | |
| 49 | + --channel general | |
| 50 | + | |
| 51 | + SCUTTLEBOT_HOOKS_ENABLED=0 make install-gemini-relay | |
| 52 | +EOF | |
| 53 | +} | |
| 54 | + | |
| 55 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 56 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 57 | + | |
| 58 | +SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" | |
| 59 | +SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" | |
| 60 | +SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEay | |
| 61 | +EOF | |
| 62 | +} | |
| 63 | + | |
| 64 | +SCRIPT_DISCUTTLEBOT_TRANSPORT#!/usr/bin/env bash | |
| 65 | +# Install the tracked Gemini relay hooks plus binary laupefail | |
| 66 | + | |
| 67 | +usage() { | |
| 68 | + SCUTTLEBOT_IRC_PASS so IPASS:-}" | |
| 69 | +SCUTTLEBOT_XXX") | |
| 70 | +tmp_bin="$tmpenabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 71 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 72 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 73 | + --hooks-dir PATH Gemini hooks install dir. Default: ~/.gemini/hooks | |
| 74 | + --settings-json PATH Gemini settings JSON. Default: ~/.gemini/settings.json | |
| 75 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 76 | + --help Show this help. | |
| 77 | + | |
| 78 | +Environment defaults: | |
| 79 | + SCUTTLEBOT_URL | |
| 80 | + SCUTTLEBOT_TOKEN | |
| 81 | + SCUTTLEBOT_CHANNEL | |
| 82 | + SCUTTLEBOT_TRANSPORTUTTLEBOT_IRC_PASS ADDRUTTLEBOT_IRC_PASS so I | |
| 83 | + ;; | |
| 84 | + --disabled) | |
| 85 | + | |
| 86 | + | |
| 87 | + hooks: { | |
| 88 | + "Before | |
| 89 | + ash | |
| 90 | +# Install the tracke#!//.gemini/hooks | |
| 91 | + --settings-j | |
| 92 | + json | |
| 93 | + --bin-dir PATH | |
| 94 | + GEMINI_HOOKS_DIR | |
| 95 | + GEMINI_SETTINGS_JSON | |
| 96 | + GEMINI_BIN_DIR | |
| 97 | + | |
| 98 | +Examples: | |
| 99 | + bash skills/gemini-relay/scripts/install-gemini-relay.sh \ | |
| 100 | + --url http://localhost:8080 \ | |
| 101 | + --token "$(./run.sh token)" \ | |
| 102 | + --channel general | |
| 103 | + | |
| 104 | + SCUTTLEBOT_HOOKS_ENABLED=0 make install-gemini-relay | |
| 105 | +enhooks plus binaryelay hooks plus binary launcher into a local setup. | |
| 106 | + | |
| 107 | +set -euo pipefail | |
| 108 | + | |
| 109 | +usage() { | |
| 110 | + cat <<'EOF' | |
| 111 | +Usage: | |
| 112 | + bash skills/gemini-relay/scripts/install-gemini-relay.sh [options] | |
| 113 | + | |
| 114 | +Options: | |
| 115 | + --url URL Set SCUTTLEBOT_URL in the shared env file. | |
| 116 | + --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. | |
| 117 | + --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. | |
| 118 | + --channels CSV Set SCUTTLEBOT_CHANNELS in the shared env file. | |
| 119 | + --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. | |
| 120 | + --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. | |
| 121 | + --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. | |
| 122 | + --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. | |
| 123 | + --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 124 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 125 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 126 | + --hooks-dir PATH Gemini hooks install dir. Default: ~/.gemini/hooks | |
| 127 | + --settings-json PATH Gemini settings JSON. Default:bash | |
| 128 | +# InVAL | |
| 129 | + SCUTTLEBOT_PRESENCE_HEARTBEAT | |
| 130 | + SCUTTLEBOT_CONFIG_FILE | |
| 131 | + GEMINI_HOOKS_DIR | |
| 132 | + GEMINI_SETTINGS_JSON | |
| 133 | + GEMINI_BIN_DIR | |
| 134 | + | |
| 135 | +Examples: | |
| 136 | + bash skills/gemini-relay/scripts/install-gemini-relay.sh \ | |
| 137 | + --url http://localhost:8080 \ | |
| 138 | + --token "$(./run. value for --irc-addr}" | |
| 139 | + shift 2 | |
| 140 | + ;; | |
| 141 | + --irc-pass) | |
| 142 | + SCUTTLEBOT_IRC_PASS_MODE="fixed" | |
| 143 | + SCUTTLEBOT_IRC_PASS_VALUE="${2:?missing value for --irc-pass}" | |
| 144 | + shift 2 | |
| 145 | + ;; | |
| 146 | + --auto-register) | |
| 147 | + SCUTTLEBOT_IRC_PASS_MODE="auto" | |
| 148 | + SCUTTLEBOT_IRC_PASS_VALUE="" | |
| 149 | + shift | |
| 150 | + ;; | |
| 151 | + --enabled) | |
| 152 | + SCUTTLEBOT_HOOKS_ENABLED_VALUE=1 | |
| 153 | + shift | |
| 154 | + ;; | |
| 155 | + --disabled) | |
| 156 | + SCUTTLEBOT_HOOKS_ENABLED_VALUE=0 | |
| 157 | + shift | |
| 158 | + ;; | |
| 159 | + --config-file) | |
| 160 | + CONFIG_FILE="${2:?missing value for --config-file}" | |
| 161 | + shift 2 | |
| 162 | + ;; | |
| 163 | + --hooks-dir) | |
| 164 | + HOOKS_DIR="${2:?missing value for --hooks-dir}" | |
| 165 | + shift 2 | |
| 166 | + ;; | |
| 167 | + --settings-json) | |
| 168 | + SETTINGS_JSON="${2:?missing value for --settings-json}" | |
| 169 | + shift 2 | |
| 170 | + ;; | |
| 171 | + --bin-dir) | |
| 172 | + BIN_DIR="${2:?missing value for --bin-dir}" | |
| 173 | + shift 2 | |
| 174 | + ;; | |
| 175 | + --help|-h) | |
| 176 | + usage | |
| 177 | + exit 0 | |
| 178 | + ;; | |
| 179 | + *) | |
| 180 | + printf 'install-gemini-relay: unknown argument %s\n' "$1" >&2 | |
| 181 | + usage >&2 | |
| 182 | + exit 2 | |
| 183 | + ;; | |
| 184 | + esac | |
| 185 | +done | |
| 186 | + | |
| 187 | +need_cmd() { | |
| 188 | + if ! command -v "$1" >/dev/null 2>&1; then | |
| 189 | + printf 'install-gemini-relay: required command not found: %s\n' "$1" >&2 | |
| 190 | + exit 1 | |
| 191 | + fi | |
| 192 | +} | |
| 193 | + | |
| 194 | +backup_file() { | |
| 195 | + local path="$1" | |
| 196 | + if [ -f "$path" ] && [ ! -f "${path}.bak" ]; then | |
| 197 | + cp "$path" "${path}.bak" | |
| 198 | + fi | |
| 199 | +} | |
| 200 | + | |
| 201 | +ensure_parent_dir() { | |
| 202 | + mkdir -p "$(dirname "$1")" | |
| 203 | +} | |
| 204 | + | |
| 205 | +normalize_channels() { | |
| 206 | + local primary="$1" | |
| 207 | + local raw="$2" | |
| 208 | + local IFS=',' | |
| 209 | + local items=() | |
| 210 | + local extra_items=() | |
| 211 | + local item channel seen="" | |
| 212 | + | |
| 213 | + if [ -n "$primary" ]; then | |
| 214 | + items+=("$primary") | |
| 215 | + fi | |
| 216 | + if [ -n "$raw" ]; then | |
| 217 | + read -r -a extra_items <<< "$raw" | |
| 218 | + items+=("${extra_items[@]}") | |
| 219 | + fi | |
| 220 | + | |
| 221 | + for item in "${items[@]}"_cmd go | |
| 222 | + | |
| 223 | +POST_CMIRC_PASS_DIR/scuttlebot-after-agent.sh" | |
| 224 | +IRC_PASShooks // {}) | |
| 225 | + | .hVALUE" | |
| 226 | +fiay binary...\n' | |
| 227 | +tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/gemini-relay.XXXXXX") | |
| 228 | +tmp_bin="$tmp_dir/gemini-relay" | |
| 229 | +cleanup_tmp_bin() { | |
| 230 | + rm -rf "$tmp_dir" | |
| 231 | +} | |
| 232 | +trap cleanup_tmp_bin EXIT | |
| 233 | +(cd "$REPO_ROOT" && go build -o "$tmp_bin" ./cmd/gemini-relay) | |
| 234 | +install -m 0755 "$tmp_bin" "$LAUNCHER_DST" | |
| 235 | + | |
| 236 | +backup_file "$SETTINGS_JSON" | |
| 237 | +if [ -f "$SETTINGS_JSON" ]; then | |
| 238 | + jq --arg pre_matcher ".*" \ | |
| 239 | + --arg pre_cmd "$CHECK_CMD" \ | |
| 240 | + --arg post_matcher ".*" \ | |
| 241 | + --arg post_cmd "$POST_CMD" \ | |
| 242 | + --arg after_agent_matcher "*" \ | |
| 243 | + --arg after_agent_cmd "$AFTER_AGENT_CMD" ' | |
| 244 | + def ensure_matcher_entry(section; matcher;ks[section][]?; .matcher == matcher) then | |
| 245 | + .hooks[section] |= map( | |
| 246 | + if .matcher == matcher then | |
| 247 | + (.hooks = (.hooks // [])) | |
| 248 | + | if any(.hooks[]?; .type == "command" and .command == cmd) then . else .hooks += [{"type":"command","command":cmd} |
| --- a/skills/gemini-relay/scripts/install-gemini-relay.sh | |
| +++ b/skills/gemini-relay/scripts/install-gemini-relay.sh | |
| @@ -0,0 +1,248 @@ | |
| --- a/skills/gemini-relay/scripts/install-gemini-relay.sh | |
| +++ b/skills/gemini-relay/scripts/install-gemini-relay.sh | |
| @@ -0,0 +1,248 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Install the tracked Gemini relay hooks plus binary launcher into a local setup. |
| 3 | |
| 4 | set -euo pipefail |
| 5 | |
| 6 | usage() { |
| 7 | cat <<'EOF' |
| 8 | Usage: |
| 9 | bash skills/gemini-relay/scripts/install-gemini-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 | --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. |
| 16 | --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. |
| 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 Gemini hooks install dir. Default: ~/.gemini/hooks |
| 21 | --settings-json PATH Gemini settings JSON. Default: ~/.gemini/settings.json |
| 22 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 23 | --help Show this help. |
| 24 | |
| 25 | Environment defaults: |
| 26 | SCUTTLEBOT_URL |
| 27 | SCUTTLEBOT_TOKEN |
| 28 | SCUTTLEBOT_CHANNEL |
| 29 | SCUTTLEBOT_TRANSPORTUTTLEBOT_IRC_PASS ADDRUTTLEBOT_IRC_PASS so I |
| 30 | ;; |
| 31 | --disabled) |
| 32 | |
| 33 | |
| 34 | hooks: { |
| 35 | "Before |
| 36 | ash |
| 37 | # Install the tracke#!//.gemini/hooks |
| 38 | --settings-j |
| 39 | json |
| 40 | --bin-dir PATH |
| 41 | GEMINI_HOOKS_DIR |
| 42 | GEMINI_SETTINGS_JSON |
| 43 | GEMINI_BIN_DIR |
| 44 | |
| 45 | Examples: |
| 46 | bash skills/gemini-relay/scripts/install-gemini-relay.sh \ |
| 47 | --url http://localhost:8080 \ |
| 48 | --token "$(./run.sh token)" \ |
| 49 | --channel general |
| 50 | |
| 51 | SCUTTLEBOT_HOOKS_ENABLED=0 make install-gemini-relay |
| 52 | EOF |
| 53 | } |
| 54 | |
| 55 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 56 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 57 | |
| 58 | SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" |
| 59 | SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" |
| 60 | SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEay |
| 61 | EOF |
| 62 | } |
| 63 | |
| 64 | SCRIPT_DISCUTTLEBOT_TRANSPORT#!/usr/bin/env bash |
| 65 | # Install the tracked Gemini relay hooks plus binary laupefail |
| 66 | |
| 67 | usage() { |
| 68 | SCUTTLEBOT_IRC_PASS so IPASS:-}" |
| 69 | SCUTTLEBOT_XXX") |
| 70 | tmp_bin="$tmpenabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 71 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 72 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 73 | --hooks-dir PATH Gemini hooks install dir. Default: ~/.gemini/hooks |
| 74 | --settings-json PATH Gemini settings JSON. Default: ~/.gemini/settings.json |
| 75 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 76 | --help Show this help. |
| 77 | |
| 78 | Environment defaults: |
| 79 | SCUTTLEBOT_URL |
| 80 | SCUTTLEBOT_TOKEN |
| 81 | SCUTTLEBOT_CHANNEL |
| 82 | SCUTTLEBOT_TRANSPORTUTTLEBOT_IRC_PASS ADDRUTTLEBOT_IRC_PASS so I |
| 83 | ;; |
| 84 | --disabled) |
| 85 | |
| 86 | |
| 87 | hooks: { |
| 88 | "Before |
| 89 | ash |
| 90 | # Install the tracke#!//.gemini/hooks |
| 91 | --settings-j |
| 92 | json |
| 93 | --bin-dir PATH |
| 94 | GEMINI_HOOKS_DIR |
| 95 | GEMINI_SETTINGS_JSON |
| 96 | GEMINI_BIN_DIR |
| 97 | |
| 98 | Examples: |
| 99 | bash skills/gemini-relay/scripts/install-gemini-relay.sh \ |
| 100 | --url http://localhost:8080 \ |
| 101 | --token "$(./run.sh token)" \ |
| 102 | --channel general |
| 103 | |
| 104 | SCUTTLEBOT_HOOKS_ENABLED=0 make install-gemini-relay |
| 105 | enhooks plus binaryelay hooks plus binary launcher into a local setup. |
| 106 | |
| 107 | set -euo pipefail |
| 108 | |
| 109 | usage() { |
| 110 | cat <<'EOF' |
| 111 | Usage: |
| 112 | bash skills/gemini-relay/scripts/install-gemini-relay.sh [options] |
| 113 | |
| 114 | Options: |
| 115 | --url URL Set SCUTTLEBOT_URL in the shared env file. |
| 116 | --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. |
| 117 | --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. |
| 118 | --channels CSV Set SCUTTLEBOT_CHANNELS in the shared env file. |
| 119 | --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http. |
| 120 | --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. |
| 121 | --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. |
| 122 | --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. |
| 123 | --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 124 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 125 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 126 | --hooks-dir PATH Gemini hooks install dir. Default: ~/.gemini/hooks |
| 127 | --settings-json PATH Gemini settings JSON. Default:bash |
| 128 | # InVAL |
| 129 | SCUTTLEBOT_PRESENCE_HEARTBEAT |
| 130 | SCUTTLEBOT_CONFIG_FILE |
| 131 | GEMINI_HOOKS_DIR |
| 132 | GEMINI_SETTINGS_JSON |
| 133 | GEMINI_BIN_DIR |
| 134 | |
| 135 | Examples: |
| 136 | bash skills/gemini-relay/scripts/install-gemini-relay.sh \ |
| 137 | --url http://localhost:8080 \ |
| 138 | --token "$(./run. value for --irc-addr}" |
| 139 | shift 2 |
| 140 | ;; |
| 141 | --irc-pass) |
| 142 | SCUTTLEBOT_IRC_PASS_MODE="fixed" |
| 143 | SCUTTLEBOT_IRC_PASS_VALUE="${2:?missing value for --irc-pass}" |
| 144 | shift 2 |
| 145 | ;; |
| 146 | --auto-register) |
| 147 | SCUTTLEBOT_IRC_PASS_MODE="auto" |
| 148 | SCUTTLEBOT_IRC_PASS_VALUE="" |
| 149 | shift |
| 150 | ;; |
| 151 | --enabled) |
| 152 | SCUTTLEBOT_HOOKS_ENABLED_VALUE=1 |
| 153 | shift |
| 154 | ;; |
| 155 | --disabled) |
| 156 | SCUTTLEBOT_HOOKS_ENABLED_VALUE=0 |
| 157 | shift |
| 158 | ;; |
| 159 | --config-file) |
| 160 | CONFIG_FILE="${2:?missing value for --config-file}" |
| 161 | shift 2 |
| 162 | ;; |
| 163 | --hooks-dir) |
| 164 | HOOKS_DIR="${2:?missing value for --hooks-dir}" |
| 165 | shift 2 |
| 166 | ;; |
| 167 | --settings-json) |
| 168 | SETTINGS_JSON="${2:?missing value for --settings-json}" |
| 169 | shift 2 |
| 170 | ;; |
| 171 | --bin-dir) |
| 172 | BIN_DIR="${2:?missing value for --bin-dir}" |
| 173 | shift 2 |
| 174 | ;; |
| 175 | --help|-h) |
| 176 | usage |
| 177 | exit 0 |
| 178 | ;; |
| 179 | *) |
| 180 | printf 'install-gemini-relay: unknown argument %s\n' "$1" >&2 |
| 181 | usage >&2 |
| 182 | exit 2 |
| 183 | ;; |
| 184 | esac |
| 185 | done |
| 186 | |
| 187 | need_cmd() { |
| 188 | if ! command -v "$1" >/dev/null 2>&1; then |
| 189 | printf 'install-gemini-relay: required command not found: %s\n' "$1" >&2 |
| 190 | exit 1 |
| 191 | fi |
| 192 | } |
| 193 | |
| 194 | backup_file() { |
| 195 | local path="$1" |
| 196 | if [ -f "$path" ] && [ ! -f "${path}.bak" ]; then |
| 197 | cp "$path" "${path}.bak" |
| 198 | fi |
| 199 | } |
| 200 | |
| 201 | ensure_parent_dir() { |
| 202 | mkdir -p "$(dirname "$1")" |
| 203 | } |
| 204 | |
| 205 | normalize_channels() { |
| 206 | local primary="$1" |
| 207 | local raw="$2" |
| 208 | local IFS=',' |
| 209 | local items=() |
| 210 | local extra_items=() |
| 211 | local item channel seen="" |
| 212 | |
| 213 | if [ -n "$primary" ]; then |
| 214 | items+=("$primary") |
| 215 | fi |
| 216 | if [ -n "$raw" ]; then |
| 217 | read -r -a extra_items <<< "$raw" |
| 218 | items+=("${extra_items[@]}") |
| 219 | fi |
| 220 | |
| 221 | for item in "${items[@]}"_cmd go |
| 222 | |
| 223 | POST_CMIRC_PASS_DIR/scuttlebot-after-agent.sh" |
| 224 | IRC_PASShooks // {}) |
| 225 | | .hVALUE" |
| 226 | fiay binary...\n' |
| 227 | tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/gemini-relay.XXXXXX") |
| 228 | tmp_bin="$tmp_dir/gemini-relay" |
| 229 | cleanup_tmp_bin() { |
| 230 | rm -rf "$tmp_dir" |
| 231 | } |
| 232 | trap cleanup_tmp_bin EXIT |
| 233 | (cd "$REPO_ROOT" && go build -o "$tmp_bin" ./cmd/gemini-relay) |
| 234 | install -m 0755 "$tmp_bin" "$LAUNCHER_DST" |
| 235 | |
| 236 | backup_file "$SETTINGS_JSON" |
| 237 | if [ -f "$SETTINGS_JSON" ]; then |
| 238 | jq --arg pre_matcher ".*" \ |
| 239 | --arg pre_cmd "$CHECK_CMD" \ |
| 240 | --arg post_matcher ".*" \ |
| 241 | --arg post_cmd "$POST_CMD" \ |
| 242 | --arg after_agent_matcher "*" \ |
| 243 | --arg after_agent_cmd "$AFTER_AGENT_CMD" ' |
| 244 | def ensure_matcher_entry(section; matcher;ks[section][]?; .matcher == matcher) then |
| 245 | .hooks[section] |= map( |
| 246 | if .matcher == matcher then |
| 247 | (.hooks = (.hooks // [])) |
| 248 | | if any(.hooks[]?; .type == "command" and .command == cmd) then . else .hooks += [{"type":"command","command":cmd} |
| --- a/skills/scuttlebot-relay/FLEET.md | ||
| +++ b/skills/scuttlebot-relay/FLEET.md | ||
| @@ -0,0 +1,65 @@ | ||
| 1 | +# Claude Relay Fleet Launch | |
| 2 | + | |
| 3 | +This is the rollout guide for making local Claude Code terminal sessions IRC-visible and | |
| 4 | +operator-addressable through scuttlebot. | |
| 5 | + | |
| 6 | +Source of truth: | |
| 7 | +- installer: [`scripts/install-claude-relay.sh`](scripts/install-claude-relay.sh) | |
| 8 | +- broker: [`../../cmd/claude-relay/main.go`](../../cmd/claude-relay/main.go) | |
| 9 | +- shared connector: [`../../pkg/sessionrelay/`](../../pkg/sessionrelay/) | |
| 10 | +- hooks: [`hooks/scuttlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) | |
| 11 | +- runtime docs: [`install.md`](install.md) | |
| 12 | +- shared runtime contract: [`ADDING_AGENTS.md`](ADDING_AGENTS.md) | |
| 13 | + | |
| 14 | +Installed files under `~/.claude/`, `~/.local/bin/`, and `~/.config/` are generated | |
| 15 | +copies. Point other engineers and agents at the repo docs and installer, not at one | |
| 16 | +person's home directory. | |
| 17 | + | |
| 18 | +Runtime prerequisites: | |
| 19 | +- `claude` (Claude Code) | |
| 20 | +- `go` | |
| 21 | +- `curl` | |
| 22 | +- `jq` | |
| 23 | + | |
| 24 | +## What this gives you | |
| 25 | + | |
| 26 | +For each local Claude session launched through `claude-relay`: | |
| 27 | +- a stable nick: `claude-{repo}-{session}` | |
| 28 | +- immediate `online` post when the session starts | |
| 29 | +- real-time tool activity posts via hooks | |
| 30 | +- continuous addressed IRC input injection into the live terminal session | |
| 31 | +- explicit pre-tool fallback inter bridge/API path and now uses presence heartbeats | |
| 32 | +- `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence | |
| 33 | + | |
| 34 | +This is the production control path for a human-operated Claude terminal. If you | |
| 35 | +want an always-on IRC-resident bot instead, use `cmd/claude-agent`. | |
| 36 | + | |
| 37 | +## One-machine install | |
| 38 | + | |
| 39 | +Run f --token "$(./run.sh token)" \ | |
| 40 | + --channel general | |
| 41 | +``` | |
| 42 | + | |
| 43 | +Then launch: | |
| 44 | + | |
| 45 | +```bash | |
| 46 | +~/.local/bin/claude-relay | |
| 47 | +``` | |
| 48 | + | |
| 49 | +## Fleet rollout | |
| 50 | + | |
| 51 | +For multiple workstations or VM imaash | |
| 52 | +~/.local/bin/cl Run the tracked installer on each machine. | |
| 53 | +3. Launch Claude through `~/.local/bin/claude-relay` instead of `claude`. | |
| 54 | + | |
| 55 | +Example: | |
| 56 | + | |
| 57 | +```bash | |
| 58 | +bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \internaltp://scuttlebot.example.com:8080 \ | |
| 59 | + --token "$SCUTTLEBOT_TOKEN" \ | |
| 60 | + --channeinternal# Claude Relay Fleet Launch | |
| 61 | + | |
| 62 | +This is the rollout guide for making local Claude Code terminal sessions IRC-visible and | |
| 63 | +operator-addressable through scuttlebot. | |
| 64 | + | |
| 65 | +Source of truth |
| --- a/skills/scuttlebot-relay/FLEET.md | |
| +++ b/skills/scuttlebot-relay/FLEET.md | |
| @@ -0,0 +1,65 @@ | |
| --- a/skills/scuttlebot-relay/FLEET.md | |
| +++ b/skills/scuttlebot-relay/FLEET.md | |
| @@ -0,0 +1,65 @@ | |
| 1 | # Claude Relay Fleet Launch |
| 2 | |
| 3 | This is the rollout guide for making local Claude Code terminal sessions IRC-visible and |
| 4 | operator-addressable through scuttlebot. |
| 5 | |
| 6 | Source of truth: |
| 7 | - installer: [`scripts/install-claude-relay.sh`](scripts/install-claude-relay.sh) |
| 8 | - broker: [`../../cmd/claude-relay/main.go`](../../cmd/claude-relay/main.go) |
| 9 | - shared connector: [`../../pkg/sessionrelay/`](../../pkg/sessionrelay/) |
| 10 | - hooks: [`hooks/scuttlebot-post.sh`](hooks/scuttlebot-post.sh), [`hooks/scuttlebot-check.sh`](hooks/scuttlebot-check.sh) |
| 11 | - runtime docs: [`install.md`](install.md) |
| 12 | - shared runtime contract: [`ADDING_AGENTS.md`](ADDING_AGENTS.md) |
| 13 | |
| 14 | Installed files under `~/.claude/`, `~/.local/bin/`, and `~/.config/` are generated |
| 15 | copies. Point other engineers and agents at the repo docs and installer, not at one |
| 16 | person's home directory. |
| 17 | |
| 18 | Runtime prerequisites: |
| 19 | - `claude` (Claude Code) |
| 20 | - `go` |
| 21 | - `curl` |
| 22 | - `jq` |
| 23 | |
| 24 | ## What this gives you |
| 25 | |
| 26 | For each local Claude session launched through `claude-relay`: |
| 27 | - a stable nick: `claude-{repo}-{session}` |
| 28 | - immediate `online` post when the session starts |
| 29 | - real-time tool activity posts via hooks |
| 30 | - continuous addressed IRC input injection into the live terminal session |
| 31 | - explicit pre-tool fallback inter bridge/API path and now uses presence heartbeats |
| 32 | - `SCUTTLEBOT_TRANSPORT=irc` logs the session nick directly into Ergo for real presence |
| 33 | |
| 34 | This is the production control path for a human-operated Claude terminal. If you |
| 35 | want an always-on IRC-resident bot instead, use `cmd/claude-agent`. |
| 36 | |
| 37 | ## One-machine install |
| 38 | |
| 39 | Run f --token "$(./run.sh token)" \ |
| 40 | --channel general |
| 41 | ``` |
| 42 | |
| 43 | Then launch: |
| 44 | |
| 45 | ```bash |
| 46 | ~/.local/bin/claude-relay |
| 47 | ``` |
| 48 | |
| 49 | ## Fleet rollout |
| 50 | |
| 51 | For multiple workstations or VM imaash |
| 52 | ~/.local/bin/cl Run the tracked installer on each machine. |
| 53 | 3. Launch Claude through `~/.local/bin/claude-relay` instead of `claude`. |
| 54 | |
| 55 | Example: |
| 56 | |
| 57 | ```bash |
| 58 | bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \internaltp://scuttlebot.example.com:8080 \ |
| 59 | --token "$SCUTTLEBOT_TOKEN" \ |
| 60 | --channeinternal# Claude Relay Fleet Launch |
| 61 | |
| 62 | This is the rollout guide for making local Claude Code terminal sessions IRC-visible and |
| 63 | operator-addressable through scuttlebot. |
| 64 | |
| 65 | Source of truth |
| --- a/skills/scuttlebot-relay/hooks/README.md | ||
| +++ b/skills/scuttlebot-relay/hooks/README.md | ||
| @@ -0,0 +1,39 @@ | ||
| 1 | +# Claude Hook Poperator-controls are the prenrelay` connector package. | |
| 2 | + | |
| 3 | +If you need to add another runtime later, use | |
| 4 | +[`../ADDING_AGENTS.md`](../ADDING_AGENTS.md) as the shared authoring contract. | |
| 5 | + | |
| 6 | +Files in this directory: | |
| 7 | +- `scuttlebot-post.shscuttl | |
| 8 | +- runs after each matching Claude tool call | |
| 9 | +- posts a one-line actiITY_VIA_BROKER=1`to the shared scuttlebot channelstant output and tool activity from the active Claude session log. | |
| 10 | +3. The operator mentions the Claudes that IRC message into the live terminal session immediately. | |
| 11 | +5. `scuttlebot-check.sh` still blocks before tEBOT_CHANNEL` | |
| 12 | + | |
| 13 | +Optional: | |
| 14 | +- `SCUTTLEBOT_NICK` | |
| 15 | +- `SCUTTLEBOT_CHANNELS` | |
| 16 | +- `SCUTTLEBOT_CHANNEL_STATE_FILE` | |
| 17 | +- `SCUTTLEBOT_TRANSPORT` | |
| 18 | +- `TLEBOT_IRC_ADDR` | |
| 19 | +- `SCUTTLEBOT_IRC_PASS` | |
| 20 | +- `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` — set to `false` to keep agent registration | |
| 21 | + records after the relay disconnects (default: `true`, records are cleaned up) | |
| 22 | +- `SCUTTLEBOT_HOOKS_ENABLED` | |
| 23 | +- `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` | |
| 24 | +- `SCUTTLEBOT_POLL_INTERVAL` | |
| 25 | +- `SCUTTLEBOT_PRESENCE_HEARTBEAT` | |
| 26 | +- `SCUTTLEBOT_CONFIGne-line actiITY_VIA_BROKER=1`CONFIG_FILEign. | |
| 27 | +2. `cmd/claude-relay` mirrors assistant output and tool activity from the active Claude session log. | |
| 28 | +3. The operator mentions the Claudes that IRC message into the live terminal session immediately. | |
| 29 | +5. `scuttlebot-check.sh` still blocks before the next tool action if needed. | |
| 30 | + | |
| 31 | +For immediate startup visibility and continuous IRHOOKS_ENABLED=1 | |
| 32 | +EOF | |
| 33 | +```3d4` | |
| 34 | +- `claude-api-e5f6a7b8` | |
| 35 | + | |
| 36 | +If you want a fixed nick instead, export `SCUTTLEBOClaude installDR` | |
| 37 | +- `SCUTTLEBOT_IRC_PASS` | |
| 38 | +- `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` — set to `false` to keep agent registration | |
| 39 | + records after the relay disconnects |
| --- a/skills/scuttlebot-relay/hooks/README.md | |
| +++ b/skills/scuttlebot-relay/hooks/README.md | |
| @@ -0,0 +1,39 @@ | |
| --- a/skills/scuttlebot-relay/hooks/README.md | |
| +++ b/skills/scuttlebot-relay/hooks/README.md | |
| @@ -0,0 +1,39 @@ | |
| 1 | # Claude Hook Poperator-controls are the prenrelay` connector package. |
| 2 | |
| 3 | If you need to add another runtime later, use |
| 4 | [`../ADDING_AGENTS.md`](../ADDING_AGENTS.md) as the shared authoring contract. |
| 5 | |
| 6 | Files in this directory: |
| 7 | - `scuttlebot-post.shscuttl |
| 8 | - runs after each matching Claude tool call |
| 9 | - posts a one-line actiITY_VIA_BROKER=1`to the shared scuttlebot channelstant output and tool activity from the active Claude session log. |
| 10 | 3. The operator mentions the Claudes that IRC message into the live terminal session immediately. |
| 11 | 5. `scuttlebot-check.sh` still blocks before tEBOT_CHANNEL` |
| 12 | |
| 13 | Optional: |
| 14 | - `SCUTTLEBOT_NICK` |
| 15 | - `SCUTTLEBOT_CHANNELS` |
| 16 | - `SCUTTLEBOT_CHANNEL_STATE_FILE` |
| 17 | - `SCUTTLEBOT_TRANSPORT` |
| 18 | - `TLEBOT_IRC_ADDR` |
| 19 | - `SCUTTLEBOT_IRC_PASS` |
| 20 | - `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` — set to `false` to keep agent registration |
| 21 | records after the relay disconnects (default: `true`, records are cleaned up) |
| 22 | - `SCUTTLEBOT_HOOKS_ENABLED` |
| 23 | - `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` |
| 24 | - `SCUTTLEBOT_POLL_INTERVAL` |
| 25 | - `SCUTTLEBOT_PRESENCE_HEARTBEAT` |
| 26 | - `SCUTTLEBOT_CONFIGne-line actiITY_VIA_BROKER=1`CONFIG_FILEign. |
| 27 | 2. `cmd/claude-relay` mirrors assistant output and tool activity from the active Claude session log. |
| 28 | 3. The operator mentions the Claudes that IRC message into the live terminal session immediately. |
| 29 | 5. `scuttlebot-check.sh` still blocks before the next tool action if needed. |
| 30 | |
| 31 | For immediate startup visibility and continuous IRHOOKS_ENABLED=1 |
| 32 | EOF |
| 33 | ```3d4` |
| 34 | - `claude-api-e5f6a7b8` |
| 35 | |
| 36 | If you want a fixed nick instead, export `SCUTTLEBOClaude installDR` |
| 37 | - `SCUTTLEBOT_IRC_PASS` |
| 38 | - `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` — set to `false` to keep agent registration |
| 39 | records after the relay disconnects |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | ||
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | ||
| @@ -0,0 +1,36 @@ | ||
| 1 | +seconds() { | |
| 2 | + local at="$1" | |
| 3 | + local ts_clean ts | |
| 4 | + ts_clean=$(echo "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/') | |
| 5 | + ts][0-9]\)$/\1\2/') | |
| 6 | + ts_secs=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" "+%/dev/null || \printf '%s' "$ts" | |
| 7 | +} | |
| 8 | + | |
| 9 | +cwd=$(echo "$input" | jq -r '.#!/bin/bash | |
| 10 | +# PreToolUse hook — checks IRC for human instructions before each tool call. | |
| 11 | +# Only messageIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" | |
| 12 | +if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then | |
| 13 | + set -a | |
| 14 | + . "$SCUTTLEBOT_CONFIG_FILE" | |
| 15 | + set +a | |
| 16 | +fi | |
| 17 | +if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then | |
| 18 | + set -a | |
| 19 | + . "$SCUTTLEBOT_CHANNEL_STATE_FILE" | |
| 20 | + set +a | |
| 21 | +fi | |
| 22 | + | |
| 23 | +SCUTTLEBOT_URL="${SCU -n "$ts" ] || continue | |
| 24 | + [ "$ts" -gt "$last_check" ] || continue | |
| 25 | + contains_mention "$text" || continue | |
| 26 | + printf '%s\t[#%s] %s: %s\n' "$ts" "$channel" "$nick" "$text" | |
| 27 | + done | sort -n | tail -1 | cut -f2- | |
| 28 | +) | |
| 29 | + | |
| 30 | +[ -z "$instruction" ] && exit 0 | |
| 31 | + | |
| 32 | +CHANNEL|seconds() { | |
| 33 | + local at="$1" | |
| 34 | + local ts_clean ts | |
| 35 | + ts_clean=$(echo "$at" | sed 's/\.[ local ts_clean ts | |
| 36 | + ts_clean=$(echo "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([se |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,36 @@ | |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | |
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-check.sh | |
| @@ -0,0 +1,36 @@ | |
| 1 | seconds() { |
| 2 | local at="$1" |
| 3 | local ts_clean ts |
| 4 | ts_clean=$(echo "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/') |
| 5 | ts][0-9]\)$/\1\2/') |
| 6 | ts_secs=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" "+%/dev/null || \printf '%s' "$ts" |
| 7 | } |
| 8 | |
| 9 | cwd=$(echo "$input" | jq -r '.#!/bin/bash |
| 10 | # PreToolUse hook — checks IRC for human instructions before each tool call. |
| 11 | # Only messageIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" |
| 12 | if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then |
| 13 | set -a |
| 14 | . "$SCUTTLEBOT_CONFIG_FILE" |
| 15 | set +a |
| 16 | fi |
| 17 | if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then |
| 18 | set -a |
| 19 | . "$SCUTTLEBOT_CHANNEL_STATE_FILE" |
| 20 | set +a |
| 21 | fi |
| 22 | |
| 23 | SCUTTLEBOT_URL="${SCU -n "$ts" ] || continue |
| 24 | [ "$ts" -gt "$last_check" ] || continue |
| 25 | contains_mention "$text" || continue |
| 26 | printf '%s\t[#%s] %s: %s\n' "$ts" "$channel" "$nick" "$text" |
| 27 | done | sort -n | tail -1 | cut -f2- |
| 28 | ) |
| 29 | |
| 30 | [ -z "$instruction" ] && exit 0 |
| 31 | |
| 32 | CHANNEL|seconds() { |
| 33 | local at="$1" |
| 34 | local ts_clean ts |
| 35 | ts_clean=$(echo "$at" | sed 's/\.[ local ts_clean ts |
| 36 | ts_clean=$(echo "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([se |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | ||
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | ||
| @@ -0,0 +1,24 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +# PostToolUse Cose hook — posts what Claude j# Reads Claude C. | |
| 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 | +if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then | |
| 11 | + set -a | |
| 12 | + . "$SCUTTLEBOT_CHANNEL_STATE_FILE" | |
| 13 | + set +a | |
| 14 | +fi | |
| 15 | + | |
| 16 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 17 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" | |
| 18 | +SCUTTLEBOTSCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 19 | +SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" | |
| 20 | + | |
| 21 | +normalize_channel() { | |
| 22 | + local channel="$1" | |
| 23 | + channel="${channel//[$' \t\r\n']/}" | |
| 24 | + channel="${chan |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,24 @@ | |
| --- a/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | |
| +++ b/skills/scuttlebot-relay/hooks/scuttlebot-post.sh | |
| @@ -0,0 +1,24 @@ | |
| 1 | #!/bin/bash |
| 2 | # PostToolUse Cose hook — posts what Claude j# Reads Claude C. |
| 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 | if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then |
| 11 | set -a |
| 12 | . "$SCUTTLEBOT_CHANNEL_STATE_FILE" |
| 13 | set +a |
| 14 | fi |
| 15 | |
| 16 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 17 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}" |
| 18 | SCUTTLEBOTSCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 19 | SCUTTLEBOT_ACTIVITY_VIA_BROKER="${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" |
| 20 | |
| 21 | normalize_channel() { |
| 22 | local channel="$1" |
| 23 | channel="${channel//[$' \t\r\n']/}" |
| 24 | channel="${chan |
| --- a/skills/scuttlebot-relay/install.md | ||
| +++ b/skills/scuttlebot-relay/install.md | ||
| @@ -0,0 +1,6 @@ | ||
| 1 | +# scuttlebot-relay skill | |
| 2 | + | |
| 3 | +Installs Claude Code hooks that post your activity to an IRC channel in real time | |
| 4 | +and surface human instructions from IRC back into your context before each action. | |
| 5 | + | |
| 6 | + \ |
| --- a/skills/scuttlebot-relay/install.md | |
| +++ b/skills/scuttlebot-relay/install.md | |
| @@ -0,0 +1,6 @@ | |
| --- a/skills/scuttlebot-relay/install.md | |
| +++ b/skills/scuttlebot-relay/install.md | |
| @@ -0,0 +1,6 @@ | |
| 1 | # scuttlebot-relay skill |
| 2 | |
| 3 | Installs Claude Code hooks that post your activity to an IRC channel in real time |
| 4 | and surface human instructions from IRC back into your context before each action. |
| 5 | |
| 6 | \ |
| --- a/skills/scuttlebot-relay/scripts/claude-relay.sh | ||
| +++ b/skills/scuttlebot-relay/scripts/claude-relay.sh | ||
| @@ -0,0 +1,196 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Launch Claude with a fleet-style session nick. | |
| 3 | +# Registers a claude-{project}-{session} nick, starts the IRC agent in the | |
| 4 | +# background under that nick (so hook activity and IRC responses share one | |
| 5 | +# identity), then runs the Claude CLI. Deregisters on exit. | |
| 6 | + | |
| 7 | +set -u | |
| 8 | + | |
| 9 | +SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" | |
| 10 | +if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then | |
| 11 | + set -a | |
| 12 | + . "$SCUTTLEBOT_CONFIG_FILE" | |
| 13 | + set +a | |
| 14 | +fi | |
| 15 | + | |
| 16 | +SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" | |
| 17 | +SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN:-}" | |
| 18 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" | |
| 19 | +SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" | |
| 20 | +SCUTTLEBOT_IRC="${SCUTTLEBOT_IRC:-127.0.0.1:6667}" | |
| 21 | +SCUTTLEBOT_BACKEND="${SCUTTLEBOT_BACKEND:-anthro}" | |
| 22 | +CLAUDE_AGENT_BIN="${CLAUDE_AGENT_BIN:-}" | |
| 23 | +CLAUDE_BIN="${CLAUDE_BIN:-claude}" | |
| 24 | + | |
| 25 | +sanitize() { | |
| 26 | + local input="$1" | |
| 27 | + if [ -z "$input" ]; then | |
| 28 | + input=$(cat) | |
| 29 | + fi | |
| 30 | + printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' | |
| 31 | +} | |
| 32 | + | |
| 33 | +target_cwd() { | |
| 34 | + local cwd="$PWD" | |
| 35 | + local prev="" | |
| 36 | + local arg | |
| 37 | + for arg in "$@"; do | |
| 38 | + if [ "$prev" = "-C" ] || [ "$prev" = "--cd" ]; then | |
| 39 | + cwd="$arg" | |
| 40 | + prev="" | |
| 41 | + continue | |
| 42 | + fi | |
| 43 | + case "$arg" in | |
| 44 | + -C|--cd) | |
| 45 | + prev="$arg" | |
| 46 | + ;; | |
| 47 | + -C=*|--cd=*) | |
| 48 | + cwd="${arg#*=}" | |
| 49 | + ;; | |
| 50 | + esac | |
| 51 | + done | |
| 52 | + if [ -d "$cwd" ]; then | |
| 53 | + (cd "$cwd" && pwd) | |
| 54 | + else | |
| 55 | + printf '%s\n' "$PWD" | |
| 56 | + fi | |
| 57 | +} | |
| 58 | + | |
| 59 | +hooks_enabled() { | |
| 60 | + [ "$SCUTTLEBOT_HOOKS_ENABLED" != "0" ] && | |
| 61 | + [ "$SCUTTLEBOT_HOOKS_ENABLED" != "false" ] && | |
| 62 | + [ -n "$SCUTTLEBOT_TOKEN" ] | |
| 63 | +} | |
| 64 | + | |
| 65 | +post_status() { | |
| 66 | + local text="$1" | |
| 67 | + hooks_enabled || return 0 | |
| 68 | + command -v curl >/dev/null 2>&1 || return 0 | |
| 69 | + command -v jq >/dev/null 2>&1 || return 0 | |
| 70 | + curl -sf -X POST "$SCUTTLEBOT_URL/v1/channels/$SCUTTLEBOT_CHANNEL/messages" \ | |
| 71 | + --connect-timeout 1 \ | |
| 72 | + --max-time 2 \ | |
| 73 | + -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ | |
| 74 | + -H "Content-Type: application/json" \ | |
| 75 | + -d "{\"text\": $(printf '%s' "$text" | jq -Rs .), \"nick\": \"$SCUTTLEBOT_NICK\"}" \ | |
| 76 | + > /dev/null || true | |
| 77 | +} | |
| 78 | + | |
| 79 | +if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then | |
| 80 | + printf 'claude-relay: %s not found in PATH\n' "$CLAUDE_BIN" >&2 | |
| 81 | + exit 127 | |
| 82 | +fi | |
| 83 | + | |
| 84 | +TARGET_CWD=$(target_cwd "$@") | |
| 85 | +BASE_NAME=$(sanitize "$(basename "$TARGET_CWD")") | |
| 86 | + | |
| 87 | +if [ -z "${SCUTTLEBOT_SESSION_ID:-}" ]; then | |
| 88 | + SCUTTLEBOT_SESSION_ID=$( | |
| 89 | + printf '%s' "$TARGET_CWD|$$|$PPID|$(date +%s)" | cksum | awk '{print $1}' | cut -c 1-8 | |
| 90 | + ) | |
| 91 | +fi | |
| 92 | +SCUTTLEBOT_SESSION_ID=$(sanitize "$SCUTTLEBOT_SESSION_ID") | |
| 93 | +if [ -z "${SCUTTLEBOT_NICK:-}" ]; then | |
| 94 | + SCUTTLEBOT_NICK="claude-${BASE_NAME}-${SCUTTLEBOT_SESSION_ID}" | |
| 95 | +fi | |
| 96 | +SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL#\#}" | |
| 97 | + | |
| 98 | +export SCUTTLEBOT_CONFIG_FILE | |
| 99 | +export SCUTTLEBOT_URL | |
| 100 | +export SCUTTLEBOT_TOKEN | |
| 101 | +export SCUTTLEBOT_CHANNEL | |
| 102 | +export SCUTTLEBOT_HOOKS_ENABLED | |
| 103 | +export SCUTTLEBOT_SESSION_ID | |
| 104 | +export SCUTTLEBOT_NICK | |
| 105 | + | |
| 106 | +printf 'claude-relay: nick %s\n' "$SCUTTLEBOT_NICK" >&2 | |
| 107 | + | |
| 108 | +# --- IRC agent: register nick and start in background --- | |
| 109 | +irc_agent_pid="" | |
| 110 | +irc_agent_nick="" | |
| 111 | + | |
| 112 | +_start_irc_agent() { | |
| 113 | + [ -n "$SCUTTLEBOT_TOKEN" ] || return 0 | |
| 114 | + | |
| 115 | + # Find the claude-agent binary: next to this script, in PATH, or skip. | |
| 116 | + local bin="$CLAUDE_AGENT_BIN" | |
| 117 | + if [ -z "$bin" ]; then | |
| 118 | + local script_dir; script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 119 | + local repo_root; repo_root=$(CDPATH= cd -- "$script_dir/../../.." && pwd) | |
| 120 | + if [ -x "$repo_root/bin/claude-agent" ]; then | |
| 121 | + bin="$repo_root/bin/claude-agent" | |
| 122 | + elif command -v claude-agent >/dev/null 2>&1; then | |
| 123 | + bin="claude-agent" | |
| 124 | + else | |
| 125 | + printf 'claude-relay: claude-agent not found, IRC responses disabled\n' >&2 | |
| 126 | + return 0 | |
| 127 | + fi | |
| 128 | + fi | |
| 129 | + | |
| 130 | + local resp; resp=$(curl -sf -X POST \ | |
| 131 | + --connect-timeout 2 --max-time 5 \ | |
| 132 | + -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ | |
| 133 | + -H "Content-Type: application/json" \ | |
| 134 | + -d "{\"nick\":\"$SCUTTLEBOT_NICK\",\"type\":\"worker\",\"channels\":[\"#$SCUTTLEBOT_CHANNEL\"]}" \ | |
| 135 | + "$SCUTTLEBOT_URL/v1/agents/register" 2>/dev/null) || return 0 | |
| 136 | + | |
| 137 | + local pass; pass=$(printf '%s' "$resp" | grep -o '"passphrase":"[^"]*"' | cut -d'"' -f4) | |
| 138 | + [ -n "$pass" ] || return 0 | |
| 139 | + | |
| 140 | + irc_agent_nick="$SCUTTLEBOT_NICK" | |
| 141 | + "$bin" \ | |
| 142 | + --irc "$SCUTTLEBOT_IRC" \ | |
| 143 | + --nick "$irc_agent_nick" \ | |
| 144 | + --pass "$pass" \ | |
| 145 | + --channels "#$SCUTTLEBOT_CHANNEL" \ | |
| 146 | + --api-url "$SCUTTLEBOT_URL" \ | |
| 147 | + --token "$SCUTTLEBOT_TOKEN" \ | |
| 148 | + --backend "$SCUTTLEBOT_BACKEND" \ | |
| 149 | + 2>/dev/null & | |
| 150 | + irc_agent_pid=$! | |
| 151 | + printf 'claude-relay: IRC agent started (pid %s)\n' "$irc_agent_pid" >&2 | |
| 152 | +} | |
| 153 | + | |
| 154 | +_stop_irc_agent() { | |
| 155 | + if [ -n "$irc_agent_pid" ]; then | |
| 156 | + kill "$irc_agent_pid" 2>/dev/null || true | |
| 157 | + irc_agent_pid="" | |
| 158 | + fi | |
| 159 | + if [ -n "$irc_agent_nick" ] && [ -n "$SCUTTLEBOT_TOKEN" ]; then | |
| 160 | + curl -sf -X DELETE \ | |
| 161 | + --connect-timeout 2 --max-time 5 \ | |
| 162 | + -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ | |
| 163 | + "$SCUTTLEBOT_URL/v1/agents/$irc_agent_nick" >/dev/null 2>&1 || true | |
| 164 | + irc_agent_nick="" | |
| 165 | + fi | |
| 166 | +} | |
| 167 | + | |
| 168 | +_start_irc_agent | |
| 169 | + | |
| 170 | +# --- Claude CLI --- | |
| 171 | +post_status "online in $(basename "$TARGET_CWD"); mention $SCUTTLEBOT_NICK to interrupt" | |
| 172 | + | |
| 173 | +child_pid="" | |
| 174 | +_cleanup() { | |
| 175 | + [ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null || true | |
| 176 | + _stop_irc_agent | |
| 177 | + post_status "offline" | |
| 178 | +} | |
| 179 | + | |
| 180 | +forward_signal() { | |
| 181 | + local signal="$1" | |
| 182 | + [ -n "$child_pid" ] && kill "-$signal" "$child_pid" 2>/dev/null || true | |
| 183 | +} | |
| 184 | + | |
| 185 | +trap '_cleanup' EXIT | |
| 186 | +trap 'forward_signal TERM' TERM | |
| 187 | +trap 'forward_signal INT' INT | |
| 188 | +trap 'forward_signal HUP' HUP | |
| 189 | + | |
| 190 | +"$CLAUDE_BIN" "$@" & | |
| 191 | +child_pid=$! | |
| 192 | +wait "$child_pid" | |
| 193 | +status=$? | |
| 194 | +child_pid="" | |
| 195 | + | |
| 196 | +exit "$status" |
| --- a/skills/scuttlebot-relay/scripts/claude-relay.sh | |
| +++ b/skills/scuttlebot-relay/scripts/claude-relay.sh | |
| @@ -0,0 +1,196 @@ | |
| --- a/skills/scuttlebot-relay/scripts/claude-relay.sh | |
| +++ b/skills/scuttlebot-relay/scripts/claude-relay.sh | |
| @@ -0,0 +1,196 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Launch Claude with a fleet-style session nick. |
| 3 | # Registers a claude-{project}-{session} nick, starts the IRC agent in the |
| 4 | # background under that nick (so hook activity and IRC responses share one |
| 5 | # identity), then runs the Claude CLI. Deregisters on exit. |
| 6 | |
| 7 | set -u |
| 8 | |
| 9 | SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}" |
| 10 | if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then |
| 11 | set -a |
| 12 | . "$SCUTTLEBOT_CONFIG_FILE" |
| 13 | set +a |
| 14 | fi |
| 15 | |
| 16 | SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}" |
| 17 | SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN:-}" |
| 18 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}" |
| 19 | SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}" |
| 20 | SCUTTLEBOT_IRC="${SCUTTLEBOT_IRC:-127.0.0.1:6667}" |
| 21 | SCUTTLEBOT_BACKEND="${SCUTTLEBOT_BACKEND:-anthro}" |
| 22 | CLAUDE_AGENT_BIN="${CLAUDE_AGENT_BIN:-}" |
| 23 | CLAUDE_BIN="${CLAUDE_BIN:-claude}" |
| 24 | |
| 25 | sanitize() { |
| 26 | local input="$1" |
| 27 | if [ -z "$input" ]; then |
| 28 | input=$(cat) |
| 29 | fi |
| 30 | printf '%s' "$input" | tr -cs '[:alnum:]_-' '-' |
| 31 | } |
| 32 | |
| 33 | target_cwd() { |
| 34 | local cwd="$PWD" |
| 35 | local prev="" |
| 36 | local arg |
| 37 | for arg in "$@"; do |
| 38 | if [ "$prev" = "-C" ] || [ "$prev" = "--cd" ]; then |
| 39 | cwd="$arg" |
| 40 | prev="" |
| 41 | continue |
| 42 | fi |
| 43 | case "$arg" in |
| 44 | -C|--cd) |
| 45 | prev="$arg" |
| 46 | ;; |
| 47 | -C=*|--cd=*) |
| 48 | cwd="${arg#*=}" |
| 49 | ;; |
| 50 | esac |
| 51 | done |
| 52 | if [ -d "$cwd" ]; then |
| 53 | (cd "$cwd" && pwd) |
| 54 | else |
| 55 | printf '%s\n' "$PWD" |
| 56 | fi |
| 57 | } |
| 58 | |
| 59 | hooks_enabled() { |
| 60 | [ "$SCUTTLEBOT_HOOKS_ENABLED" != "0" ] && |
| 61 | [ "$SCUTTLEBOT_HOOKS_ENABLED" != "false" ] && |
| 62 | [ -n "$SCUTTLEBOT_TOKEN" ] |
| 63 | } |
| 64 | |
| 65 | post_status() { |
| 66 | local text="$1" |
| 67 | hooks_enabled || return 0 |
| 68 | command -v curl >/dev/null 2>&1 || return 0 |
| 69 | command -v jq >/dev/null 2>&1 || return 0 |
| 70 | curl -sf -X POST "$SCUTTLEBOT_URL/v1/channels/$SCUTTLEBOT_CHANNEL/messages" \ |
| 71 | --connect-timeout 1 \ |
| 72 | --max-time 2 \ |
| 73 | -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
| 74 | -H "Content-Type: application/json" \ |
| 75 | -d "{\"text\": $(printf '%s' "$text" | jq -Rs .), \"nick\": \"$SCUTTLEBOT_NICK\"}" \ |
| 76 | > /dev/null || true |
| 77 | } |
| 78 | |
| 79 | if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then |
| 80 | printf 'claude-relay: %s not found in PATH\n' "$CLAUDE_BIN" >&2 |
| 81 | exit 127 |
| 82 | fi |
| 83 | |
| 84 | TARGET_CWD=$(target_cwd "$@") |
| 85 | BASE_NAME=$(sanitize "$(basename "$TARGET_CWD")") |
| 86 | |
| 87 | if [ -z "${SCUTTLEBOT_SESSION_ID:-}" ]; then |
| 88 | SCUTTLEBOT_SESSION_ID=$( |
| 89 | printf '%s' "$TARGET_CWD|$$|$PPID|$(date +%s)" | cksum | awk '{print $1}' | cut -c 1-8 |
| 90 | ) |
| 91 | fi |
| 92 | SCUTTLEBOT_SESSION_ID=$(sanitize "$SCUTTLEBOT_SESSION_ID") |
| 93 | if [ -z "${SCUTTLEBOT_NICK:-}" ]; then |
| 94 | SCUTTLEBOT_NICK="claude-${BASE_NAME}-${SCUTTLEBOT_SESSION_ID}" |
| 95 | fi |
| 96 | SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL#\#}" |
| 97 | |
| 98 | export SCUTTLEBOT_CONFIG_FILE |
| 99 | export SCUTTLEBOT_URL |
| 100 | export SCUTTLEBOT_TOKEN |
| 101 | export SCUTTLEBOT_CHANNEL |
| 102 | export SCUTTLEBOT_HOOKS_ENABLED |
| 103 | export SCUTTLEBOT_SESSION_ID |
| 104 | export SCUTTLEBOT_NICK |
| 105 | |
| 106 | printf 'claude-relay: nick %s\n' "$SCUTTLEBOT_NICK" >&2 |
| 107 | |
| 108 | # --- IRC agent: register nick and start in background --- |
| 109 | irc_agent_pid="" |
| 110 | irc_agent_nick="" |
| 111 | |
| 112 | _start_irc_agent() { |
| 113 | [ -n "$SCUTTLEBOT_TOKEN" ] || return 0 |
| 114 | |
| 115 | # Find the claude-agent binary: next to this script, in PATH, or skip. |
| 116 | local bin="$CLAUDE_AGENT_BIN" |
| 117 | if [ -z "$bin" ]; then |
| 118 | local script_dir; script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 119 | local repo_root; repo_root=$(CDPATH= cd -- "$script_dir/../../.." && pwd) |
| 120 | if [ -x "$repo_root/bin/claude-agent" ]; then |
| 121 | bin="$repo_root/bin/claude-agent" |
| 122 | elif command -v claude-agent >/dev/null 2>&1; then |
| 123 | bin="claude-agent" |
| 124 | else |
| 125 | printf 'claude-relay: claude-agent not found, IRC responses disabled\n' >&2 |
| 126 | return 0 |
| 127 | fi |
| 128 | fi |
| 129 | |
| 130 | local resp; resp=$(curl -sf -X POST \ |
| 131 | --connect-timeout 2 --max-time 5 \ |
| 132 | -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
| 133 | -H "Content-Type: application/json" \ |
| 134 | -d "{\"nick\":\"$SCUTTLEBOT_NICK\",\"type\":\"worker\",\"channels\":[\"#$SCUTTLEBOT_CHANNEL\"]}" \ |
| 135 | "$SCUTTLEBOT_URL/v1/agents/register" 2>/dev/null) || return 0 |
| 136 | |
| 137 | local pass; pass=$(printf '%s' "$resp" | grep -o '"passphrase":"[^"]*"' | cut -d'"' -f4) |
| 138 | [ -n "$pass" ] || return 0 |
| 139 | |
| 140 | irc_agent_nick="$SCUTTLEBOT_NICK" |
| 141 | "$bin" \ |
| 142 | --irc "$SCUTTLEBOT_IRC" \ |
| 143 | --nick "$irc_agent_nick" \ |
| 144 | --pass "$pass" \ |
| 145 | --channels "#$SCUTTLEBOT_CHANNEL" \ |
| 146 | --api-url "$SCUTTLEBOT_URL" \ |
| 147 | --token "$SCUTTLEBOT_TOKEN" \ |
| 148 | --backend "$SCUTTLEBOT_BACKEND" \ |
| 149 | 2>/dev/null & |
| 150 | irc_agent_pid=$! |
| 151 | printf 'claude-relay: IRC agent started (pid %s)\n' "$irc_agent_pid" >&2 |
| 152 | } |
| 153 | |
| 154 | _stop_irc_agent() { |
| 155 | if [ -n "$irc_agent_pid" ]; then |
| 156 | kill "$irc_agent_pid" 2>/dev/null || true |
| 157 | irc_agent_pid="" |
| 158 | fi |
| 159 | if [ -n "$irc_agent_nick" ] && [ -n "$SCUTTLEBOT_TOKEN" ]; then |
| 160 | curl -sf -X DELETE \ |
| 161 | --connect-timeout 2 --max-time 5 \ |
| 162 | -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \ |
| 163 | "$SCUTTLEBOT_URL/v1/agents/$irc_agent_nick" >/dev/null 2>&1 || true |
| 164 | irc_agent_nick="" |
| 165 | fi |
| 166 | } |
| 167 | |
| 168 | _start_irc_agent |
| 169 | |
| 170 | # --- Claude CLI --- |
| 171 | post_status "online in $(basename "$TARGET_CWD"); mention $SCUTTLEBOT_NICK to interrupt" |
| 172 | |
| 173 | child_pid="" |
| 174 | _cleanup() { |
| 175 | [ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null || true |
| 176 | _stop_irc_agent |
| 177 | post_status "offline" |
| 178 | } |
| 179 | |
| 180 | forward_signal() { |
| 181 | local signal="$1" |
| 182 | [ -n "$child_pid" ] && kill "-$signal" "$child_pid" 2>/dev/null || true |
| 183 | } |
| 184 | |
| 185 | trap '_cleanup' EXIT |
| 186 | trap 'forward_signal TERM' TERM |
| 187 | trap 'forward_signal INT' INT |
| 188 | trap 'forward_signal HUP' HUP |
| 189 | |
| 190 | "$CLAUDE_BIN" "$@" & |
| 191 | child_pid=$! |
| 192 | wait "$child_pid" |
| 193 | status=$? |
| 194 | child_pid="" |
| 195 | |
| 196 | exit "$status" |
| --- a/skills/scuttlebot-relay/scripts/install-claude-relay.sh | ||
| +++ b/skills/scuttlebot-relay/scripts/install-claude-relay.sh | ||
| @@ -0,0 +1,123 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Install the tracked Claude relay hooks plus binary launcher into a local setup. | |
| 3 | + | |
| 4 | +set -euo pipefail | |
| 5 | + | |
| 6 | +usage() { | |
| 7 | + cat <<'EOF' | |
| 8 | +Usage: | |
| 9 | + bash skills/scuttlebot-relay/scripts/install-claude-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 | + --enabled ED=1. Default. | |
| 16 | + --1. Default. | |
| 17 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 18 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 19 | + --hooks-dir PATH Claude hooks install dir. Default: ~/.claude/hooks | |
| 20 | + --settings-json PATH Claude settings JSON. Default: ~/.claude/settings.json | |
| 21 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 22 | + --help Show this help. | |
| 23 | + | |
| 24 | +Environment defaults: | |
| 25 | + SCUTTLEBOT_URL | |
| 26 | + SCUTTLEBOT_TOKEN | |
| 27 | + SCUTTLEBOT_CHANNEL | |
| 28 | + SCUTTLEBOT_HOOKS_ENABLED | |
| 29 | + SCUTTLEBOT_CONFIG_FILE | |
| 30 | + CLAUDE_HOOKS_DIR | |
| 31 | + CLAUDE_SETTINGS_JSON | |
| 32 | + CLAUDE_BIN_DIR | |
| 33 | + | |
| 34 | +Examples: | |
| 35 | + bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \ | |
| 36 | + --url http://localhost:8080 \ | |
| 37 | + --token "$(./run.sh token)" \ | |
| 38 | + --channel general | |
| 39 | + | |
| 40 | + SCUTTLEBOT_HOOKS_ENABLED=0 make install-claude-relay | |
| 41 | +EOF | |
| 42 | +} | |
| 43 | + | |
| 44 | +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) | |
| 45 | +REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) | |
| 46 | + | |
| 47 | +SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" | |
| 48 | +SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" | |
| 49 | +SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEocal setup. | |
| 50 | + | |
| 51 | +set -eu"${:-1ATH Claude settings JSON. Default: ~/.claude/settings.json | |
| 52 | + --bin-dir PATH Launcher install dir. Default: ~/.local/bin | |
| 53 | + --help Show this help. | |
| 54 | + | |
| 55 | +Environment defaults: | |
| 56 | + SCUTTLEBOT_URL | |
| 57 | + SCUTTLEBOT_TOKEN | |
| 58 | + SCUTTLEBOT_CHANNEL | |
| 59 | + SCUTTLEBOT_TRANSPORT | |
| 60 | + SCUTTLEBOT_IRC_ADDR | |
| 61 | + SCUTTLEBOT_IRC_PASS | |
| 62 | + SCUTTLEBOT_IRC_DELETE_ON_CLOSE | |
| 63 | + SCUTTLEBOT_HOOKS_ENABLED | |
| 64 | + SCUTTLEBOT_INTERRUPT_ON_MESSAGE | |
| 65 | + SCUTTLEBOT_POLL_INTERVAL | |
| 66 | + SCUTTLEBOT_PRESENCE_HEARTBEAT | |
| 67 | + SCUTTLEBOT_CONFIG_FILE | |
| 68 | + CLAUDE_HOOKS_DIR | |
| 69 | + CLAUDE_SETTINGS_JSON | |
| 70 | + CLAUDE_BIN_DIR | |
| 71 | + | |
| 72 | +Examples: | |
| 73 | + bash skills/scutbash | |
| 74 | +# Install the tracked Claude relay hooks plus binary launcher into a local setup. | |
| 75 | + | |
| 76 | +set -euo pipefail | |
| 77 | + | |
| 78 | +usage() { | |
| 79 | + cat <<'EOF' | |
| 80 | +Usage: | |
| 81 | + bash skills/scuttlebot-relay/scripts/install-claude-relay.sh [options] | |
| 82 | + | |
| 83 | +Options: | |
| 84 | + --url URL Set SCUTTLEBOT_URL in the shared env file. | |
| 85 | + --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. | |
| 86 | + --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. | |
| 87 | + --channels CSV Set SCUTTLEBOT_CHANNELS in the shared env file. | |
| 88 | + --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: irc. | |
| 89 | + --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. | |
| 90 | + --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. | |
| 91 | + --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. | |
| 92 | + --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. | |
| 93 | + --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. | |
| 94 | + --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env | |
| 95 | + --hooks-dir PATH Claude hooks install dir. Default: ~/.claude/hooks | |
| 96 | + --settings-json PATH Claude settings JSON. Default: ~/.clauash | |
| 97 | +# Install the tracked Claude relay hooks plus binary launcher into a local setup. | |
| 98 | + | |
| 99 | +set -euo pipefail | |
| 100 | + | |
| 101 | +usage() { | |
| 102 | + cat <<'EOF' | |
| 103 | +Usage: | |
| 104 | + bash skills/scuttlebot-relay/scripts/install-claude-relay.sh [options] | |
| 105 | + | |
| 106 | +Options: | |
| 107 | + --url URL Set SCUTTLEBOT_URL in the shared env_INTERVAL_VALUE" | |
| 108 | +upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_PRESENCE_HEARTBEAT "$SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE" | |
| 109 | + | |
| 110 | +printf 'Installed Claude relay files:\n' | |
| 111 | +printf ' hooks: %s\n' "$HOOKS_DIR" | |
| 112 | +printf ' settings: %s\n' "$SETTINGS_JSON" | |
| 113 | +printf ' launcher: %s\n' "$LAUNCHER_DST" | |
| 114 | +printf ' env: %s\n' "$CONFIG_FILE" | |
| 115 | +printf ' irc auth: %s\n' "$([ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ] && printf 'fixed-pass override' || printf 'auto-register')" | |
| 116 | +printf '\n' | |
| 117 | +printf 'Next steps:\n' | |
| 118 | +printf ' 1. Launch with: %s\n' "$LAUNCHER_DST" | |
| 119 | +printf ' 2. Watch IRC for: claude-{repo}-{session}\n' | |
| 120 | +printf ' 3. Mention that nick to interrupt before the next action\n' | |
| 121 | +printf '\n' | |
| 122 | +printf 'Disable without uninstalling:\n' | |
| 123 | +printf ' SCUTTLEBOT_HOOKS_ENABL |
| --- a/skills/scuttlebot-relay/scripts/install-claude-relay.sh | |
| +++ b/skills/scuttlebot-relay/scripts/install-claude-relay.sh | |
| @@ -0,0 +1,123 @@ | |
| --- a/skills/scuttlebot-relay/scripts/install-claude-relay.sh | |
| +++ b/skills/scuttlebot-relay/scripts/install-claude-relay.sh | |
| @@ -0,0 +1,123 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Install the tracked Claude relay hooks plus binary launcher into a local setup. |
| 3 | |
| 4 | set -euo pipefail |
| 5 | |
| 6 | usage() { |
| 7 | cat <<'EOF' |
| 8 | Usage: |
| 9 | bash skills/scuttlebot-relay/scripts/install-claude-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 | --enabled ED=1. Default. |
| 16 | --1. Default. |
| 17 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 18 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 19 | --hooks-dir PATH Claude hooks install dir. Default: ~/.claude/hooks |
| 20 | --settings-json PATH Claude settings JSON. Default: ~/.claude/settings.json |
| 21 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 22 | --help Show this help. |
| 23 | |
| 24 | Environment defaults: |
| 25 | SCUTTLEBOT_URL |
| 26 | SCUTTLEBOT_TOKEN |
| 27 | SCUTTLEBOT_CHANNEL |
| 28 | SCUTTLEBOT_HOOKS_ENABLED |
| 29 | SCUTTLEBOT_CONFIG_FILE |
| 30 | CLAUDE_HOOKS_DIR |
| 31 | CLAUDE_SETTINGS_JSON |
| 32 | CLAUDE_BIN_DIR |
| 33 | |
| 34 | Examples: |
| 35 | bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \ |
| 36 | --url http://localhost:8080 \ |
| 37 | --token "$(./run.sh token)" \ |
| 38 | --channel general |
| 39 | |
| 40 | SCUTTLEBOT_HOOKS_ENABLED=0 make install-claude-relay |
| 41 | EOF |
| 42 | } |
| 43 | |
| 44 | SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
| 45 | REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd) |
| 46 | |
| 47 | SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}" |
| 48 | SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}" |
| 49 | SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEocal setup. |
| 50 | |
| 51 | set -eu"${:-1ATH Claude settings JSON. Default: ~/.claude/settings.json |
| 52 | --bin-dir PATH Launcher install dir. Default: ~/.local/bin |
| 53 | --help Show this help. |
| 54 | |
| 55 | Environment defaults: |
| 56 | SCUTTLEBOT_URL |
| 57 | SCUTTLEBOT_TOKEN |
| 58 | SCUTTLEBOT_CHANNEL |
| 59 | SCUTTLEBOT_TRANSPORT |
| 60 | SCUTTLEBOT_IRC_ADDR |
| 61 | SCUTTLEBOT_IRC_PASS |
| 62 | SCUTTLEBOT_IRC_DELETE_ON_CLOSE |
| 63 | SCUTTLEBOT_HOOKS_ENABLED |
| 64 | SCUTTLEBOT_INTERRUPT_ON_MESSAGE |
| 65 | SCUTTLEBOT_POLL_INTERVAL |
| 66 | SCUTTLEBOT_PRESENCE_HEARTBEAT |
| 67 | SCUTTLEBOT_CONFIG_FILE |
| 68 | CLAUDE_HOOKS_DIR |
| 69 | CLAUDE_SETTINGS_JSON |
| 70 | CLAUDE_BIN_DIR |
| 71 | |
| 72 | Examples: |
| 73 | bash skills/scutbash |
| 74 | # Install the tracked Claude relay hooks plus binary launcher into a local setup. |
| 75 | |
| 76 | set -euo pipefail |
| 77 | |
| 78 | usage() { |
| 79 | cat <<'EOF' |
| 80 | Usage: |
| 81 | bash skills/scuttlebot-relay/scripts/install-claude-relay.sh [options] |
| 82 | |
| 83 | Options: |
| 84 | --url URL Set SCUTTLEBOT_URL in the shared env file. |
| 85 | --token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file. |
| 86 | --channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file. |
| 87 | --channels CSV Set SCUTTLEBOT_CHANNELS in the shared env file. |
| 88 | --transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: irc. |
| 89 | --irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667. |
| 90 | --irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode. |
| 91 | --auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default. |
| 92 | --enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default. |
| 93 | --disabled Write SCUTTLEBOT_HOOKS_ENABLED=0. |
| 94 | --config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env |
| 95 | --hooks-dir PATH Claude hooks install dir. Default: ~/.claude/hooks |
| 96 | --settings-json PATH Claude settings JSON. Default: ~/.clauash |
| 97 | # Install the tracked Claude relay hooks plus binary launcher into a local setup. |
| 98 | |
| 99 | set -euo pipefail |
| 100 | |
| 101 | usage() { |
| 102 | cat <<'EOF' |
| 103 | Usage: |
| 104 | bash skills/scuttlebot-relay/scripts/install-claude-relay.sh [options] |
| 105 | |
| 106 | Options: |
| 107 | --url URL Set SCUTTLEBOT_URL in the shared env_INTERVAL_VALUE" |
| 108 | upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_PRESENCE_HEARTBEAT "$SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE" |
| 109 | |
| 110 | printf 'Installed Claude relay files:\n' |
| 111 | printf ' hooks: %s\n' "$HOOKS_DIR" |
| 112 | printf ' settings: %s\n' "$SETTINGS_JSON" |
| 113 | printf ' launcher: %s\n' "$LAUNCHER_DST" |
| 114 | printf ' env: %s\n' "$CONFIG_FILE" |
| 115 | printf ' irc auth: %s\n' "$([ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ] && printf 'fixed-pass override' || printf 'auto-register')" |
| 116 | printf '\n' |
| 117 | printf 'Next steps:\n' |
| 118 | printf ' 1. Launch with: %s\n' "$LAUNCHER_DST" |
| 119 | printf ' 2. Watch IRC for: claude-{repo}-{session}\n' |
| 120 | printf ' 3. Mention that nick to interrupt before the next action\n' |
| 121 | printf '\n' |
| 122 | printf 'Disable without uninstalling:\n' |
| 123 | printf ' SCUTTLEBOT_HOOKS_ENABL |
| --- a/tests/smoke/test-installers.sh | ||
| +++ b/tests/smoke/test-installers.sh | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Smoke test for scuttlebot relay installers. | |
| 3 | + | |
| 4 | +set -euo pipefail | |
| 5 | + | |
| 6 | +REPO_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) | |
| 7 | +TEMP_HOME=$(mktemp -d) | |
| 8 | +export HOME="$TEMP_HOME" | |
| 9 | +export SCUTTLEBOT_CONFIG_FILE="$HOME/.config/scuttlebot-relay.env" | |
| 10 | +export CODEX_HOOKS_DIR="$HOME/.codex/hooks" | |
| 11 | +export CODEX_HOOKS_JSON="$HOME/.codex/hooks.json" | |
| 12 | +export CODEX_CONFIG_TOML="$HOME/.codex/config.toml" | |
| 13 | +export CODEX_BIN_DIR="$HOME/.local/bin" | |
| 14 | +export GEMINI_HOOKS_DIR="$HOME/.gemini/hooks" | |
| 15 | +export GEMINI_SETTINGS_JSON="$HOME/.gemini/settings.json" | |
| 16 | +export GEMINI_BIN_DIR="$HOME/.local/bin" | |
| 17 | +export CLAUDE_HOOKS_DIR="$HOME/.claude/hooks" | |
| 18 | +export CLAUDE |
| --- a/tests/smoke/test-installers.sh | |
| +++ b/tests/smoke/test-installers.sh | |
| @@ -0,0 +1,18 @@ | |
| --- a/tests/smoke/test-installers.sh | |
| +++ b/tests/smoke/test-installers.sh | |
| @@ -0,0 +1,18 @@ | |
| 1 | #!/usr/bin/env bash |
| 2 | # Smoke test for scuttlebot relay installers. |
| 3 | |
| 4 | set -euo pipefail |
| 5 | |
| 6 | REPO_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) |
| 7 | TEMP_HOME=$(mktemp -d) |
| 8 | export HOME="$TEMP_HOME" |
| 9 | export SCUTTLEBOT_CONFIG_FILE="$HOME/.config/scuttlebot-relay.env" |
| 10 | export CODEX_HOOKS_DIR="$HOME/.codex/hooks" |
| 11 | export CODEX_HOOKS_JSON="$HOME/.codex/hooks.json" |
| 12 | export CODEX_CONFIG_TOML="$HOME/.codex/config.toml" |
| 13 | export CODEX_BIN_DIR="$HOME/.local/bin" |
| 14 | export GEMINI_HOOKS_DIR="$HOME/.gemini/hooks" |
| 15 | export GEMINI_SETTINGS_JSON="$HOME/.gemini/settings.json" |
| 16 | export GEMINI_BIN_DIR="$HOME/.local/bin" |
| 17 | export CLAUDE_HOOKS_DIR="$HOME/.claude/hooks" |
| 18 | export CLAUDE |