|
5ac549c…
|
lmata
|
1 |
package main |
|
5ac549c…
|
lmata
|
2 |
|
|
5ac549c…
|
lmata
|
3 |
import ( |
|
5ac549c…
|
lmata
|
4 |
"bufio" |
|
5ac549c…
|
lmata
|
5 |
"fmt" |
|
5ac549c…
|
lmata
|
6 |
"os" |
|
5ac549c…
|
lmata
|
7 |
"strconv" |
|
5ac549c…
|
lmata
|
8 |
"strings" |
|
5ac549c…
|
lmata
|
9 |
|
|
5ac549c…
|
lmata
|
10 |
"gopkg.in/yaml.v3" |
|
5ac549c…
|
lmata
|
11 |
) |
|
5ac549c…
|
lmata
|
12 |
|
|
5ac549c…
|
lmata
|
13 |
// cmdSetup runs an interactive wizard that writes scuttlebot.yaml. |
|
5ac549c…
|
lmata
|
14 |
// It does not require a running server or an API token. |
|
5ac549c…
|
lmata
|
15 |
func cmdSetup(path string) { |
|
5ac549c…
|
lmata
|
16 |
s := newSetupScanner() |
|
5ac549c…
|
lmata
|
17 |
|
|
5ac549c…
|
lmata
|
18 |
fmt.Println() |
|
5ac549c…
|
lmata
|
19 |
fmt.Println(" scuttlebot setup wizard") |
|
5ac549c…
|
lmata
|
20 |
fmt.Println(" ─────────────────────────────────────────") |
|
5ac549c…
|
lmata
|
21 |
fmt.Println(" Answers in [brackets] are the default — press Enter to accept.") |
|
5ac549c…
|
lmata
|
22 |
fmt.Println() |
|
5ac549c…
|
lmata
|
23 |
|
|
5ac549c…
|
lmata
|
24 |
// Check for existing file. |
|
5ac549c…
|
lmata
|
25 |
if _, err := os.Stat(path); err == nil { |
|
5ac549c…
|
lmata
|
26 |
if !s.confirm(fmt.Sprintf(" %s already exists — overwrite?", path), false) { |
|
5ac549c…
|
lmata
|
27 |
fmt.Println(" Aborted.") |
|
5ac549c…
|
lmata
|
28 |
os.Exit(0) |
|
5ac549c…
|
lmata
|
29 |
} |
|
5ac549c…
|
lmata
|
30 |
} |
|
5ac549c…
|
lmata
|
31 |
|
|
5ac549c…
|
lmata
|
32 |
cfg := buildConfig(s) |
|
5ac549c…
|
lmata
|
33 |
|
|
5ac549c…
|
lmata
|
34 |
data, err := yaml.Marshal(cfg) |
|
5ac549c…
|
lmata
|
35 |
if err != nil { |
|
5ac549c…
|
lmata
|
36 |
fmt.Fprintln(os.Stderr, "error encoding yaml:", err) |
|
5ac549c…
|
lmata
|
37 |
os.Exit(1) |
|
5ac549c…
|
lmata
|
38 |
} |
|
5ac549c…
|
lmata
|
39 |
|
|
5ac549c…
|
lmata
|
40 |
if err := os.WriteFile(path, data, 0600); err != nil { |
|
5ac549c…
|
lmata
|
41 |
fmt.Fprintln(os.Stderr, "error writing config:", err) |
|
5ac549c…
|
lmata
|
42 |
os.Exit(1) |
|
5ac549c…
|
lmata
|
43 |
} |
|
5ac549c…
|
lmata
|
44 |
|
|
5ac549c…
|
lmata
|
45 |
fmt.Println() |
|
5ac549c…
|
lmata
|
46 |
fmt.Printf(" ✓ wrote %s\n", path) |
|
5ac549c…
|
lmata
|
47 |
fmt.Println() |
|
5ac549c…
|
lmata
|
48 |
fmt.Println(" Next steps:") |
|
5ac549c…
|
lmata
|
49 |
fmt.Println(" ./run.sh start # start scuttlebot") |
|
5ac549c…
|
lmata
|
50 |
fmt.Println(" ./run.sh token # print API token") |
|
5ac549c…
|
lmata
|
51 |
fmt.Println(" open http://localhost:8080/ui/") |
|
5ac549c…
|
lmata
|
52 |
fmt.Println() |
|
5ac549c…
|
lmata
|
53 |
} |
|
5ac549c…
|
lmata
|
54 |
|
|
5ac549c…
|
lmata
|
55 |
func buildConfig(s *setupScanner) map[string]any { |
|
5ac549c…
|
lmata
|
56 |
cfg := map[string]any{} |
|
5ac549c…
|
lmata
|
57 |
|
|
5ac549c…
|
lmata
|
58 |
// ── network ────────────────────────────────────────────────────────────── |
|
5ac549c…
|
lmata
|
59 |
printSection("IRC / network") |
|
5ac549c…
|
lmata
|
60 |
|
|
5ac549c…
|
lmata
|
61 |
networkName := s.ask(" IRC network name", "scuttlebot") |
|
0e244d2…
|
lmata
|
62 |
serverName := s.ask(" IRC server hostname", "irc.scuttlebot.local") |
|
0e244d2…
|
lmata
|
63 |
ircAddr := s.ask(" IRC listen address", "127.0.0.1:6667") |
|
0e244d2…
|
lmata
|
64 |
apiAddr := s.ask(" HTTP API listen address", ":8080") |
|
5ac549c…
|
lmata
|
65 |
|
|
5ac549c…
|
lmata
|
66 |
cfg["ergo"] = map[string]any{ |
|
5ac549c…
|
lmata
|
67 |
"network_name": networkName, |
|
5ac549c…
|
lmata
|
68 |
"server_name": serverName, |
|
5ac549c…
|
lmata
|
69 |
"irc_addr": ircAddr, |
|
5ac549c…
|
lmata
|
70 |
} |
|
5ac549c…
|
lmata
|
71 |
cfg["api_addr"] = apiAddr |
|
5ac549c…
|
lmata
|
72 |
|
|
5ac549c…
|
lmata
|
73 |
// ── TLS ────────────────────────────────────────────────────────────────── |
|
5ac549c…
|
lmata
|
74 |
printSection("TLS / HTTPS (skip for local/dev)") |
|
5ac549c…
|
lmata
|
75 |
|
|
5ac549c…
|
lmata
|
76 |
if s.confirm(" Enable Let's Encrypt TLS?", false) { |
|
5ac549c…
|
lmata
|
77 |
domain := s.ask(" Domain name", "") |
|
0e244d2…
|
lmata
|
78 |
email := s.ask(" Email for cert expiry notices", "") |
|
5ac549c…
|
lmata
|
79 |
cfg["tls"] = map[string]any{ |
|
5ac549c…
|
lmata
|
80 |
"domain": domain, |
|
5ac549c…
|
lmata
|
81 |
"email": email, |
|
5ac549c…
|
lmata
|
82 |
"allow_insecure": true, |
|
5ac549c…
|
lmata
|
83 |
} |
|
5ac549c…
|
lmata
|
84 |
} |
|
5ac549c…
|
lmata
|
85 |
|
|
5ac549c…
|
lmata
|
86 |
// ── bridge ─────────────────────────────────────────────────────────────── |
|
5ac549c…
|
lmata
|
87 |
printSection("web chat bridge") |
|
5ac549c…
|
lmata
|
88 |
|
|
5ac549c…
|
lmata
|
89 |
channels := s.ask(" Default channels (comma-separated)", "#general") |
|
5ac549c…
|
lmata
|
90 |
chList := splitComma(channels) |
|
5ac549c…
|
lmata
|
91 |
cfg["bridge"] = map[string]any{ |
|
5ac549c…
|
lmata
|
92 |
"enabled": true, |
|
5ac549c…
|
lmata
|
93 |
"channels": chList, |
|
5ac549c…
|
lmata
|
94 |
"buffer_size": 200, |
|
5ac549c…
|
lmata
|
95 |
} |
|
5ac549c…
|
lmata
|
96 |
|
|
5ac549c…
|
lmata
|
97 |
// ── LLM backends ───────────────────────────────────────────────────────── |
|
5ac549c…
|
lmata
|
98 |
printSection("LLM backends (for oracle summarisation)") |
|
5ac549c…
|
lmata
|
99 |
|
|
5ac549c…
|
lmata
|
100 |
var backends []map[string]any |
|
5ac549c…
|
lmata
|
101 |
for { |
|
5ac549c…
|
lmata
|
102 |
if !s.confirm(" Add an LLM backend?", len(backends) == 0) { |
|
5ac549c…
|
lmata
|
103 |
break |
|
5ac549c…
|
lmata
|
104 |
} |
|
5ac549c…
|
lmata
|
105 |
b := buildBackend(s) |
|
5ac549c…
|
lmata
|
106 |
backends = append(backends, b) |
|
5ac549c…
|
lmata
|
107 |
} |
|
5ac549c…
|
lmata
|
108 |
if len(backends) > 0 { |
|
5ac549c…
|
lmata
|
109 |
cfg["llm"] = map[string]any{"backends": backends} |
|
5ac549c…
|
lmata
|
110 |
} |
|
5ac549c…
|
lmata
|
111 |
|
|
5ac549c…
|
lmata
|
112 |
// ── logging ─────────────────────────────────────────────────────────────── |
|
5ac549c…
|
lmata
|
113 |
printSection("message logging (scribe bot)") |
|
5ac549c…
|
lmata
|
114 |
|
|
5ac549c…
|
lmata
|
115 |
if s.confirm(" Enable scribe message logging?", true) { |
|
0e244d2…
|
lmata
|
116 |
logDir := s.ask(" Log directory", "./data/logs/scribe") |
|
0e244d2…
|
lmata
|
117 |
format := s.choice(" Format", []string{"jsonl", "csv", "text"}, "jsonl") |
|
5ac549c…
|
lmata
|
118 |
rotatef := s.choice(" Rotation", []string{"none", "daily", "weekly", "monthly", "size"}, "daily") |
|
5ac549c…
|
lmata
|
119 |
// Stored as scribe bot policy — just print a note, actual policy is in policies.json |
|
0e244d2…
|
lmata
|
120 |
_ = logDir |
|
0e244d2…
|
lmata
|
121 |
_ = format |
|
0e244d2…
|
lmata
|
122 |
_ = rotatef |
|
5ac549c…
|
lmata
|
123 |
fmt.Printf("\n Note: scribe is enabled via the web UI (settings → system behaviors).\n") |
|
5ac549c…
|
lmata
|
124 |
fmt.Printf(" Set dir=%s format=%s rotation=%s in oracle's behavior config.\n\n", logDir, format, rotatef) |
|
5ac549c…
|
lmata
|
125 |
} |
|
5ac549c…
|
lmata
|
126 |
|
|
5ac549c…
|
lmata
|
127 |
return cfg |
|
5ac549c…
|
lmata
|
128 |
} |
|
5ac549c…
|
lmata
|
129 |
|
|
5ac549c…
|
lmata
|
130 |
func buildBackend(s *setupScanner) map[string]any { |
|
5ac549c…
|
lmata
|
131 |
backends := []string{ |
|
5ac549c…
|
lmata
|
132 |
"openai", "anthropic", "gemini", "bedrock", "ollama", |
|
5ac549c…
|
lmata
|
133 |
"openrouter", "groq", "together", "fireworks", "mistral", |
|
5ac549c…
|
lmata
|
134 |
"deepseek", "xai", "cerebras", "litellm", "lmstudio", "vllm", "localai", |
|
5ac549c…
|
lmata
|
135 |
} |
|
5ac549c…
|
lmata
|
136 |
backendType := s.choice(" Backend type", backends, "openai") |
|
5ac549c…
|
lmata
|
137 |
name := s.ask(" Backend name (identifier)", backendType+"-1") |
|
5ac549c…
|
lmata
|
138 |
|
|
5ac549c…
|
lmata
|
139 |
b := map[string]any{ |
|
5ac549c…
|
lmata
|
140 |
"name": name, |
|
5ac549c…
|
lmata
|
141 |
"backend": backendType, |
|
5ac549c…
|
lmata
|
142 |
} |
|
5ac549c…
|
lmata
|
143 |
|
|
5ac549c…
|
lmata
|
144 |
switch backendType { |
|
5ac549c…
|
lmata
|
145 |
case "bedrock": |
|
5ac549c…
|
lmata
|
146 |
b["region"] = s.ask(" AWS region", "us-east-1") |
|
5ac549c…
|
lmata
|
147 |
if s.confirm(" Use static AWS credentials? (No = IAM role auto-detected)", false) { |
|
0e244d2…
|
lmata
|
148 |
b["aws_key_id"] = s.ask(" AWS access key ID", "") |
|
5ac549c…
|
lmata
|
149 |
b["aws_secret_key"] = s.ask(" AWS secret access key", "") |
|
5ac549c…
|
lmata
|
150 |
} else { |
|
5ac549c…
|
lmata
|
151 |
fmt.Println(" → credentials will be resolved from env vars or instance/task role") |
|
5ac549c…
|
lmata
|
152 |
} |
|
5ac549c…
|
lmata
|
153 |
b["model"] = s.ask(" Default model", "anthropic.claude-3-5-sonnet-20241022-v2:0") |
|
5ac549c…
|
lmata
|
154 |
|
|
5ac549c…
|
lmata
|
155 |
case "ollama": |
|
5ac549c…
|
lmata
|
156 |
b["base_url"] = s.ask(" Ollama base URL", "http://localhost:11434") |
|
0e244d2…
|
lmata
|
157 |
b["model"] = s.ask(" Default model", "llama3.2") |
|
5ac549c…
|
lmata
|
158 |
|
|
5ac549c…
|
lmata
|
159 |
case "anthropic": |
|
5ac549c…
|
lmata
|
160 |
b["api_key"] = s.secret(" API key") |
|
0e244d2…
|
lmata
|
161 |
b["model"] = s.ask(" Default model", "claude-3-5-sonnet-20241022") |
|
5ac549c…
|
lmata
|
162 |
|
|
5ac549c…
|
lmata
|
163 |
case "gemini": |
|
5ac549c…
|
lmata
|
164 |
b["api_key"] = s.secret(" API key") |
|
0e244d2…
|
lmata
|
165 |
b["model"] = s.ask(" Default model", "gemini-1.5-flash") |
|
5ac549c…
|
lmata
|
166 |
|
|
5ac549c…
|
lmata
|
167 |
default: |
|
5ac549c…
|
lmata
|
168 |
b["api_key"] = s.secret(" API key") |
|
0e244d2…
|
lmata
|
169 |
b["model"] = s.ask(" Default model", defaultModelFor(backendType)) |
|
5ac549c…
|
lmata
|
170 |
} |
|
5ac549c…
|
lmata
|
171 |
|
|
5ac549c…
|
lmata
|
172 |
if s.confirm(" Add model allow/block regex filters?", false) { |
|
5ac549c…
|
lmata
|
173 |
allow := s.ask(" Allow patterns (comma-separated regex)", "") |
|
5ac549c…
|
lmata
|
174 |
block := s.ask(" Block patterns (comma-separated regex)", "") |
|
5ac549c…
|
lmata
|
175 |
if allow != "" { |
|
5ac549c…
|
lmata
|
176 |
b["allow"] = splitComma(allow) |
|
5ac549c…
|
lmata
|
177 |
} |
|
5ac549c…
|
lmata
|
178 |
if block != "" { |
|
5ac549c…
|
lmata
|
179 |
b["block"] = splitComma(block) |
|
5ac549c…
|
lmata
|
180 |
} |
|
5ac549c…
|
lmata
|
181 |
} |
|
5ac549c…
|
lmata
|
182 |
|
|
5ac549c…
|
lmata
|
183 |
if s.confirm(" Mark as default backend?", len([]map[string]any{b}) == 0) { |
|
5ac549c…
|
lmata
|
184 |
b["default"] = true |
|
5ac549c…
|
lmata
|
185 |
} |
|
5ac549c…
|
lmata
|
186 |
|
|
5ac549c…
|
lmata
|
187 |
return b |
|
5ac549c…
|
lmata
|
188 |
} |
|
5ac549c…
|
lmata
|
189 |
|
|
5ac549c…
|
lmata
|
190 |
func defaultModelFor(backend string) string { |
|
5ac549c…
|
lmata
|
191 |
defaults := map[string]string{ |
|
0e244d2…
|
lmata
|
192 |
"openai": "gpt-4o-mini", |
|
0e244d2…
|
lmata
|
193 |
"openrouter": "openai/gpt-4o-mini", |
|
0e244d2…
|
lmata
|
194 |
"groq": "llama-3.3-70b-versatile", |
|
0e244d2…
|
lmata
|
195 |
"together": "meta-llama/Llama-3.3-70B-Instruct-Turbo", |
|
0e244d2…
|
lmata
|
196 |
"fireworks": "accounts/fireworks/models/llama-v3p3-70b-instruct", |
|
0e244d2…
|
lmata
|
197 |
"mistral": "mistral-large-latest", |
|
0e244d2…
|
lmata
|
198 |
"deepseek": "deepseek-chat", |
|
0e244d2…
|
lmata
|
199 |
"xai": "grok-2", |
|
0e244d2…
|
lmata
|
200 |
"cerebras": "llama3.3-70b", |
|
0e244d2…
|
lmata
|
201 |
"litellm": "", |
|
0e244d2…
|
lmata
|
202 |
"lmstudio": "", |
|
0e244d2…
|
lmata
|
203 |
"vllm": "", |
|
0e244d2…
|
lmata
|
204 |
"localai": "", |
|
5ac549c…
|
lmata
|
205 |
} |
|
5ac549c…
|
lmata
|
206 |
if m, ok := defaults[backend]; ok { |
|
5ac549c…
|
lmata
|
207 |
return m |
|
5ac549c…
|
lmata
|
208 |
} |
|
5ac549c…
|
lmata
|
209 |
return "" |
|
5ac549c…
|
lmata
|
210 |
} |
|
5ac549c…
|
lmata
|
211 |
|
|
5ac549c…
|
lmata
|
212 |
func printSection(title string) { |
|
5ac549c…
|
lmata
|
213 |
fmt.Printf("\n ── %s\n\n", title) |
|
5ac549c…
|
lmata
|
214 |
} |
|
5ac549c…
|
lmata
|
215 |
|
|
5ac549c…
|
lmata
|
216 |
// setupScanner wraps a line reader with prompt helpers. |
|
5ac549c…
|
lmata
|
217 |
type setupScanner struct { |
|
5ac549c…
|
lmata
|
218 |
scanner *bufio.Scanner |
|
5ac549c…
|
lmata
|
219 |
} |
|
5ac549c…
|
lmata
|
220 |
|
|
5ac549c…
|
lmata
|
221 |
func newSetupScanner() *setupScanner { |
|
5ac549c…
|
lmata
|
222 |
return &setupScanner{scanner: bufio.NewScanner(os.Stdin)} |
|
5ac549c…
|
lmata
|
223 |
} |
|
5ac549c…
|
lmata
|
224 |
|
|
5ac549c…
|
lmata
|
225 |
func (s *setupScanner) readLine() string { |
|
5ac549c…
|
lmata
|
226 |
if s.scanner.Scan() { |
|
5ac549c…
|
lmata
|
227 |
return strings.TrimSpace(s.scanner.Text()) |
|
5ac549c…
|
lmata
|
228 |
} |
|
5ac549c…
|
lmata
|
229 |
return "" |
|
5ac549c…
|
lmata
|
230 |
} |
|
5ac549c…
|
lmata
|
231 |
|
|
5ac549c…
|
lmata
|
232 |
func (s *setupScanner) ask(prompt, def string) string { |
|
5ac549c…
|
lmata
|
233 |
if def != "" { |
|
5ac549c…
|
lmata
|
234 |
fmt.Printf("%s [%s]: ", prompt, def) |
|
5ac549c…
|
lmata
|
235 |
} else { |
|
5ac549c…
|
lmata
|
236 |
fmt.Printf("%s: ", prompt) |
|
5ac549c…
|
lmata
|
237 |
} |
|
5ac549c…
|
lmata
|
238 |
v := s.readLine() |
|
5ac549c…
|
lmata
|
239 |
if v == "" { |
|
5ac549c…
|
lmata
|
240 |
return def |
|
5ac549c…
|
lmata
|
241 |
} |
|
5ac549c…
|
lmata
|
242 |
return v |
|
5ac549c…
|
lmata
|
243 |
} |
|
5ac549c…
|
lmata
|
244 |
|
|
5ac549c…
|
lmata
|
245 |
func (s *setupScanner) secret(prompt string) string { |
|
5ac549c…
|
lmata
|
246 |
fmt.Printf("%s: ", prompt) |
|
5ac549c…
|
lmata
|
247 |
return s.readLine() |
|
5ac549c…
|
lmata
|
248 |
} |
|
5ac549c…
|
lmata
|
249 |
|
|
5ac549c…
|
lmata
|
250 |
func (s *setupScanner) confirm(prompt string, def bool) bool { |
|
5ac549c…
|
lmata
|
251 |
yn := "y/N" |
|
5ac549c…
|
lmata
|
252 |
if def { |
|
5ac549c…
|
lmata
|
253 |
yn = "Y/n" |
|
5ac549c…
|
lmata
|
254 |
} |
|
5ac549c…
|
lmata
|
255 |
fmt.Printf("%s [%s]: ", prompt, yn) |
|
5ac549c…
|
lmata
|
256 |
v := strings.ToLower(strings.TrimSpace(s.readLine())) |
|
5ac549c…
|
lmata
|
257 |
if v == "" { |
|
5ac549c…
|
lmata
|
258 |
return def |
|
5ac549c…
|
lmata
|
259 |
} |
|
5ac549c…
|
lmata
|
260 |
return v == "y" || v == "yes" |
|
5ac549c…
|
lmata
|
261 |
} |
|
5ac549c…
|
lmata
|
262 |
|
|
5ac549c…
|
lmata
|
263 |
func (s *setupScanner) choice(prompt string, options []string, def string) string { |
|
5ac549c…
|
lmata
|
264 |
fmt.Printf("%s\n", prompt) |
|
5ac549c…
|
lmata
|
265 |
for i, o := range options { |
|
5ac549c…
|
lmata
|
266 |
marker := " " |
|
5ac549c…
|
lmata
|
267 |
if o == def { |
|
5ac549c…
|
lmata
|
268 |
marker = "→ " |
|
5ac549c…
|
lmata
|
269 |
} |
|
5ac549c…
|
lmata
|
270 |
fmt.Printf(" %s%d) %s\n", marker, i+1, o) |
|
5ac549c…
|
lmata
|
271 |
} |
|
5ac549c…
|
lmata
|
272 |
fmt.Printf(" choice [%s]: ", def) |
|
5ac549c…
|
lmata
|
273 |
v := strings.TrimSpace(s.readLine()) |
|
5ac549c…
|
lmata
|
274 |
if v == "" { |
|
5ac549c…
|
lmata
|
275 |
return def |
|
5ac549c…
|
lmata
|
276 |
} |
|
5ac549c…
|
lmata
|
277 |
// Accept number or name. |
|
5ac549c…
|
lmata
|
278 |
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= len(options) { |
|
5ac549c…
|
lmata
|
279 |
return options[n-1] |
|
5ac549c…
|
lmata
|
280 |
} |
|
5ac549c…
|
lmata
|
281 |
for _, o := range options { |
|
5ac549c…
|
lmata
|
282 |
if strings.EqualFold(o, v) { |
|
5ac549c…
|
lmata
|
283 |
return o |
|
5ac549c…
|
lmata
|
284 |
} |
|
5ac549c…
|
lmata
|
285 |
} |
|
5ac549c…
|
lmata
|
286 |
return def |
|
5ac549c…
|
lmata
|
287 |
} |
|
5ac549c…
|
lmata
|
288 |
|
|
5ac549c…
|
lmata
|
289 |
func splitComma(s string) []string { |
|
5ac549c…
|
lmata
|
290 |
var out []string |
|
5ac549c…
|
lmata
|
291 |
for _, p := range strings.Split(s, ",") { |
|
5ac549c…
|
lmata
|
292 |
if p = strings.TrimSpace(p); p != "" { |
|
5ac549c…
|
lmata
|
293 |
out = append(out, p) |
|
5ac549c…
|
lmata
|
294 |
} |
|
5ac549c…
|
lmata
|
295 |
} |
|
5ac549c…
|
lmata
|
296 |
return out |
|
5ac549c…
|
lmata
|
297 |
} |