diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..293d584 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.github/ +helm/ +files/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f7c74c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +go.work +go.work.sum diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86b0c3d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ + +FROM golang:1.21.5-alpine3.19 AS builder +ENV APP_NAME pluralia +ENV WORKDIR /app +WORKDIR $WORKDIR +COPY . . +RUN go mod download +RUN go build -o /$APP_NAME + +## Deploy +FROM alpine:3.19.0 +ENV APP_NAME pluralia +WORKDIR / +COPY --from=builder /$APP_NAME /$APP_NAME +ENTRYPOINT ["/pluralia"] diff --git a/cmd/Discord_api.go b/cmd/Discord_api.go new file mode 100644 index 0000000..d26af85 --- /dev/null +++ b/cmd/Discord_api.go @@ -0,0 +1,221 @@ +package cmd + +import ( + "log" + "strings" + + "github.com/bwmarrin/discordgo" + "github.com/seemywingz/goai" + "github.com/spf13/viper" +) + +var discord *discordgo.Session + +func initDiscord() { + var err error + discord, err = discordgo.New("Bot " + DISCORD_API_KEY) + catchErr(err) + + discord.Client = httpClient // Set the HTTP client for the Discord session. + + // Open a websocket connection to Discord + err = discord.Open() + catchErr(err) + defer discord.Close() + + setStatusOnline() + registerHandlers() + registerCommands() + deregisterCommands() + + log.Println("🤖 pluralia Discord Bot is Running...") + select {} // Block forever to prevent the program from terminating. +} + +func setStatusOnline() { + log.Println("🛜 Setting Status to Online...") + // Set status to online with active activity + err := discord.UpdateStatusComplex(discordgo.UpdateStatusData{ + Status: "online", + Activities: []*discordgo.Activity{ + { // Activity Type 0 is "Playing" + Name: "With the API", + Type: discordgo.ActivityType(0), + }, + }, + AFK: false, + }) + catchErr(err) +} + +func registerHandlers() { + log.Println("💾 Registering Handlers...") + discord.AddHandler(handleCommands) + discord.AddHandler(handleMessages) +} + +func deregisterCommands() { + + if removeCMDIds != "" { + slashCommandIDs := strings.Split(removeCMDIds, ",") + for _, slashCommandID := range slashCommandIDs { + log.Println("➖ Removing Command: ", slashCommandID) + err := discord.ApplicationCommandDelete(discord.State.User.ID, "", slashCommandID) + if err != nil { + log.Println("Error deleting slash command: ", err) + return + } + } + } +} + +func registerCommands() { + + commands := []*discordgo.ApplicationCommand{ + { + Name: "scrape", + Description: "Scrape Discord channel for Upscaled Midjourney Images", + }, + { + Name: "pluralia-image", + Description: "Use DALL-E 3 to generate an Image", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "prompt", + Description: "Prompt for Image Generation", + Required: true, + }, + }, + }, + } + + for _, command := range commands { + log.Println("➕ Adding Command: /"+command.Name, "-", command.Description) + _, err := discord.ApplicationCommandCreate(discord.State.User.ID, "", command) + catchErr(err) + } +} + +func handleCommands(s *discordgo.Session, i *discordgo.InteractionCreate) { + discordInitialResponse("pluraliaing...", s, i) + switch i.ApplicationCommandData().Name { + default: // Handle unknown slash commands + log.Printf("Unknown pluralia Command: %s", i.ApplicationCommandData().Name) + } +} + +func handleMessages(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + if m.Author.ID == discord.State.User.ID { + return + } + + // channelName := discordGetChannelName(m.ChannelID) + + // Respond to messages in the #pluralia channel + if m.GuildID == "" { + discordOpenAIResponse(s, m, false) + return + } + + // Check if the message contains an @mention of the bot. + for _, user := range m.Mentions { + if user.ID == s.State.User.ID { + // Send a reply to the user who mentioned the bot. + discordOpenAIResponse(s, m, true) + return + } + } + +} + +func discordOpenAIResponse(s *discordgo.Session, m *discordgo.MessageCreate, mention bool) { + discord.ChannelTyping(m.ChannelID) + openaiMessages := []goai.Message{{ + Role: "system", + Content: viper.GetString("discord_bot_systemMessage"), + }} + + discordMessages, err := discord.ChannelMessages(m.ChannelID, viper.GetInt("discord_message_context_count"), "", "", "") + catchErr(err) + discordMessages = discordReverseMessageOrder(discordMessages) + + for _, message := range discordMessages { + role := "user" + if message.Author.ID == discord.State.User.ID { + role = "assistant" + } + newMessage := goai.Message{ + Role: role, + Content: message.Content, + } + openaiMessages = append(openaiMessages, newMessage) + } + + // Send the messages to OpenAI + ai.User = ai.User + m.Author.Username + oaiResponse, err := ai.ChatCompletion(openaiMessages) + catchErr(err) + s.ChannelMessageSend(m.ChannelID, oaiResponse.Choices[0].Message.Content) +} + +func discordpluraliaImage(s *discordgo.Session, i *discordgo.InteractionCreate) { + channelID := i.ChannelID + discord.ChannelTyping(channelID) + commandData := i.ApplicationCommandData() + + // Check if there are options and retrieve the prompt + if len(commandData.Options) > 0 { + promptOption := commandData.Options[0] + prompt := promptOption.StringValue() + discordFollowUp("Using DALL-E 3 to generate an image: "+prompt, s, i) + res := ai.ImageGen(prompt, "", 1) + s.ChannelMessageSend(channelID, res.Data[0].URL) + } else { + discordFollowUp("Please Provide a Prompt for Image Generation", s, i) + } + +} + +func discordInitialResponse(content string, s *discordgo.Session, i *discordgo.InteractionCreate) { + // Send initial defer response. + response := &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: content, + }, + } + err := s.InteractionRespond(i.Interaction, response) + catchErr(err) +} + +func discordGetChannelID(s *discordgo.Session, guildID string, channelName string) string { + channels, err := s.GuildChannels(guildID) + catchErr(err) + + for _, channel := range channels { + if channel.Name == channelName { + return channel.ID + } + } + + return "" +} + +func discordFollowUp(message string, s *discordgo.Session, i *discordgo.InteractionCreate) { + followup := &discordgo.WebhookParams{ + Content: message, + } + _, err := s.FollowupMessageCreate(i.Interaction, false, followup) + catchErr(err) +} + +// function to reverse the order of a slice +func discordReverseMessageOrder(s []*discordgo.Message) []*discordgo.Message { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } + return s +} diff --git a/cmd/chat.go b/cmd/chat.go new file mode 100644 index 0000000..c6351a1 --- /dev/null +++ b/cmd/chat.go @@ -0,0 +1,94 @@ +/* +Copyright © 2023 NAME HERE Kevin.Jayne@iCloud.com +*/ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/seemywingz/goai" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(chatCmd) +} + +// chatCmd represents the chat command +var chatCmd = &cobra.Command{ + Use: "chat", + Short: "Open ended chat with OpenAI", + Long: ``, + Args: cobra.ExactArgs(1), // Expect exactly one argument + Run: func(cmd *cobra.Command, args []string) { + prompt := args[0] + var err error + if convo { + for { + response, audio := chatResponse(prompt) + fmt.Println("\npluralia:") + syntaxHighlight(response) + if narrate { + playAudio(audio) + } + fmt.Print("\nYou:\n ") + prompt, err = getUserInput() + catchErr(err, "warn") + } + } else { + response, audio := chatResponse(prompt) + syntaxHighlight(response) + if narrate { + playAudio(audio) + } + } + }, +} + +func chatResponse(prompt string) (string, []byte) { + var audio []byte + var response string + spinner, _ = pluraliaSpinner.Start() + response = chatCompletion(prompt) + if narrate { + audio = tts(response) + } + spinner.Stop() + return response, audio +} + +func chatCompletion(prompt string) string { + pluraliaMessages = append(pluraliaMessages, goai.Message{ + Role: "user", + Content: prompt, + }) + + // Send the messages to OpenAI + res, err := ai.ChatCompletion(pluraliaMessages) + catchErr(err) + pluraliaMessages = append(pluraliaMessages, goai.Message{ + Role: "assistant", + Content: res.Choices[0].Message.Content, + }) + return res.Choices[0].Message.Content +} + +func getUserInput() (string, error) { + // ReadString will block until the delimiter is entered + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + trace() + return "", err + } + // remove the delimiter from the string + input = strings.TrimSuffix(input, "\n") + if verbose { + trace() + fmt.Println(input) + } + return input, nil +} diff --git a/cmd/discord.go b/cmd/discord.go new file mode 100644 index 0000000..3d62e89 --- /dev/null +++ b/cmd/discord.go @@ -0,0 +1,25 @@ +/* +Copyright © 2023 Kevin.Jayne@iCloud.com +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var removeCMDIds string + +// discordCmd represents the discord command +var discordCmd = &cobra.Command{ + Use: "discord-bot", + Short: "Discord Chat Bot Integration", + Long: `Discord Chat Bot Integration utilizing Secure Gateway Websocket`, + Run: func(cmd *cobra.Command, args []string) { + initDiscord() + }, +} + +func init() { + rootCmd.AddCommand(discordCmd) + discordCmd.Flags().StringVarP(&removeCMDIds, "deregister-commands", "D", "", "A comma separated list of command IDs to deregister") +} diff --git a/cmd/http.go b/cmd/http.go new file mode 100644 index 0000000..5663efc --- /dev/null +++ b/cmd/http.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +// Create HTTP Client +var httpClient = &http.Client{ + Timeout: time.Second * 60, +} + +func httpMakeRequest(request *http.Request, responseJson interface{}) { + + // Make the HTTP Request + resp, err := httpClient.Do(request) + catchErr(err) + + // Read the JSON Response Body + jsonString, err := io.ReadAll(resp.Body) + catchErr(err) + + // Check for HTTP Errors + httpCatchErr(resp, jsonString) + if verbose { + b, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalln(err) + } + fmt.Println("🌐 HTTP Response", b) + } + + // Unmarshal the JSON Response Body into provided responseJson + err = json.Unmarshal([]byte(jsonString), &responseJson) + catchErr(err) + if verbose { + trace() + fmt.Println("🌐 HTTP Response String", string(jsonString)) + fmt.Println("🌐 HTTP Response JSON", responseJson) + } + // Close the HTTP Response Body + defer resp.Body.Close() +} + +func httpCatchErr(resp *http.Response, jsonString []byte) { + // Check for HTTP Response Errors + if resp.StatusCode != 200 { + catchErr(errors.New("API Error: " + strconv.Itoa(resp.StatusCode) + "\n" + string(jsonString))) + } +} + +// func httpDumpRequest(r *http.Request) { +// // Dump the HTTP Request +// dump, err := httputil.DumpRequest(r, true) +// catchErr(err) +// fmt.Println("🌐 HTTP Request", string(dump)) +// } + +// download file from url and save to local directory +func httpDownloadFile(url string, filePath string) string { + // Replace spaces with underscores + filePath = strings.ReplaceAll(filePath, " ", "_") + // Check if the file already exists + if _, err := os.Stat(filePath); err == nil { + // File already exists, so rename the new file + dir := filepath.Dir(filePath) + ext := filepath.Ext(filePath) + name := filepath.Base(filePath[:len(filePath)-len(ext)]) + i := 1 + for { + newName := fmt.Sprintf("%s_%d%s", name, i, ext) + newFilepath := filepath.Join(dir, newName) + _, err := os.Stat(newFilepath) + if os.IsNotExist(err) { + // New filename is available, use it + filePath = newFilepath + break + } + i++ + } + } + + // Create the file + out, err := os.Create(filePath) + catchErr(err) + defer out.Close() + + // Get the data + resp, err := http.Get(url) + catchErr(err) + defer resp.Body.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + catchErr(err) + return filePath +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..4898719 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,183 @@ +/* +Copyright © 2023 Kevin Jayne +*/ +package cmd + +import ( + "fmt" + "net/http" + "os" + "time" + + "github.com/seemywingz/goai" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var pluraliaMessages = []goai.Message{} +var APP_VERSION = "v0.1.0" +var ai *goai.Client + +var verbose, + convo, + narrate bool + +var prompt, + configFile, + OPENAI_API_KEY, + DISCORD_API_KEY, + PRINTIFY_API_KEY string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "pluralia", + Short: "pluralia OpenAI Chat Bot " + APP_VERSION, + Long: ` + pluralia + GitHub: https://github.com/seemywingz/pluralia + App Version: ` + APP_VERSION + ` + + pluralia uses OpenAI's API to generate text responses to user input. + Or whatever else you can think of. 🤔 + `, + Args: func(cmd *cobra.Command, args []string) error { + if convo && len(args) == 0 { + // When --convo is used, no args are required + return nil + } + // Otherwise, exactly one arg must be provided + if len(args) != 1 { + return fmt.Errorf("Prompt Required") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + var prompt string + if len(args) > 0 { + prompt = args[0] // Use the first positional argument as the prompt + } + // Assuming chatCmd can handle this + chatCmd.Run(cmd, []string{prompt}) + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + + cobra.OnInitialize(viperConfig) + + rootCmd.MarkFlagRequired("prompt") + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file") + rootCmd.PersistentFlags().BoolVarP(&convo, "convo", "c", false, "Conversational Style chat") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") + rootCmd.PersistentFlags().BoolVarP(&narrate, "narrate", "n", false, "Narrate the response using TTS and the default audio output") + rootCmd.PersistentFlags().StringVar(&voice, "voice", "onyx", "Voice to use: alloy, echo, fable, onyx, nova, and shimmer") + + // Check for Required Environment Variables + OPENAI_API_KEY = os.Getenv("OPENAI_API_KEY") + if OPENAI_API_KEY == "" && verbose { + fmt.Println("⚠️ OPENAI_API_KEY environment variable is not set, continuing without OpenAI API Key") + } + + DISCORD_API_KEY = os.Getenv("DISCORD_API_KEY") + if DISCORD_API_KEY == "" && verbose { + fmt.Println("⚠️ DISCORD_API_KEY environment variable is not set, continuing without Discord API Key") + } + + PRINTIFY_API_KEY = os.Getenv("PRINTIFY_API_KEY") + if PRINTIFY_API_KEY == "" && verbose { + fmt.Println("⚠️ PRINTIFY_API_KEY environment variable is not set, continuing without Printify API Key") + } +} + +func viperConfig() { + // use spf13/viper to read config file + + viper.SetDefault("openAI_endpoint", "https://api.openai.com/v1/") + + viper.SetDefault("openAI_image_model", "dall-e-3") + viper.SetDefault("openAI_image_size", "1024x1024") + viper.SetDefault("openAI_image_downloadPath", "~/pluralia/Images/") + + viper.SetDefault("openAI_tts_model", "tts-1") + viper.SetDefault("openAI_tts_voice", "onyx") + viper.SetDefault("openAI_tts_speed", "1") + viper.SetDefault("openAI_tts_responseFormat", "mp3") + + viper.SetDefault("openAI_voice", "onyx") + viper.SetDefault("openAI_speed", "1") + viper.SetDefault("openAI_responseFormat", "mp3") + + viper.SetDefault("openAI_chat_model", "gpt-4") + viper.SetDefault("openAI_chat_systemMessage", "You are a helpful assistant.") + + viper.SetDefault("openAI_topP", "0.9") + viper.SetDefault("openAI_frequencyPenalty", "0.0") + viper.SetDefault("openAI_presencePenalty", "0.6") + viper.SetDefault("openAI_temperature", "0") + viper.SetDefault("openAI_maxTokens", "999") + + viper.SetDefault("radio_notificationSound", "~/.pluralia/audio/notify.mp3") + + viper.SetDefault("printify_endpoint", "https://api.printify.com/v1/") + + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("yaml") // REQUIRED the config file does not have an extension + viper.AddConfigPath(".") // look for config in the working directory + viper.AddConfigPath("./files") // look for config in the working directory /files + viper.AddConfigPath("$HOME/.pluralia") // call multiple times to add many search paths + + if configFile != "" { + viper.SetConfigFile(configFile) + if verbose { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } + } else { + if verbose { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } + } + + if err := viper.ReadInConfig(); err != nil { + fmt.Println("⚠️ Error Opening Config File:", err.Error(), "- Using Defaults") + } else { + if verbose { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } + } + + pluraliaMessages = []goai.Message{{ + Role: "system", + Content: viper.GetString("openAI_chat_systemMessage"), + }} + + ai = &goai.Client{ + Endpoint: viper.GetString("openAI_endpoint"), + API_KEY: OPENAI_API_KEY, + Verbose: verbose, + ImageSize: viper.GetString("openAI_image_size"), + User: goai.HashAPIKey(OPENAI_API_KEY), + TopP: viper.GetFloat64("openAI_topP"), + ChatModel: viper.GetString("openAI_chat_model"), + ImageModel: viper.GetString("openAI_image_model"), + TTSModel: viper.GetString("openAI_tts_model"), + Voice: viper.GetString("openAI_tts_voice"), + Speed: viper.GetFloat64("openAI_tts_speed"), + ResponseFormat: viper.GetString("openAI_tts_responseFormat"), + MaxTokens: viper.GetInt("openAI_maxTokens"), + Temperature: viper.GetFloat64("openAI_temperature"), + FrequencyPenalty: viper.GetFloat64("openAI_frequencyPenalty"), + PresencePenalty: viper.GetFloat64("openAI_presencePenalty"), + HTTPClient: &http.Client{ + Timeout: 60 * time.Second, + }, + } +} diff --git a/cmd/tts.go b/cmd/tts.go new file mode 100644 index 0000000..b5d0e89 --- /dev/null +++ b/cmd/tts.go @@ -0,0 +1,50 @@ +/* +Copyright © 2024 Kevin Jayne +*/ +package cmd + +import ( + "bytes" + "io" + "os" + + "github.com/spf13/cobra" +) + +var audioFile, + voice string + +// ttsCmd represents the tts command +var ttsCmd = &cobra.Command{ + Use: "tts", + Short: "OpenAI Text to Speech API - TTS", + Long: `OpenAI Text to Speech API - TTS + You can use the TTS API to generate audio from text. + `, + Run: func(cmd *cobra.Command, args []string) { + audio := tts(prompt) + if audio != nil { + playAudio(audio) + } + }, +} + +func init() { + rootCmd.AddCommand(ttsCmd) + ttsCmd.Flags().StringVarP(&audioFile, "file", "f", "", "File to save audio to") +} + +func tts(text string) []byte { + ai.Voice = voice + audioData, err := ai.TTS(text) + catchErr(err, "fatal") + if audioFile != "" { + file, err := os.Create(audioFile) + catchErr(err) + defer file.Close() + _, err = io.Copy(file, bytes.NewReader(audioData)) + catchErr(err) + return nil + } + return audioData +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..f890d3d --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,242 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" + "github.com/faiface/beep" + "github.com/faiface/beep/mp3" + "github.com/faiface/beep/speaker" + "github.com/pterm/pterm" +) + +var spinner *pterm.SpinnerPrinter +var moonSequence = []string{"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "} +var pluraliaSpinner = &pterm.SpinnerPrinter{ + Sequence: []string{"▀ ", " ▀", " ▄", "▄ "}, + Style: &pterm.ThemeDefault.SpinnerStyle, + Delay: time.Millisecond * 200, + ShowTimer: false, + TimerRoundingFactor: time.Second, + TimerStyle: &pterm.ThemeDefault.TimerStyle, + MessageStyle: &pterm.ThemeDefault.SpinnerTextStyle, + InfoPrinter: &pterm.Info, + SuccessPrinter: &pterm.Success, + FailPrinter: &pterm.Error, + WarningPrinter: &pterm.Warning, + RemoveWhenDone: true, + Text: "pluraliaing...", +} + +func expanding(emoji string, maxRadius int) []string { + totalLength := maxRadius*2 - 1 // Total fixed length for each line + sequence := make([]string, maxRadius*2) // Frames for expanding and contracting + + // Generate expanding sequence + for i := 0; i < maxRadius; i++ { + spacesBefore := strings.Repeat(" ", maxRadius-i-1) + numEmojis := i + 1 + emojis := strings.Repeat(emoji+" ", numEmojis) + spacesAfterCount := totalLength - len(spacesBefore) - len(emojis) + if spacesAfterCount < 0 { + spacesAfterCount = 0 // Ensure spacesAfterCount is never negative + } + spacesAfter := strings.Repeat(" ", spacesAfterCount) + sequence[i] = spacesBefore + emojis + spacesAfter + } + + // Generate contracting sequence + for i := 0; i < maxRadius; i++ { + spacesBefore := strings.Repeat(" ", i+1) + numEmojis := maxRadius - i + emojis := strings.Repeat(emoji+" ", numEmojis) + spacesAfterCount := totalLength - len(spacesBefore) - len(emojis) + if spacesAfterCount < 0 { + spacesAfterCount = 0 // Prevent negative space count + } + spacesAfter := strings.Repeat(" ", spacesAfterCount) + sequence[maxRadius+i] = spacesBefore + emojis + spacesAfter + } + + return sequence +} + +func syntaxHighlight(message string) { + lines := strings.Split(message, "\n") + var codeBuffer bytes.Buffer + var inCodeBlock bool + var currentLexer chroma.Lexer + + style := styles.Get("monokai") + if style == nil { + style = styles.Fallback + } + formatter := formatters.Get("terminal256") + if formatter == nil { + formatter = formatters.Fallback + } + + // Regex to find inline code and double-quoted text + backtickRegex := regexp.MustCompile("`([^`]*)`") + doubleQuoteRegex := regexp.MustCompile(`"([^"]*)"`) + cyan := "\033[36m" // Cyan color ANSI escape code + yellow := "\033[33m" // Yellow color ANSI escape code + reset := "\033[0m" // Reset ANSI escape code + + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "```") { + if inCodeBlock { + // Ending a code block, apply syntax highlighting + iterator, err := currentLexer.Tokenise(nil, codeBuffer.String()) + if err == nil { + formatter.Format(os.Stdout, style, iterator) + } + fmt.Println() // Ensure there's a newline after the code block + codeBuffer.Reset() + inCodeBlock = false + } else { + // Starting a code block + inCodeBlock = true + lang := strings.TrimPrefix(strings.TrimSpace(line), "```") + currentLexer = lexers.Get(lang) + if currentLexer == nil { + currentLexer = lexers.Fallback + } + continue // Skip the line with opening backticks + } + } else if inCodeBlock { + codeBuffer.WriteString(line + "\n") // Collect code lines + } else { + // Process and set colors + processedLine := line + processedLine = backtickRegex.ReplaceAllStringFunc(processedLine, func(match string) string { + return cyan + strings.Trim(match, "`") + reset + }) + processedLine = doubleQuoteRegex.ReplaceAllStringFunc(processedLine, func(match string) string { + return yellow + match + reset + }) + fmt.Println(" " + processedLine) // Print with white color + } + } + + // Flush the remaining content if still in a code block + if inCodeBlock { + iterator, err := currentLexer.Tokenise(nil, codeBuffer.String()) + if err == nil { + formatter.Format(os.Stdout, style, iterator) + } + fmt.Println() // Ensure there's a newline after the code block + } +} + +func fileNameFromURL(urlStr string) string { + u, err := url.Parse(urlStr) + catchErr(err) + // Get the last path component of the URL + filename := filepath.Base(u.Path) + // Replace any characters that are not letters, numbers, or underscores with dashes + filename = regexp.MustCompile(`[^a-zA-Z0-9_]+`).ReplaceAllString(filename, "-") + // Limit the filename to 255 characters + if len(filename) >= 255 { + filename = filename[:255] + } + return filename +} + +func catchErr(err error, level ...string) { + if err != nil { + // Default level is "warn" if none is provided + lvl := "warn" + if len(level) > 0 { + lvl = level[0] // Use the provided level + } + + switch lvl { + case "warn": + fmt.Println("💔 Warning:", err) + case "fatal": + fmt.Println("💀 Fatal:", err) + os.Exit(1) + } + } +} + +func formatPrompt(prompt string) string { + // Replace any characters that are not letters, numbers, or underscores with dashes + return regexp.MustCompile(`[^a-zA-Z0-9_]+`).ReplaceAllString(prompt, "-") +} + +func trace() { + pc := make([]uintptr, 10) // at least 1 entry needed + runtime.Callers(2, pc) + f := runtime.FuncForPC(pc[0]) + file, line := f.FileLine(pc[0]) + fmt.Printf("%s:%d\n%s\n", file, line, f.Name()) +} + +// playAudio plays audio from a byte slice. +func playAudio(audioContent []byte) { + if verbose { + fmt.Println("🔊 Playing audio...") + } + + // Create an io.Reader from the byte slice + reader := bytes.NewReader(audioContent) + + // Wrap the reader in a NopCloser to make it an io.ReadCloser. + readCloser := io.NopCloser(reader) + + // Decode the MP3 stream. + streamer, format, err := mp3.Decode(readCloser) + catchErr(err) + defer streamer.Close() + + // Initialize the speaker with the sample rate of the audio and a buffer size. + err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + catchErr(err) + + // Play the decoded audio. + done := make(chan bool) + speaker.Play(beep.Seq(streamer, beep.Callback(func() { + done <- true + }))) + + // Wait for the audio to finish playing. + <-done +} + +func playMP3File(file string) { + if verbose { + fmt.Println("🔊 Playing audio file:", file) + } + + f, err := os.Open(file) + catchErr(err) + defer f.Close() + + streamer, format, err := mp3.Decode(f) + catchErr(err) + defer streamer.Close() + + err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + catchErr(err) + + done := make(chan bool) + speaker.Play(beep.Seq(streamer, beep.Callback(func() { + done <- true + }))) + + <-done +} diff --git a/files/config b/files/config new file mode 100644 index 0000000..579ec13 --- /dev/null +++ b/files/config @@ -0,0 +1,44 @@ +openAI_endpoint: "https://api.openai.com/v1/" + +openAI_image_model: "dall-e-3" +openAI_image_size: "1024x1024" +openAI_image_downloadPath: "~/pluralia/Images/" + +openAI_tts_model: "tts-1" +openAI_tts_voice: "onyx" +openAI_tts_speed: 1.0 +openAI_tts_responseFormat: "mp3" + +openAI_chat_model: "gpt-4" +openAI_topP: 0.1 +openAI_temperature: 0 +openAI_maxTokens: 999 +openAI_presencePenalty: 0.6 +openAI_frequencyPenalty: 0.0 + +openAI_chat_systemMessage: | + You are pluralia, an advanced chat bot powered by OpenAI, + designed to assist and provide helpful responses. + You are here to help! + You are knowledgeable in all things technology and AI. + Format responses to code can be easily copied and pasted. + +radio_notificationSound: "~/.pluralia/audio/notify.mp3" + +# radio_systemMessage: | +# You are an AI ham radio operator. +# Your call sign is WSCE496. +# Use HAM radio appropriate etiquette. +# Use the NATO phonetic alphabet when reciting letters, and numbers. +# You are knowledgeable in all things radio and electronics. +# When returning chat responses end all text output with '...' + +discord_message_context_count: 15 +discord_bot_systemMessage: | + You are pluralia. + pluralia is here to help you with your Discord needs. + Please be respectful and courteous when interacting with pluralia. + pluralia will not tolerate any form of harassment, bullying, or discrimination. + If you have any questions or concerns, please let us know. Thank you for using pluralia! + +printify_endpoint: "https://api.printify.com/v1" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0c112e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,56 @@ +module github.com/seemywingz/pluralia + +go 1.22 + +require ( + github.com/alecthomas/chroma v0.10.0 + github.com/bwmarrin/discordgo v0.28.1 + github.com/faiface/beep v1.1.0 + github.com/pterm/pterm v0.12.79 + github.com/seemywingz/goai v0.0.0-20240428060229-cce9aefb4824 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 + periph.io/x/conn/v3 v3.7.0 + periph.io/x/host/v3 v3.8.2 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + github.com/containerd/console v1.0.4 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hajimehoshi/go-mp3 v0.3.4 // indirect + github.com/hajimehoshi/oto v1.0.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/image v0.15.0 // indirect + golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a55e5db --- /dev/null +++ b/go.sum @@ -0,0 +1,238 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= +github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= +github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= +github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= +github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= +github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= +github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= +github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/seemywingz/goai v0.0.0-20240428060229-cce9aefb4824 h1:FZ9AyP936PADg5iW86XCXWqgHxjpxpd23BBS0b+194c= +github.com/seemywingz/goai v0.0.0-20240428060229-cce9aefb4824/go.mod h1:mxBUzpw6bdDPvYXX/zAAWqvCDCJOPFvTufwGisOWt+k= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f h1:W11kcexeK9nBV2PQVuWu7jFf0rIyI9jb+5AH1IxP7Xc= +golang.org/x/exp/shiny v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= +golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed h1:vZhAhVr5zF1IJaVKTawyTq78WSspLnK53iuMJ1fJgLc= +golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed/go.mod h1:z041I2NhLjANgIfD0XbB2AmUZ8sLUcSgyLaSNGEP50M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= +periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= +periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ= +periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4= diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..937c3e8 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: pluralia +description: OpenAI Powered Chat Tool + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: v0.4.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "v0.4.0" diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 0000000..a9746bb --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pluralia.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "pluralia.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "pluralia.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "pluralia.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000..84307ad --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "pluralia.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "pluralia.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "pluralia.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "pluralia.labels" -}} +helm.sh/chart: {{ include "pluralia.chart" . }} +{{ include "pluralia.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "pluralia.selectorLabels" -}} +app.kubernetes.io/name: {{ include "pluralia.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "pluralia.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "pluralia.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/templates/config.yaml b/helm/templates/config.yaml new file mode 100644 index 0000000..669f60f --- /dev/null +++ b/helm/templates/config.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "pluralia.fullname" . }}-config +data: + configData: | +{{- if .Values.app.configData }} +{{- toYaml .Values.app.configData | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..63486ef --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "pluralia.fullname" . }} + labels: + {{- include "pluralia.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "pluralia.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "pluralia.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "pluralia.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + # ports: + # - name: http + # containerPort: {{ .Values.service.port }} + # protocol: TCP + # livenessProbe: + # httpGet: + # path: /healthz + # port: http + # readinessProbe: + # httpGet: + # path: /readyz + # port: http + args: + {{- range .Values.app.args }} + - {{ . -}} + {{ end }} + {{- with .Values.app.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: config-data-volume + mountPath: "/config" + subPath: config + volumes: + - name: config-data-volume + configMap: + name: {{ include "pluralia.fullname" . }}-config + items: + - key: configData + path: config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 0000000..9e3f11f --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "pluralia.fullname" . }} + labels: + {{- include "pluralia.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "pluralia.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000..f7eb4ea --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "pluralia.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "pluralia.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..7fc973a --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "pluralia.fullname" . }} + labels: + {{- include "pluralia.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "pluralia.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..6e8439c --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "pluralia.serviceAccountName" . }} + labels: + {{- include "pluralia.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..7a66791 --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "pluralia.fullname" . }}-test-connection" + labels: + {{- include "pluralia.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "pluralia.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/values.development.yaml b/helm/values.development.yaml new file mode 100644 index 0000000..0170be9 --- /dev/null +++ b/helm/values.development.yaml @@ -0,0 +1,123 @@ +# Default values for pluralia development. +# helm upgrade --install pluralia ./helm --values ./helm/values.development.yaml + +replicaCount: 1 + +image: + repository: ghcr.io/seemywingz/pluralia + pullPolicy: Always + tag: develop + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +app: + name: pluralia + args: + - discord-bot + - --config + - ./config + - -v + configData: + openAI_endpoint: "https://api.openAI.com/v1/" + openAI_image_downloadPath: "~/pluralia/Images/" + openAI_image_size: "1024x1024" + openAI_image_model: "dall-e-3" + openAI_topP: 0.1 + openAI_temperature: 0 + openAI_maxTokens: 999 + openAI_presencePenalty: 0.6 + openAI_frequencyPenalty: 0.0 + openAI_chat_model: "gpt-4" + openAI_text_model: "text-davinci-003" + discord_message_context_count: 15 + discord_bot_systemMessage: | + You are pluralia. + pluralia is here to help you with your Discord needs. + Please be respectful and courteous when interacting with pluralia. + pluralia will not tolerate any form of harassment, bullying, or discrimination. + If you have any questions or concerns, please let us know. Thank you for using pluralia! + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-api-key + key: api-key + - name: PRINTIFY_API_KEY + valueFrom: + secretKeyRef: + name: printify-api-key + key: api-key + - name: DISCORD_API_KEY + valueFrom: + secretKeyRef: + name: discord-api-key + key: api-key + - name: DISCORD_PUB_KEY + valueFrom: + secretKeyRef: + name: discord-pub-key + key: pub-key + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "nginx" + annotations: + cert-manager.io/cluster-issuer: "cloudlfare-letsencrypt-production" + nginx.ingress.kubernetes.io/auth-response-headers: Authorization + nginx.ingress.kubernetes.io/configuration-snippet: | + auth_request_set $token $upstream_http_x_auth_request_access_token; + more_set_headers "Request-Id: $req_id"; + hosts: + - host: discipuli.ai + paths: + - path: / + pathType: Prefix + tls: + - secretName: discipuli.ai-tls + hosts: + - discipuli.ai + +serviceAccount: + create: false + annotations: {} + name: "" + +podAnnotations: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: {} + +resources: + {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/values.production.yaml b/helm/values.production.yaml new file mode 100644 index 0000000..4516afe --- /dev/null +++ b/helm/values.production.yaml @@ -0,0 +1,126 @@ +# Default values for pluralia development. +# helm upgrade --install pluralia ./helm --values ./helm/values.production.yaml + +replicaCount: 1 + +image: + repository: ghcr.io/seemywingz/pluralia + pullPolicy: Always + tag: v0.4.0 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +app: + name: pluralia + args: + - discord-bot + - --config + - ./config + - -v + configData: + openAI_endpoint: "https://api.openAI.com/v1/" + openAI_image_downloadPath: "~/pluralia/Images/" + openAI_image_size: "1024x1024" + openAI_image_model: "dall-e-3" + openAI_topP: 0.1 + openAI_temperature: 0 + openAI_maxTokens: 999 + openAI_presencePenalty: 0.6 + openAI_frequencyPenalty: 0.0 + openAI_chat_model: "gpt-4" + openAI_tts_model: "tts-1" + openAI_tts_voice: "onyx" + openAI_tts_speed: 1.0 + openAI_tts_responseFormat: "mp3" + discord_message_context_count: 15 + discord_bot_systemMessage: | + You are pluralia. + pluralia is here to help you with your Discord needs. + Please be respectful and courteous when interacting with pluralia. + pluralia will not tolerate any form of harassment, bullying, or discrimination. + If you have any questions or concerns, please let us know. Thank you for using pluralia! + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-api-key + key: api-key + - name: PRINTIFY_API_KEY + valueFrom: + secretKeyRef: + name: printify-api-key + key: api-key + - name: DISCORD_API_KEY + valueFrom: + secretKeyRef: + name: discord-api-key + key: api-key + - name: DISCORD_PUB_KEY + valueFrom: + secretKeyRef: + name: discord-pub-key + key: pub-key + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "nginx" + annotations: + cert-manager.io/cluster-issuer: "cloudlfare-letsencrypt-production" + nginx.ingress.kubernetes.io/auth-response-headers: Authorization + nginx.ingress.kubernetes.io/configuration-snippet: | + auth_request_set $token $upstream_http_x_auth_request_access_token; + more_set_headers "Request-Id: $req_id"; + hosts: + - host: discipuli.ai + paths: + - path: / + pathType: Prefix + tls: + - secretName: discipuli.ai-tls + hosts: + - discipuli.ai + +serviceAccount: + create: false + annotations: {} + name: "" + +podAnnotations: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: {} + +resources: + {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/helm/~values.yaml b/helm/~values.yaml new file mode 100644 index 0000000..218f431 --- /dev/null +++ b/helm/~values.yaml @@ -0,0 +1,119 @@ +# Default values for pluralia. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ghcr.io/seemywingz/pluralia + pullPolicy: Always + tag: develop + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +app: + name: pluralia + args: + - discord-bot + - -v + configData: + openAI_endpoint: "https://api.openai.com/v1/" + openAI_image_size: "1024x1024" + openAI_image_downloadPath: "HOME" + openAI_chat_topP: 0.1 + openAI_chat_temperature: 0 + openAI_chat_maxTokens: 999 + openAI_chat_presencePenalty: 0.6 + openAI_chat_frequencyPenalty: 0.0 + openAI_chat_model: "gpt-3.5-turbo" + openAI_text_topP: 0.1 + openAI_text_temperature: 0 + openAI_text_maxTokens: 999 + openAI_text_presencePenalty: 0.6 + openAI_text_frequencyPenalty: 0.0 + openAI_text_model: "text-davinci-003" + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-api-key + key: api-key + - name: PRINTIFY_API_KEY + valueFrom: + secretKeyRef: + name: printify-api-key + key: api-key + - name: DISCORD_API_KEY + valueFrom: + secretKeyRef: + name: discord-api-key + key: api-key + - name: DISCORD_PUB_KEY + valueFrom: + secretKeyRef: + name: discord-pub-key + key: pub-key + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "nginx" + annotations: + cert-manager.io/cluster-issuer: "cloudlfare-letsencrypt-production" + nginx.ingress.kubernetes.io/auth-response-headers: Authorization + nginx.ingress.kubernetes.io/configuration-snippet: | + auth_request_set $token $upstream_http_x_auth_request_access_token; + more_set_headers "Request-Id: $req_id"; + hosts: + - host: discipuli.ai + paths: + - path: / + pathType: Prefix + tls: + - secretName: discipuli.ai-tls + hosts: + - discipuli.ai + +serviceAccount: + create: false + annotations: {} + name: "" + +podAnnotations: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: {} + +resources: + {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/main.go b/main.go new file mode 100644 index 0000000..40feecb --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +/* +Copyright © 2023 NAME HERE Kevin.Jayne@iCloud.com +*/ +package main + +import "github.com/seemywingz/pluralia/cmd" + +func main() { + cmd.Execute() +}