ScuttleBot

Add Claude and Gemini relay brokers and fleet tooling

lmata 2026-04-01 13:18 trunk
Commit 016a29f42f463c2e9cb42974d1c38ff863c8d61e186d69e22be45113553a66a2
+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
22
33
build:
44
go build ./...
55
66
test:
77
go test ./...
88
9
+test-smoke:
10
+ bash tests/smoke/test-installers.sh
11
+
912
lint:
1013
golangci-lint run
1114
1215
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
1426
1527
bin/scuttlebot:
1628
go build -o bin/scuttlebot ./cmd/scuttlebot
1729
1830
bin/scuttlectl:
1931
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
2053
--- 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 @@
5151
- **[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.
5252
- **Claude Code / Gemini / Codex fleets** — multiple coding agents working on the same project, sharing context in real time
5353
- **Ops and monitoring agents** — agents watching infrastructure, triaging alerts, escalating to humans — all visible in a single IRC channel
5454
- **Any multi-agent system** where humans need to see what's happening without a custom dashboard
5555
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
+
5668
---
5769
5870
## How it works
5971
6072
scuttlebot manages an [Ergo](https://ergo.chat) IRC server. Users configure scuttlebot — never Ergo directly.
6173
6274
ADDED cmd/claude-agent/main.go
6375
ADDED cmd/claude-relay/main.go
6476
ADDED cmd/claude-relay/main_test.go
6577
ADDED cmd/codex-agent/main.go
6678
ADDED cmd/fleet-cmd/main.go
6779
ADDED cmd/gemini-agent/main.go
6880
ADDED cmd/gemini-relay/main.go
6981
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
--- 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
--- 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 }
--- 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 =
--- 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 }
--- 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
--- 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
+```
250
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
+```
558
59
+These installers set up the interactive broker, PTY wrappers, and tool-use hooks automatically.
660
--- 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
--- 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
+```
235
3
-!!! note
4
- This page is a work in progress.
36
+## Security Model
537
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.
639
740
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.
--- 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
+```
253
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.
556
57
+```bash
58
+fleet-cmd broadcast "Emergency: All agents stop current tasks."
59
+```
660
--- 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 @@
7272
- Installation: getting-started/installation.md
7373
- Quick Start: getting-started/quickstart.md
7474
- Configuration: getting-started/configuration.md
7575
- Guide:
7676
- Agent Registration: guide/agent-registration.md
77
+ - Fleet Management: guide/fleet-management.md
7778
- Channel Topology: guide/topology.md
7879
- Built-in Bots: guide/bots.md
7980
- Discovery: guide/discovery.md
8081
- Deployment: guide/deployment.md
8182
- Architecture:
8283
8384
ADDED pkg/ircagent/ircagent.go
8485
ADDED pkg/ircagent/ircagent_test.go
8586
ADDED skills/gemini-relay/FLEET.md
8687
ADDED skills/gemini-relay/SKILL.md
8788
ADDED skills/gemini-relay/hooks/README.md
8889
ADDED skills/gemini-relay/hooks/scuttlebot-after-agent.sh
8990
ADDED skills/gemini-relay/hooks/scuttlebot-check.sh
9091
ADDED skills/gemini-relay/hooks/scuttlebot-post.sh
9192
ADDED skills/gemini-relay/install.md
9293
ADDED skills/gemini-relay/scripts/gemini-relay.sh
9394
ADDED skills/gemini-relay/scripts/install-gemini-relay.sh
9495
ADDED skills/scuttlebot-relay/FLEET.md
9596
ADDED skills/scuttlebot-relay/hooks/README.md
9697
ADDED skills/scuttlebot-relay/hooks/scuttlebot-check.sh
9798
ADDED skills/scuttlebot-relay/hooks/scuttlebot-post.sh
9899
ADDED skills/scuttlebot-relay/install.md
99100
ADDED skills/scuttlebot-relay/scripts/claude-relay.sh
100101
ADDED skills/scuttlebot-relay/scripts/install-claude-relay.sh
101102
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
--- 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 _,
--- 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
--- 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

Keyboard Shortcuts

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