// Command ai-bot is a plaintext Matrix bot user (@ai) that answers xAI Grok // completions in its rooms: @-mentions in group rooms and every message in a // 1:1. It runs as a Synapse application service — Synapse pushes transactions to // its HTTP endpoint — and talks the Matrix CS-API over plain HTTP (no Olm/Megolm, // Vojo rooms are unencrypted by default), calling the xAI OpenAI-compatible Chat // Completions API. See README.md and docs/plans/grok_bot.md. package main import ( "context" "fmt" "os" "os/signal" "strings" "syscall" ) func main() { logger := newLogger() // `ai-bot generate-registration` writes a fresh registration.yaml with random // tokens (the mautrix bridge idiom), then exits. Runs BEFORE LoadConfig — the // tokens don't exist yet. Inputs: BOT_MXID (required), REGISTRATION_PATH // (default /data/registration.yaml), AS_URL (default http://ai-bot:8009). if len(os.Args) > 1 && os.Args[1] == "generate-registration" { mxid := getenv("BOT_MXID", "") if mxid == "" { logger.Error("BOT_MXID is required to generate the registration") os.Exit(1) } path := getenv("REGISTRATION_PATH", "/data/registration.yaml") asURL := getenv("AS_URL", "http://ai-bot:8009") if err := GenerateRegistration(path, asURL, localpartOf(mxid), serverOf(mxid)); err != nil { logger.Error("generate-registration failed", "err", err) os.Exit(1) } fmt.Printf("wrote %s\n", path) fmt.Println("Next: mount this file into Synapse, add it to app_service_config_files,") fmt.Println("and restart Synapse. The bot reads its tokens from this file (set") fmt.Println("REGISTRATION_PATH to the same path in the bot's environment).") return } cfg, err := LoadConfig() if err != nil { logger.Error("config error", "err", err) os.Exit(1) } // Load the system prompt up front so a missing/unreadable file fails fast // at startup rather than on the first message. promptBytes, err := os.ReadFile(cfg.SystemPromptPath) if err != nil { logger.Error("cannot read system prompt", "path", cfg.SystemPromptPath, "err", err) os.Exit(1) } cfg.SystemPrompt = string(promptBytes) // Load the curated project KB the same way (fail fast at startup, not on the first // product question) when the route is enabled. LoadConfig already required the path; here // we read it and reject an empty file (fail-closed — an empty KB would ground nothing). // A KB much larger than the prompt budget is also refused so it can't blow maxPromptTokens // (insertSystemNote adds it AFTER history truncation). Off → ProjectKB stays "". if cfg.ProjectKBEnabled { kbBytes, err := os.ReadFile(cfg.ProjectKBPath) if err != nil { logger.Error("cannot read project KB", "path", cfg.ProjectKBPath, "err", err) os.Exit(1) } cfg.ProjectKB = string(kbBytes) if strings.TrimSpace(cfg.ProjectKB) == "" { logger.Error("PROJECT_KB_PATH is empty", "path", cfg.ProjectKBPath) os.Exit(1) } if t := estimateTokens(cfg.ProjectKB); t > maxProjectKBTokens { logger.Error("project KB is too large for the prompt budget", "path", cfg.ProjectKBPath, "est_tokens", t, "max", maxProjectKBTokens) os.Exit(1) } } // `ai-bot check-config` validates env + prompt + state dir and exits 0. // Used by the A1 acceptance check ("container starts, reads env") and as a // cheap operator smoke test without touching the homeserver. if len(os.Args) > 1 && os.Args[1] == "check-config" { fmt.Println(cfg.Summary()) fmt.Printf(" SYSTEM_PROMPT = loaded (%d bytes)\n", len(cfg.SystemPrompt)) if cfg.ProjectKBEnabled { fmt.Printf(" PROJECT_KB = loaded (%d bytes, ~%d tokens)\n", len(cfg.ProjectKB), estimateTokens(cfg.ProjectKB)) } fmt.Println("config OK") return } if err := os.MkdirAll(cfg.StateDir, 0o700); err != nil { logger.Error("cannot create state dir", "path", cfg.StateDir, "err", err) os.Exit(1) } fmt.Fprintf(os.Stderr, "%s\n", cfg.Summary()) logger.Info("starting Vojo AI bot") // Cancel on SIGINT/SIGTERM so the transaction server shuts down cleanly. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() bot, err := NewBot(ctx, cfg, logger) if err != nil { logger.Error("startup failed", "err", err) os.Exit(1) } defer bot.Close() if err := bot.Run(ctx); err != nil && ctx.Err() == nil { logger.Error("appservice server exited", "err", err) os.Exit(1) } logger.Info("shut down cleanly") }