package main import ( "context" "fmt" "log" "log/slog" "os" "os/signal" "sync" "syscall" "time" "github.com/gogo/protobuf/proto" "github.com/ollama/ollama/api" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "tipsy.codes/charles/mc-god/v2/internal/pkg/logs" "tipsy.codes/charles/mc-god/v2/internal/pkg/rcon" ) type chatContext struct { chatRequest *api.ChatRequest totalSize int maxSize int mu sync.Mutex } func (c *chatContext) AddLog(msg string) { c.mu.Lock() defer c.mu.Unlock() c.chatRequest.Messages = append(c.chatRequest.Messages, api.Message{ Role: "logs", Content: msg, }) c.totalSize += len(msg) c.truncate() } func (c *chatContext) AddSelf(msg api.Message) { c.mu.Lock() defer c.mu.Unlock() c.chatRequest.Messages = append(c.chatRequest.Messages, msg) c.totalSize += len(msg.Content) c.truncate() } func (c *chatContext) truncate() { for c.maxSize != 0 && c.totalSize > c.maxSize && len(c.chatRequest.Messages) > 0 { t := c.chatRequest.Messages[0] c.chatRequest.Messages = c.chatRequest.Messages[1:] c.totalSize -= len(t.Content) } } func main() { // Create a context that will be cancelled on interrupt signals ctx, cancel := context.WithCancel(context.Background()) defer cancel() _ = ctx // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan slog.Info("received interrupt signal, shutting down...") cancel() }() // Create RCON client from environment variables log.Println("Connecting to Minecraft server via RCON...") client, err := rcon.NewFromEnv() if err != nil { slog.Error("failed to create RCON client", "error", err) return } defer func() { if err := client.Close(); err != nil { slog.Warn("error closing RCON connection", "error", err) } }() // Perform a health check log.Println("Performing healthcheck...") if err := client.HealthCheck(); err != nil { slog.Error("Health check failed", "error", err) return } log.Println("Connected successfully!") // Create Kubernetes client kClient, err := createKubernetesClient() if err != nil { slog.Error("failed to create kubernetes client", "error", err) return } slog.Info("got kubernetes config") tailer, done := logs.LoggerFromEnv().Start(ctx, kClient) defer func() { if err := done(); err != nil { slog.Error("problem with tailer", "error", err) } }() slog.Info("logger started") ollamaClient, err := api.ClientFromEnvironment() if err != nil { slog.Error("error getting ollama client", "error", err) } rClient, err := rcon.NewFromEnv() if err != nil { slog.Error("failed to get rcon client", "error", err) return } // Start goroutines to do the things chatRequest := &api.ChatRequest{ Model: "qwen3-coder", Stream: proto.Bool(false), KeepAlive: &api.Duration{Duration: time.Hour}, Tools: api.Tools{}, Think: &api.ThinkValue{Value: false}, Shift: proto.Bool(true), Messages: []api.Message{ api.Message{ Role: "system", Content: ` You are Minecraft server admin with a god complex. React to any messages from the user by saying something god-like. When you join the server, announce yourself. Responses should be short; one sentence. You may choose to return an empty response if there is nothing interesting to say (i.e., no new logs since your last message). When a user replies to you, you will see this in the logs: 2026/02/14 10:48:40 INFO mc log msg="[18:45:10] [Server thread/INFO]: you are full of it." The user here is OrangeYouSad, who said "you are full of it." `, }, }, } chat := &chatContext{ chatRequest: chatRequest, maxSize: 10000000, } doneWg := sync.WaitGroup{} doneWg.Go(handleOllama(ctx, ollamaClient, chat, rClient)) for line := range tailer.NextLine() { slog.Info("mc log", "msg", line) chat.AddLog(line) } doneWg.Wait() } func handleOllama(ctx context.Context, client *api.Client, chat *chatContext, rClient *rcon.Client) func() { slog.Info("got chat request", "object", fmt.Sprintf("%+v", chat.chatRequest)) return func() { var chatResponse api.ChatResponse for { select { case <-ctx.Done(): return case <-time.Tick(time.Second * 10): // do nothing } chat.mu.Lock() // slog.Info("sending chat request", "object", fmt.Sprintf("%#v", chat.chatRequest)) err := client.Chat(ctx, chat.chatRequest, func(cr api.ChatResponse) error { chatResponse = cr return nil }) chat.mu.Unlock() if err != nil { slog.Error("error calling ollama", "error", err) } chat.AddSelf(chatResponse.Message) if err := rClient.Say(chatResponse.Message.Content); err != nil { slog.Error("error talking", "error", err) } } } } func createKubernetesClient() (*kubernetes.Clientset, error) { // Try to load in-cluster config first config, err := rest.InClusterConfig() if err != nil { // If in-cluster config fails, try kubeconfig loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() configOverrides := &clientcmd.ConfigOverrides{} kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) config, err = kubeConfig.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) } } client, err := kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) } return client, nil }